OpenCLI converges any website, Electron desktop app, and external CLI into one opencli <site> <command>, then add one opencli browser <session> <command> to drive the browser live.
It usesthe user's real, logged-in Chrome on their local machine(browser extension + local daemon), not an incognito instance, not a sandbox. This single fact determines almost all the rules in this Skill.
This Skill targets the fork we maintain ourselves (yan-labs/OpenCLI), and differs from upstream jackwener/opencli
see the difference list in references/our-fork.md。
Zero, choose the entry point and consolidate
Acceptance entry point:scripts/accept.mjs. For example:
node <opencli-skill-dir>/scripts/accept.mjs --url http://localhost:3000/ --url /pricing --widths 1440,390 --height 900 --out /tmp/site-accept
Three iron rules of acceptance: ① Use only OpenCLI to drive real Chrome, not Playwright, Firefox/Nightly, or a sandbox browser; ② make fixed acceptance into scripts that output only a summary and save screenshots to disk; ③ capability gaps must be stated explicitly: the new extension supports real console/pageerror capture and continuous viewport set/reset, probe template capabilities and verify readings; an empty console return from the old extension cannot be treated as no errors, viewport falls back to same-origin iframe and records the reason; external domains can only be detected and logged, not yet blocked. See references/browser-driving.md。
OpenCLI handles page operations and data acquisition in the user's real Chrome; research topics first go into agent-reach, SEO / backlink decisions first go into rankup / backlink. For public text, use curl or a public API first; use this Skill when you need login state, screenshots, E2E, form filling, or real browser testing. Submission decisions belong to the business Skill. For browsers, use only the user's locally logged-in Chrome; sandbox browsers (Playwright / agent-browser / headless) are not an option on this machine, including testing your own site, E2E, and screenshots, and are used only when the user explicitly names them.
Look in order: an existing project or Skill script → HTTP API → opencli <site> <command> adapter → opencli browser <session> → write an adapter when reuse is truly needed. First use opencli list、opencli <site> --help to check existing capabilities; do not re-walk a path that already has a script. For quota sites, first read Section 3 and run scripts/pressure.mjs。
Hard rule: once it works, consolidate it into a script; this is a completion condition and does not need the user to push you. Driving the browser live is the most expensive operation,the same path must never be walked manually a second time by the agent:
- Before opening the browserfirst check whether there is an existing script or adapter:
opencli list/opencli <site> --help, this Skill'sscripts/、rankupscripts/(GSC indexing, removals, exports), backlinkscripts/(Semrush/Similarweb). If there is one, run it directly; if it does not work,fix the script, and do not bypass it and click manually again. - If there is none, record while driving. If any one condition is met, you must consolidate: you will do it again, rerun with a different parameter, drive more than about 5 steps, or manually type the same command string more than 2 times. After it works the first time, immediately organize it into a parameterized script (
.mjs, callopencli browser/ batch, return the session with try/finally): SEO data fetching goes to rankup, backlinks and competitors go to backlink, each placed in that Skill'sscripts/; general site capabilities are written as an opencli adapter or placed in this Skill'sscripts/; those belonging to only one project go in<project>/.rankup/scripts/. At the top of the script, clearly state its purpose, parameters, login-state dependencies, known pitfalls (page lazy loading, row limits, quotas), and the verification date. - After writing it, run it once with real parameters, and only if it runs successfully does it count as accumulated.
- The delivery report must have a "## Accumulated Scripts" section: write the script path and verification command; if there is nothing to accumulate, write "None, reason: ...". If this section is missing, the task is judged incomplete and the main thread will send it back.
- Writing scripts can be delegated to third-party models (Codex, etc.) according to the global dispatch rules, but the responsibility of "must accumulate" lies with the current executor and cannot be offloaded.
For an entry example, see references/field-notes.md。
Minimal browser loop:
opencli browser site-acceptance open http://localhost:3000 --window dedicated
opencli browser site-acceptance state
opencli browser site-acceptance screenshot /tmp/site-acceptance.png
opencli browser site-acceptance close
opencli's built-in chatgpt adapter cannot read answers on the new version of the page; when you need web ChatGPT Q&A, use agent-fleet of fleet web start/say/close/list(chatgpt-web-ask.mjs): unlimited turns, keeps the page by default, and explicitly close it when done.
1. Before starting work: doctor
opencli doctor
doctor only diagnosesthe browser bridge(daemon + extension + Chrome connection).
PUBLIC / LOCAL strategy adapters,opencli list, and external CLI passthrough do not need it to be green.
COOKIE / INTERCEPT / UI strategy and all opencli browser * do need it.
When the behavior does not match this document, the first thing to do is check the extension version
All default behaviors described in this Skill live in the extension—starting from extension 1.5.x, the default is dedicated,opencli browser and adapter commands
use by default a dedicated window that does not steal focus; background must be specified explicitly, it does not switch away from the active tab, one tab group named after the session per session,
--window isolated、sessions reports windowId / groupTitle / windowFallbackReason.
If you installed the version from the Chrome Web Store,every command still succeeds, only the behavior returns to upstream:
foreground by default, opens its own window, steals the tab the user is currently viewing,isolated is ignored.
Such failures produce no error, only "why is this different from what the document says?" So:
| Observed | What to do |
|---|---|
| The command succeeds but the window/focus behavior does not match this document | Run opencli doctor, look at Extension the version on the line |
| Version < 1.0.33 | Tell the user they installed the Web Store version, they need to switch to the zip in yan-labs's Release , and remove or disable the store version |
doctor itself reports this |
Do as it says—it will print the download address and loading steps |
frames returns []for a cross-origin iframe that clearly exists, without an error |
Extensions < 1.1.0 do not have OOPIF support. It has been released with v1.9.0-yan.2; just install the tgz from the new Release and reload the extension, see Section 10 |
frames/contexts is normal, but eval --frame/--context silently reads the main page after opening and closing the panel a few times |
Known regression in extension 1.1.0, fixed in 1.1.1 (frame_not_attached error code criterion). See references/our-fork.md |
doctor will proactively report this problem when the extension is lower than 1.0.33,do not skip its output.
After changing the extension source code (or just pulling a new build), in chrome://extensions click reloadon OpenCLI—
if not reloaded, Chrome is still running the old version,doctor will indicate that the loaded version is below the minimum requirement; that is not a misinstallation, it is a failure to reload.
When it turns red, first check references/troubleshooting.md.
The first step in troubleshooting is always npm ls -g @jackwener/opencli confirm whether the CLI is a release build or linked to local source—
this step determines whether you then inspect the code or the environment; skipping it wastes an entire round.
doctor the first two lines are green and the third is redis a specific signal: the daemon and the extension are both alive,
what is broken is the command path between them, and restarting the daemon usually will not help.
Three, session discipline: the most expensive section of this Skill
opencli browser <session> in <session> is the ownership declaration for the tab.
Sessions with the same name share the same tab, while differently named ones do not interfere with each other. So the most common cause of "my tab was taken by someone else"
is:two tasks picked the same session name。
OpenCLI 1.8.7's daemon protects the same profile + surface + session: the secondconcurrent write
will queue locally, checking every 2 seconds; after the previous task finishes it continues automatically, and does not hand session_busy to the
outer Agent, preventing it from retrying immediately. The queue check only accesses the local daemon and does not access the target website; the first wait will
explicitly print the occupant, the reason for waiting, and the next check time. By default it waits at most 10 minutes; on timeout it will state that the command has not yet been sent to
Chrome/the target website, and require that you not retry immediately. Read operations can still run in parallel; a mixed batch containing any write operation
is handled as a write as a whole.
This only serializes writes at the same moment. Two tasks sequentially or alternately reusing the same session name will still operate on the same tab, and then read the page opened by the other, so the unique session name rule remains unchanged.
This lock also does not govern concurrency and rate limiting for site accounts. If a data source script cannot run concurrently on the same account, it still needs its own global lock.
Four laws (full measured data in references/session-laws.md)
| # | Law | One-sentence reason |
|---|---|---|
| 1 | One session, one tab; N pages require N session names | Three agents each use an independent name: cross-agent preemption 0 times. Sharing work: 3 / 12 / 2 times, and one of them read the wrong thing on every read.The only exception is quota sites, see the next section |
| 2 | Do not use tab new / tab select / open --tab to put multiple pages in one session |
All threesilentlyfailed: the command reported success, and the next read returned the wrong page. One three-agent run grew the user's Chrome from 11 tabs to 30 orphan pages |
| 3 | Never hardcode session names | opencli browser --help 's first example is work, and everyone who copied it ran into each other |
| 4 | Before starting work, open all the sessions you will use at once and get all the handles, then enter the work loop | Creating while using turns a theoretical race into a reproducible race |
Law 1 protects tab identity, not the site's server-side state. All sessions share the same Chrome
profile and the same logged-in identity, so if the site stores the "currently selected project/client" in a server-side session,
when one tab switches targets, the other tabs will change after refreshing—no matter how separate the session names are, this cannot be stopped.
Criterion: after switching targets in the site, does the URL change? If unchanged, verify first then parallelize;
for details see references/session-laws.md。
Automatic reclamation and temporary keep-alive
browser sessions are automatically reclaimed after 10 minutes idle by default; released blank placeholder windows are closed within 15 seconds.
In-flight commands also have a hard limit: 10 minutes by default; if the command's own timeout is longer, that timeout is respected;
if stuck beyond the limit, it is forcibly released during the reclamation check.OPENCLI_INFLIGHT_MAX_MS can override the default limit (positive integer milliseconds).
opencli browser captcha-help open https://example.com --keep-alive
opencli browser long-review --idle-timeout 1800 open https://example.com
opencli browser captcha-help close
--keep-alive cancels idle reclamation;--idle-timeout <秒> sets a positive integer number of seconds. Both can also be placed between the session name and the subcommand.
OPENCLI_BROWSER_IDLE_TIMEOUT is compatible with seconds, and also accepts never; command-line arguments take precedence over environment variables, then the default.
Do not use both command-line arguments at the same time;close and opencli browser cleanup can both release a keep-alive session.
Use only when waiting for a human to manually pass a CAPTCHA, for human-in-the-loop pages that need to stay open for a long time, or for brief pauses where a page must be retained between consecutive collections on quota-limited sites;
other than that, it should almost never be used.The user is responsible for the final close. When a brief or script uses keep-alive,
the session name and reason must be stated in the report;sub-agents must not use it by default; it must be explicitly authorized by the main thread in the brief。
Use opencli browser sessions to view occupants,[keep-alive] markers, and remaining time until reclamation (JSON has keepAlive、idleDeadlineAt);
use scripts/orphan-windows.mjs to read-only check for leftover blank windows.
Quota-site exception
For same-account quota-limited sites such as Semrush / Similarweb, use a fixed site session name, andput one visit into one batch; a single batch is serialized by the daemon, and a full collection round spanning multiple commands must also hold yan-tools-share-<tool>.lock. Do not let multiple agents collect in parallel: one collector fetches sequentially and writes to disk, and analysts read the files. Before starting, run:
node <opencli-skill-dir>/scripts/pressure.mjs --tool semrush
unknown means the session list cannot be read and must not be treated as zero occupancy. When a quota site is rate-limited, first close release the tab; on navigation timeout, first probe liveness in the original session extract . For fixed names, locks, intervals, and measured evidence, see references/field-notes.md and references/session-laws.md。
$$ is safe in scripts, unsafe in the Bash tool
This is a real incident we hit and must be distinguished:
| Scenario | $$ / process.pid behavior |
Correct approach |
|---|---|---|
| Node script(one process runs the whole time) | the same PID throughout the lifecycle, safe | let session = `ahs-${process.pid}` |
| Claude Code's Bash tool | each call is a new process, with a different PID | Usedescriptive literal constants(naver-birthstone、bing-check-mysite), or S=$(uuidgen | cut -c1-8) save into a file and read it back |
Verified incident (2026-08-23): a sub-agent used S="naver-bs-$$" to call OpenCLI repeatedly,
each command created a new session (a new blank tab), and the page opened by the previous one was abandoned.
The agent always saw a blank page, thought the page had not loaded properly, kept retrying, and eventually leaked 9 sessions.
Names should describe the work, not just be unique:backlink-probe-<后缀> beats bl-1.
The session name is the only identifier that exists; a unique but meaningless name still cannot answer "whose tab is this".
Do not hand-roll suffixes in JS; use scripts/opencli-core.mjs 's defaultSession(base);
In Bash source scripts/session.sh then S=$(oc_session <base>)——the batch that went wrong.
All the sessions were sent directly from the Bash tool, without going through that JS assistant at all.
Both sides have a guard that willrejectsession names ending in 3–6 digits (guardSessionName /
oc_guard_session), because that is the shape of $$ after expansion. This failure originally reported no error,
it only showed up as "why is the page always empty", so it must be made to go red on the spot.
Must be returned after use
opencli browser <session> close # 释放这一个
opencli browser sessions # 看现在还有谁活着,以及各自在哪个窗口
opencli browser cleanup # 释放**全部**——只有主线能跑,见下
A sub agent must explicitly close its own session in a finally block or before exit——it will not be cleaned up automatically on crash.
cleanup is for the main line only. What it releases isall on this machineof the leases, not "mine"——
if a sub agent runs it, it will close the tabs that sibling agents are using,
and those agents will only see their own pages mysteriously disappear. A kept session is a tab in the user's Chrome, looking exactly like whatever someone else is working on.
The parent should clean up with a set difference, do not use cleanup:
const before = await snapshotSessions(); // 扇出前存快照
// ... 扇出 ...
await reconcileSessions(before, { prefix: 'tm-' }); // 只关自己那批
It can collect the tabs left behind by crashed sub agents without touching a single sibling's.
prefix or sessions must be given——otherwise it only reports and does not act, because "newly appeared after the snapshot"
also includes sessions opened by sibling agents in the same period, and indiscriminately closing them degrades into cleanup
(in an actual test a single dry-run mixed in someone else's sweep2-*)。
The set difference is also faster than the idle alarm: in an actual test on 2026-08-28, 31 tabs were dropped by idle on their own, and until they dropped the user's tab bar was dirty the whole time.
Five window modes, the default is already the non-intrusive one
--window |
Behavior | When to use |
|---|---|---|
background |
must be explicitly specified. Opens the tab in the user's current normal window, without raising the window or switching the active tab.When background is explicitly specified,opencli browser and the adapter command (opencli <site> …) are both like this——since 1.0.33 the adapter no longer opens its own window |
Almost all cases |
active |
Make the tab the active tab of its window (the extension only calls chrome.tabs.update({active:true}), not chrome.windows.update({focused:true})), without raising the OS window, and the tab is not throttled.The placement is the same as background Same: when the user has their own Chrome window open, the tab is placed in the user's window, so it will switch away from the tab he is viewing; when the window is completely obscured by another app it still reads as hidden |
You want "selected/visible" but cannot steal OS focus, and have confirmed the user is not using that window; for stable visibility see "visible but not stealing focus" below |
foreground |
Raise the window (chrome.windows.update({focused:true}), bringing Chrome to the OS foreground) and select the tab |
Onlywhen the user needs to personally complete a CAPTCHA, or has explicitly said he wants to watch |
isolated |
Background, but not in the user's window——automation's own separate window (multiple isolated sessions share this one separate window, each still being its own tab group) | Long batch jobs, when you do not want to pile things up in the user's tab bar |
dedicated |
Default since extension 1.5.x, does not steal focus. Dedicated window for named slots:focused:false Create, never focus; by default autoSelect makes the session tab the active tab of that window before every command (visible); "foreign tabs" not opened by OpenCLI are evicted by default (evict) |
Long-running batch jobs, or lazy-loaded reports that need to actually render, but must not disturb the window the user is using |
The flag's position isbetween the session name and the subcommand(works even when placed after the subcommand):
opencli browser <session> --window isolated open "https://..."
Placed before the session namein frontwill report unknown command: <你的会话名>, which reads like a broken install, but is actually a syntax error.
dedicated For the full lifecycle, positioning, isolation, and observability details, see the "Dedicated window" subsection below.
Requires extension ≥ 1.0.33(opencli doctor that line is the criterion). On older extensions the default is still foreground,
isolated is silently ignored—that is exactly the trap in the table below.
background Only creates a new window when a normal window cannot be borrowed, and records the reason:
opencli browser sessions At the end of that line it displays [new window: <reason>](in JSON it is windowFallbackReason)。
| reason | meaning |
|---|---|
no-normal-window |
Chrome has no ordinary windows open (only app windows, popups, or no windows at all) |
all-incognito |
There are windows, but they are all incognito windows—incognito cookies are not the user's login state, so do not borrow them |
all-owned |
There are windows, but they are all created by us (for example, only an isolated window remains) |
query-failed |
The step of asking Chrome "which windows exist" itself failed |
When neither browser nor adapter can be borrowed, only createonestand-in window is shared; if the user later opens their own window, new sessions will follow it.
When this field is null, it means it landed in the user's own window, or it is what the user requested isolated。
For the two failure modes of old isolated , along with version and retest records, see references/field-notes.md. To determine the current version, run opencli doctor, then compare opencli browser sessions the default in and isolated 's windowId。
Never steal the user's browser focus
The Chrome on this machine is the one the user is using. Stealing focus is not "slightly worse experience"; it directly interrupts what they are doing—they are typing or viewing a page, and the window is raised and the tab is switched away.
| Wrong approach | Correct approach | Why it is wrong |
|---|---|---|
--window foreground(unless the user wants to operate it personally) |
Add nothing (since extension 1.5.x the default is dedicated, and does not steal focus); also use a dedicated window just for data retrieval | Testing shows it will switch the user'sactive tab away(jumping from the 1st to the 3rd). In the measurement on 2026-08-23, the frontmostappdid not change; but later extensions will, when creating a tab lease, chrome.windows.update({focused:true}), and starting 2026-09-13 there have been user reports of being repeatedly raised to the foreground—"foreground does not switch the foreground app" no longer holds, so do not use that as a reason to allow it |
| Use foreground when calling adapter for "convenient page viewing" | --keep-tab true + screenshot / state |
Debugging is a high-frequency action; one round can interrupt a dozen times. Leave the tab there; if the user wants to see it, they can switch over themselves |
Omit on old extensions (< 1.0.33) --window background |
First check doctor extended version of ; the old version had every command explicitly carry |
In the old version, both layers defaulted to foreground; omitting it was equivalent to raising the window once per command |
add to the PUBLIC / LOCAL command --window |
do not add | they do not accept this flag and will error with unknown option '--window'; such commands do not open a browser anyway |
| after a crash, no cleanup is done, leaving a bunch of orphan tabs behind | finally in close |
a leaked session in the user's window is just a bunch of inexplicable tabs, which is more annoying than stealing focus once |
Measured (2026-08-23, macOS + Chrome): in background mode open / eval / screenshot /
click / type throughout—the user window'sactive tab index does not change, and the tab count returns to baseline after close , the page-side document.hasFocus() is always false、visibilityState is always hidden。
On the same machine, switching to --window foreground, the active tab is immediately switched from the 1st to the 3rd.
This overturns the Skill's old conclusion as of 2026-08-22 that "neither mode steals focus"—
the old measurement only checked the "frontmost application" (which indeed does not change in foreground mode), missing the "active tab" axis.
See the full comparison table in references/session-laws.md。
This used to be broken and was fixed on 2026-08-23. At the time,
--window isolateddid not open a new window, behaving exactly the same asbackgroundso the docs recorded "there is no way to move the agent's tab out of the user's window." The real cause was that four layers each silently vetoed it: the runtime allowlist only recognized two values and droppedisolated; "is this window mine" was guessed (any window with all non-http pages counted as mine) and mistook a blank window the user casually opened as the container; after the window was created correctly, group convergence moved the tab back into the user's window; and choosing "which window the user is in" usedfocused, while when Chrome is not frontmost, all windows'focusedare false. Every layer reports no error, so after fixing each layer you thought it was fine.
How to confirm you have the fixed version:opencli doctor the Extension line is ≥ 1.0.33;
then run opencli browser <s> --window isolated open <url> after opencli browser sessions,
its line's windowId should differ from an explicit --window background session; when background borrows the user's windowthere is no [new window: …]。
dedicated window and visibility
| scenario | must use | do not use |
|---|---|---|
| only fetch data, no need for the page to actually render | must explicitly --window background |
do not add foreground / active |
| website acceptance, screenshots, E2E, lazy-load reports require real rendering | must use --window dedicated |
do not hand-write --window-slot、--window-display pointing to the main screen, manually calculating position |
| viewing mobile / H5 layout | must use --window dedicated --half |
do not simulate by changing the bounds width |
| need to specify window width and height (e.g. 390 / 1360 viewport comparison) | must use --window dedicated --window-bounds 0,0,<宽>,<高>: only when the bounds overlap with the user's current screen will it convert to auto grid and adopt the grid size; explicit bounds not converted to auto retain width and height, 0,0 is not guaranteed to migrate, see the exceptions after the table |
Do not write left/top as coordinates outside the primary screen to "pick a position" (leave position-picking to the grid) |
| Multiple pages need to be visible at the same time | Each page must use its own separate session name, each --window dedicated |
Do not use tab new / tab select within one session |
| The user must personally complete the captcha | You must use --window foreground, and tell the user which tab to go to and what to click |
Do not use foreground in any other situation |
| Quota sites (Semrush/Similarweb, etc.) | Must fix the site session name + one batch per visit (follow the Quota Sites section of this Skill) | Do not open multiple in parallel |
Only when the explicit bounds overlap with the user's current screen is it converted to the automatic grid, and the size follows the grid cell, no longer retaining the width/height from before migration long-term. The automated screen set is fixed as all non-primary screens, ordered by external/virtual screen priority first, then by ascending id numeric value; the primary screen is used only when no non-primary screen is available. The screen the mouse is on only affects the screen-selection preference for new windows; when other automated screens still have empty slots, prefer other screens, otherwise use the mouse screen as usual. When the user's current screen is on a negative-coordinate secondary screen,0,0,… If it does not intersect at all with the user's current screen, the original bounds are retained, and the window may appear on the primary screen. When current-screen detection is unavailable, use the primary screen as the fallback for migration judgment.
When a primary-screen position appears, first look at window status -f json and whether the current screen is exactly the primary screen, then judge in combination with the screen count; if migration is needed but did not happen, then check whether the extension version is ≥ 1.5.3. placement.source、relocatedFrom and whether the current screen is exactly the primary screen, then judge in combination with the screen count; if migration is needed but did not happen, then check whether the extension version is ≥ 1.5.3.
dedicated Use a dedicated slot window to keep the page visible, suitable for lazy-loaded reports, website acceptance, and screenshots; still uses the user's same Chrome and login state. Extension ≥ 1.2.0, CLI ≥ 1.10.0; first use opencli browser window status -f json to confirm supported and dedicated-window capability. Use --window-slot to separate sessions that need to be visible at the same time; for window positioning, environment variables, foreign-tab policy, and the legacy isolated + virtual screen approach, see references/field-notes.md。
The automatic grid is calculated from each screen's own workArea resolution; the target size is close to 1280×900, minimum 900×620 (degraded when the screen itself is smaller), and the maximum is preferentially kept within 1600×1100; if integer grid division cannot satisfy both the upper and lower limits at the same time, prioritize guaranteeing the minimum size and filling the work area. Cells have no large gaps and are independent of the current window count. For example, 5120×2850 is a 4×3 grid, about 1280×950; 2560×1440 is a 2×2 grid, 1280×720; 1512×949 is 1×1. Windows persistently occupy fixed slots by "displayId + tileIndex", row-first starting from the top left, filling one screen before filling the next (with the exception of the mouse screen's new-window preference); preferentially reuse the smallest empty slot. Opening/closing a window will not move or resize other windows, and moving the mouse will not migrate existing windows. Only after all screens' cells are used up do windows stack on the last screen; stacked windows may be judged by Chrome as hidden, so lazy-loaded reports should avoid exceeding the natural capacity.
Automatically placing windows (placement.source==='auto') will serially reconcile on screen changes, before each command acquires/creates a window, and at about 30-second alarms; dragging away from a slot will return it, and maximizing, fullscreen, and minimizing will first restore normal before returning it, without passing focused. When a screen is rebuilt or removed, only migrate windows that lost their screen; when the resolution changes, update that screen's cells. Explicit bounds / display placement is not affected by automatic reconciliation.opencli browser window relayout can immediately reconcile and show old position → new position;-f json returns windows[] of old、new(each containing bounds/state),displayId、tileIndex、changed and optional error。
Default idle reclaim is 15 seconds,OPENCLI_DEDICATED_IDLE_MS overridable; at lease end a setTimeout check runs, with alarm and lazy reclamation before each acquire/create as a fallback. The pool upper limit is the sum of grid cells across all automation displays, with a lower limit of 4;window status -f json The pool.capacity is the upper limit,pool.naturalCapacity is the total non-overlapping capacity,pool.automationDisplays[] lists each display's id, name, area, naturalCapacity,cols、rows、tile: {width,height}, so you can directly see each display's cols×rows and cell size; leftover pixels from the division are padded onto the edge cells. The compatibility field automationDisplay refers only to the first display,windows[] retains tileIndex and adds displayId, optional reconciledAt(the time the last completion returned to position or the first alignment, as a millisecond timestamp). The two 5120×2850 workspaces total 24 cells.--window-display <名称片段> / OPENCLI_WINDOW_DISPLAY still follows the matched display's old grid-partition rules and does not cross to other displays.
Parallel tasks use their own sessions directly and do not queue ahead of time waiting for slots; when the pool is full (live>=pool.capacity) it first reclaims windows with no holder/lease by idle time (both named slots and pool-N count), and only when all are busy does it report dedicated-pool-exhausted, at which point it waits for a task to release. Extension changes take effect only after chrome://extensions a reload; a reload interrupts running sessions, so wait until tasks are idle first.
opencli browser <session> --window dedicated --window-slot <slot> open <url>
opencli browser window status -f json
opencli browser window relayout -f json
Automatic display selection and half width: the CLI detects the display the mouse is on, providing a preference only for new window allocation, without changing the automation display set. Existing windows' displays and cells are fixed and will not be rearranged due to mouse changes or other windows opening/closing. The primary display is a candidate only when no usable non-primary display exists.placement.excludedDisplayBounds is retained as a compatibility field and no longer means the mouse display is excluded from the set; use windows[].displayId and pool.automationDisplays[] as authoritative. Callers do not need to manually compute cells or positions. The above fixed grid, self-healing, and relayout belong to the to-be-merged feat/window-layout behavior and must be accepted on the main thread and enabled in the corresponding build before use.
To see the mobile / H5 layout, add --half: the window still occupies one full cell slot, with width only half a cell wide, aligned to the left inside the cell; after the session ends and returns to the pool, it automatically restores full width, andwindow status / window list 's half field can be checked. Do not add it otherwise. This only shrinks the window width and does not emulate a mobile UA, touch, or DPR.
opencli browser <session> --window dedicated --half open <url>
Part four, discovery capability: don't memorize the command list, go ask
There are 160+ site adapters, and the number changes every week.Any list hardcoded in the docs will go stale, so this Skill does not list them.
opencli list # 按站点分组的表格
opencli list -f json # 机器可读,agent 用这个
opencli list | grep -i twitter # 找某个站
opencli <site> --help # 这个站有哪些命令
opencli <site> <command> --help # 位置参数、专属标志、输出列
opencli list -f json Each entry gives {site, name, aliases, description, strategy, browser, args, columns}。
strategy to decide whether a browser is needed:
| strategy | What is needed |
|---|---|
PUBLIC |
Nothing is needed, pure HTTP |
COOKIE |
Chrome is already logged in to that site + the extension is installed; commands take credentials from the live session, no need to log in again |
INTERCEPT |
Same as above, plus it opens an automation window to capture the signed request |
UI |
Same as above, full DOM interaction |
LOCAL |
No browser needed, connect to a local/dev endpoint |
Before falling back to bare opencli browser , first check whether an adapter already covers this workflow.
It is especially worthwhile on login sites that change frequently—the pitfalls encapsulated in the adapter would have to be stepped through again by driving on the spot.
Common flags (most adapter commands have them, with a few browser-related exceptions)
| Flag | Effect |
|---|---|
-f, --format <fmt> |
table(TTY default) · yaml(non-TTY default) · json · plain · md · csv。agent basically always wants -f json |
--trace <mode> |
off(default) · on · retain-on-failure. Used when troubleshooting and writing adapters |
-v, --verbose |
debug logs + failure stack |
--window <mode> |
background(must be specified explicitly) / active / foreground / isolated / dedicated(default since extension 1.5.x; semantics see "Five window modes" above).dedicated 's positioning/isolation parameters (slot, bounds, display) go through env or --window-slot / --window-bounds / --window-display, they are not governed by this flag itself.PUBLIC / LOCAL policy commands do not accept it—adding it directly reports unknown option '--window', it reads like something is broken, but actually this kind of command doesn't open a browser at all (measured on 342 public + 25 local commands). First look at strategy then decide whether to add it |
--site-session <mode> |
ephemeral(default) / persistent。For batch calls on the same site, always use persistent: reuse site:<x> one tab, and if already in the domain skip the site-root pre-navigation; the default mode opens a new tab every time and navigates the site root first, which looks like "constantly refreshing the homepage". See session-laws |
--keep-tab <bool> |
whether to keep the tab lease after it ends |
V. Field-driven: minimal closed loop
S="recon-pricing" # 描述性常量,Bash tool 里不要用 $$
opencli browser "$S" open "https://example.com/pricing"
opencli browser "$S" state # 拿到带 [N] 编号的快照
opencli browser "$S" click 7
opencli browser "$S" wait selector "[data-loaded]" --timeout 15000
opencli browser "$S" state # 页面变了就必须重新 state
opencli browser "$S" close
Four mental models, enough to read all the returns:
- Selector-first target contract: every interaction command acceptsone
<target>, either the numeric ref given bystate/findor a CSS selector. When multiple match, use--nth <n>to disambiguate. - Every envelope reports
matches_nandmatch_level(exact/stable/reidentified). The CLI has already rescued you from moderate DOM drift,match_leveltells you how much to trust it. - Ask for compact output first, full output only when needed:
stateis a budget-aware snapshot;networkgives a shape preview first, then use--detail <key>to fetch a single body. Spewing a huge payload is just burning context for nothing. - Errors are machine-readable: failure returns
{error: {code, message, hint?, candidates?}}。 branch oncode, do not match message strings.
For the full command table, target contract, compound form controls, cost table, recipes and pitfalls, see
references/browser-driving.md。
The three most commonly violated rules
- Look before you act. first
stateorfind. Numeric refs areunique to each snapshot, never hardcode them from memory across sessions. - If the page changes, re-
state。 navigation, form submission, and SPA route switching all invalidate old refs— invalid is still the good case, worse isreidentifiedonto a similarly shaped element on the new page. - On ordinary pages use
evalfor reading, and wrap in an IIFE. In this environment the eval context persists across calls, redeclaration throws an errorand that call didn't execute at all. To change the page useclick/type/select/keys, they have structured output and fingerprints,evaldoes not. For special interactions with extension-injected iframes see the next section.
Cross-origin iframe: really usable since 2026-09-11
Cross-origin iframe (includingside panels injected by other browser extensions—it is usually one
inside a shadow root
<iframe src="https://<厂商域>/">) can now frames list,eval --frame N directly read/write the DOM,
No clipboard needed, no coordinate-based screenshots. Requires extension ≥ 1.1.0; below that, frames silently returns []。
Three counterintuitive prerequisites: extension hotkeys must be dispatched with eval synthetic KeyboardEvent (browser keys won't reach the extension layer), React buttons inside iframes must dispatch the full pointer/mouse sequence (.click() is ineffective),
restoring the panel never reloads the page(after reload the frame target is unavailable, and opencli silently falls back to executing in the main page).
For usage,frames --debug troubleshooting table, and tested reference scripts, see
references/browser-driving.md 's "Cross-origin iframes and extension-injected panels".
batch: run multiple steps in one call
fixed sequences (open → wait → eval)always use batch, it reuses a single Page connection, saving the connect—parse—teardown overhead paid for each command.
opencli browser "$S" batch --commands '[
{"cmd": "open", "args": {"url": "https://example.com"}},
{"cmd": "wait", "args": {"selector": ".loaded", "timeout": 10000}},
{"cmd": "state", "args": {"source": "dom"}}
]'
Returns {cmd, index, ok, result?, error?} array; by default continues on error,--stop-on-error switches to aborting.
In the fixed-version CLI, any step with ok:false or fill verified:false the whole command exits non-zero,index indicating the failing step; older versions may exit 0, so scripts must still check each step's result. Screenshot parameters use {path, width, height, fullPage, annotate}。
Conditional logic(each step determines the next) uses sequential calls, do not force it into batch.
Model-driven auto(only when the installed version supports it)
As of 2026-09-26,auto recorded in a branch not yet merged into fork/main 's feat/jev-auto branch; check opencli browser <session> --helpfirst, and do not treat it as a released capability. When supported, you can give --goal、--data multi-step navigation or form filling; submission requires explicit --allow-submit, captchas and login walls stop for a human. For usage, gates, tested results, and known limitations see references/model-driven.md and references/field-notes.md。
VI. Data fetching and persisting to disk
Data-fetch order when the page has no API
network— if the page's data comes from a JSON interface,the interface is almost always more reliable than the rendered DOM. Firstnetworkto see the shape, then--detail <key>to fetch that one item.extract— long-form body text, returns with anext_start_charcursor, loop until it isnull。eval— point extraction when neither of the former two is suitable.- Scroll and scrape the table — a fallback method, not the default method.Before scraping, spend a minute looking for that free export button。
Three pitfalls you must know before scraping
- Same-name control trap: on the same report there are often two export controls side by side with highly similar names, one goes through paid quota, the other exports the current page for free, and their behaviors are completely opposite.Whenever you are about to write "a certain feature is unavailable", first confirm you are not clicking the other control with the same name.
- Export models can be completely different across reports in the same tool. Verifying "can only export page by page" in report A does not constitute a conclusion about report B. Every time you switch to another report, take another look at the export panel.
- Export triggers are often
<svg>icons, with no.click()method, you mustclosest('button,[role=button],a')go up to find the real button; for panels mounted asynchronously you need topoll until the button appears, do not use fixed sleep or coordinate clicks.
Persisting to disk: captured data must not be left in the download directory
Prefer a local receiver: start a service that only listens on 127.0.0.1 and have the page fetch(..., {method:'POST'})
send data directly into the project directory. It eliminates four problems at once—no need to wait for files to all arrive, no need to merge duplicate names,
unaffected by download directory permissions, and it doesn't consume conversation context.
The receiver's port must not be hardcoded as a constant, for exactly the same reason session names must not be hardcoded: when two projects start at the same time,
the second instance EADDRINUSE can't start, and the common pattern of running it as a background daemon will dump output into /dev/null——
this failure is completely silent, and then the page's fetch still returns 200, hittinganother project's receiver。
For the complete disk-persistence SOP (receiver implementation, wait-for-all criteria, duplicate name merging, manifest validation), see
references/data-extraction.md。
VII. What to do when things break
Come back and check the evidence after a problem occurs: the daemon's logs are categorized under ~/.opencli/logs/,
opencli daemon logs(default errors) / commands / extension / daemon,
supports -n and --grep. It only records starting from the daemon's next startup; anything before that was not kept.
A native alert may cause eval and the same session's dialog accept to queue up and time out together; if you hit consecutive timeouts, first check access-report.mjs --suspicious, and handle it according to the steps in references/field-notes.md . Daemon logs do not contain HTTP status codes or response bodies;scripts/opencli-core.mjs separately records site-access.jsonl, use the report below to see routing, callers, and degradation evidence:
node <opencli-skill-dir>/scripts/access-report.mjs --since 2h
node <opencli-skill-dir>/scripts/access-report.mjs --suspicious
node <opencli-skill-dir>/scripts/access-report.mjs --degraded
degraded is an observation result, not equivalent to an automatic retry; the original criteria, sampling, and limitations are in the same reference file.
| Symptom | Where to look first |
|---|---|
doctor red,session_not_found, daemon/extension issues |
references/troubleshooting.md |
Just daemon restart and the extension can't connect |
service worker is dead asleep:open -g -a "Google Chrome" "https://example.com" wake up. Restarting the daemon again won't help, see references/troubleshooting.md |
| The page you read back is not the one you navigated to | Suspect session name collision first, then suspect the site or CLI. For the diagnostic order, see references/session-laws.md |
selector_not_found / stale_ref / click succeeds but there is no response |
references/browser-driving.md 's troubleshooting table |
opencli <site> <command> failed because the site changed layout |
Use --trace retain-on-failure to get evidence, and follow references/adapters.md 's self-repair process to modify the adapter |
Hard stop conditions for self-repair(do not modify code):AUTH_REQUIRED(tell the user to log in in Chrome),
BROWSER_CONNECT(tell the user to run doctor), CAPTCHA / rate limiting. Repair budget is at most 3 rounds.
"Empty" does not equal "broken". EMPTY_RESULT is often not an adapter bug: the platform will
actively downgrade results under anti-scraping heuristics, and sites will also use HTTP 200 + empty body instead of a real 404. Try another query term,
take a look with your own eyes in a normal tab, and only enter the repair process if it reproduces—otherwise you are patching a normal adapter.
VIII. CAPTCHA: automating to the last step
When encountering nodes that cannot be automated, such as CAPTCHA or SMS verification codes,complete all the steps that can be automated before that—— Form filled in, options selected, page open—leave only that one click to the user, and clearly tell them which tab in the browser right now, and what needs to be clicked。
Don't dump the whole SOP back on the user, and don't write a string of "please go to https://…,然后输入…」。 The goal is to reduce the user's effort from "a whole workflow" to "a single click".
9. Reference Files
| File | When to read |
|---|---|
references/session-laws.md |
When sessions/tabs have problems; before multiple agents start work in parallel |
references/browser-driving.md |
Need to operate the page live: click, fill forms, wait, read, screenshot |
references/data-extraction.md |
Need to fetch data back and persist it: network / extract / scrape tables / receiving end |
references/adapters.md |
Need to write a new adapter, or fix a broken adapter |
references/troubleshooting.md |
doctor red, can't connect, the command reports self-contradictory errors |
references/drivers.md |
Someone asks "why not use X"; or the OpenCLI path really is a dead end |
references/our-fork.md |
The command doesn't exist on someone else's machine; before upgrading/syncing upstream |
references/field-notes.md |
Need low-frequency command details, old-version failures, and raw records from actual testing |
Bundled scripts
| Script | What it does |
|---|---|
scripts/appfigures.mjs |
Appfigures public app overview, supports product ID / URL and sequential batching; outputs month, region, post-deduction download/revenue estimates, new ratings, and access status; when logged in, can read the store keyword table (subject to what --help supports for reports).--help to view parameters; product ID is not Apple App ID, and revenue ranges and missing values are not disguised as precise numbers. |
scripts/opencli-core.mjs |
Minimal wrapper for JS callers:defaultSession() / sessionForUrl() generates safe session names,openAndExtract() packages a single visit into an atomic batch,sequentialCrawl() sequential collection with intervals,reconcileSessions() difference-based reclamation,sleepStep() true sleep,batchBrowser() / openAndEval() wraps batch |
scripts/session.sh |
The same set on the Bash tool side:oc_session <base>、oc_session_for <url>(quota sites auto-converge),oc_guard_session rejects $$ names shaped like |
scripts/pressure.mjs |
Pre-work self-check: can we start now. how many tabs each quota site has (split into "mine / shared / others'"), whether limits are reached, which pid has held the tools-share lock and for how long, whether that process is still alive, adjudicates go / wait / stale-lock / unknown and gives specific actions.--tool <key> looks at only one tool,--json machine-readable, exit codes 0/2/3/4 can be used directly as gates.Stale locks are only reported, not deleted—deleting someone else's lock is more dangerous than waiting |
scripts/daemon-restart-safe.mjs |
Safe version of restarting the daemon: refuse if a collection task is running (--force can force it), after restart confirm the bridge really came back, and if it didn't, wake the service worker |
scripts/access-report.mjs |
Read site-access.jsonl for review: frequency and p50/p95 by route, who opened tabs by caller,--suspicious pick suspicious lines,--degraded look only at detectDegradation throttling/degradation determined by |
scripts/orphan-windows.mjs |
Read-only diagnosis of dedicated-window leftovers: compare against the blank about:blank#opencli-dedicated=<slot> window in Chrome and window status, classify into held / idle-ok / idle-overdue(reclamation failed) / untracked(extensions are unrecognized and will not be automatically reclaimed); with the latter two categories present, exit code is 2. Does not close any windows |
tests/quota-sites.test.mjs |
Pure-function tests for the above guardrails, without touching the browser:node --test opencli/tests/quota-sites.test.mjs |
tests/pressure.test.mjs |
pressure.mjs pure-function tests, with the session list and lock state all injected, without touching the browser |
scripts/receiver.mjs |
Local receiver: the page POSTs data into the project directory, bypassing the download directory. The port is derived from the project root, crashes on occupation,/ping reports root,/script feeds the extractor source code per the whitelist |
node <opencli-skill-dir>/scripts/receiver.mjs --root . --out data/<主题>/raw
Do not rewrite the receiver every time. A self-written version will almost certainly miss the rule "must crash when the port is occupied", and the consequence of missing that rule is not a crash, but data being silently written into another project's directory.
10. Installation and Updates
npx skills add yan-labs/yan-skills --skill opencli -g -y
npx skills update opencli -g -y
OpenCLI itself consists of two halves,both halves must have our build installed, the source is the Release of yan-labs/OpenCLI:
# 1) CLI
npm i -g https://github.com/yan-labs/OpenCLI/releases/download/v1.9.0-yan.3/opencli-cli-1.9.0-yan.3.tgz
# 2) 浏览器扩展:下载 opencli-extension-v*.zip 解压,
# chrome://extensions → 开启开发者模式 → 加载已解压的扩展程序
# ⚠️ 先移除或停用 Chrome 应用商店那个 OpenCLI
# 3) 验证:三行都要 [OK],Extension 那行的版本 ≥ 1.0.33
opencli doctor
Why the app store version cannot be used: The default behavior described by this Skill — starting from extension 1.5.x, dedicated is the default; background must be specified explicitly,
browser and adapter default to using a dedicated window that does not steal focus, do not switch away from the user's active tab, one tab group per session,
--window isolated、sessions reports windowId / groupTitle / windowFallbackReason —
all only exist in our build. The store version defaults to foreground; if installed, this Skill's rules will not match actual behavior.
Installing both at the same time will also make them both connect to the daemon and fight each other.
CLI 1.9.0 / extension 1.1.1(cross-origin iframe support,frames --debug、browser <会话> clipboard,
and the OOPIF eval routing fixed in extension 1.1.1 — a known regression where, after repeatedly opening and closing the panel, eval silently falls back to the main page)
have been released with v1.9.0-yan.3 , see references/our-fork.md. If
frames --debug reports unknown option, it means the globally installed one is still the old tgz;npm i -g just use that new URL above
; on the extension side remember to reload in chrome://extensions , and after installation verify per step 3 the version number printed by doctor .
See the diff list at references/our-fork.md。
After modifying the extension source code, you must manually reload once in chrome://extensions for it to take effect—
changes on the CLI side only need a daemon restart; changes on the extension side do not take effect automatically.opencli doctor The extension version printed by it
is the criterion: whatever it displays is what is loaded.
X Search and Free Original-Post Tracing
node scripts/x-research.mjs read POST_URL --depth 1: Reads known posts and reply parent posts through the free FxTwitter public API, without using Chrome cookies; quoted posts are trimmed along with the response. depth defaults to 0, max 3; stops on error, no automatic retry.
node scripts/x-research.mjs search 'QUERY' --scrolls 0: OpenCLI opens the real Chrome search page, reads the DOM only, and outputs trimmed JSON; by default only the currently loaded screen, scrolling at most 3 times. This is not the complete search universe; parent post ID is kept as null when unknown, and external links may still be t.co. The webpage itself will still request X, and account rate limiting cannot be avoided. Do not run continuously against an account that is already rate-limited.
Verified on 2026-09-17: FxTwitter reply → parent post, quoted posts, and game external links succeeded; the real Chrome page shows "Something went wrong"; normal search DOM extraction still awaits acceptance after the account recovers. page_error is not equivalent to a confirmed HTTP 429. Run the offline check:node tests/x-research.test.mjs。