* refactor(browser): replace --session flag with <sessionname> positional
The `--session <name>` flag was semantically required but syntactically
optional, which is an anti-pattern. Required + flag is a contradiction:
flag form implies "optional", required is a runtime patch on top. Session
is OpenCLI's "operation target" identifier — the natural form for that is
a positional argument, like `docker exec <container> <cmd>` or
`git checkout <branch>`.
New surface:
opencli browser <sessionname> open https://x.com
opencli browser <sessionname> click 12
opencli browser <sessionname> bind
opencli browser <sessionname> unbind
Commander 14 cannot natively combine a parent positional with subcommand
dispatch — the parent's positional is shadowed by subcommand matching. To
bridge that, main.ts now pre-processes argv: when the token after `browser`
is non-flag and not a known subcommand name, it is treated as the
sessionname and rewritten to the internal `--session <name>` flag form
before commander parses it. Help text on the `browser` command is
overridden via `.usage('<sessionname> <command> [options]')` so users see
the positional form.
Reserved subcommand names (33) are listed in cli-argv-preprocess.ts and
tested for parity with cli.ts subcommand registrations. If a future
subcommand is added, the test fails loudly.
Synced surfaces:
- README.md / README.zh-CN.md — all examples
- docs/guide/browser-bridge.md (+ zh)
- skills/opencli-browser/SKILL.md (bind/unbind, examples, table)
- skills/opencli-usage/SKILL.md
- tests/e2e/browser-tabs.test.ts
- CHANGELOG.md (Unreleased BREAKING)
The internal `--session` flag and the unit tests calling
`program.parseAsync(['...', 'browser', '--session', 'foo', ...])` are
preserved as a stable internal API: tests bypass main.ts pre-processing
and exercise commander directly. The pre-processor has its own targeted
test file (cli-argv-preprocess.test.ts, 10 tests, all green).
Verification:
- npx tsc --noEmit — pass
- npx vitest run --project unit — 1073/1074 pass (1 unrelated skip)
- npx vitest run --project extension — 61/61 pass
- npm run check:typed-error-lint — baseline 189
- npm run check:silent-column-drop — baseline 103
* fix(cli-argv): only rewrite when `browser` is the root command
The preprocessor was looping through every argv slot and would mis-rewrite
occurrences of the literal word `browser` deeper in argv (e.g. `opencli
adapter init browser/x` or arg values containing `browser`).
Now the preprocessor walks past leading root flags + their values to
identify the root command token, and only acts when that token is
`browser`. The full set of root value-consuming flags
(`ROOT_VALUE_FLAGS`) is documented inline and kept in sync with the
`program.option()` calls in cli.ts.
Adds regression tests:
- `opencli adapter init browser x` not rewritten
- URL/path values containing `browser` not rewritten
- `list browser state` (different root command) not rewritten
- `--profile work browser foo state` correctly identifies `foo` as
sessionname (not as --profile's value)
- `--profile=work` long-form-with-equals consumes one slot only
- boolean flags (`-v`) don't consume the next value
12/12 preprocessor tests pass.
* fix(cli-argv): hide --session flag, fail-fast on retired form, rename to <session>
Three blockers in #1505 review:
1. `--session` flag was still visible in `opencli browser --help` and could
be used as a public entrance, contradicting "positional only" UX.
Fix: switch from `.requiredOption()` to `.addOption(new Option(...).hideHelp())`.
The flag is preserved as an internal API for the daemon protocol and direct
`program.parseAsync` callers (tests), but is no longer documented or
surfaced in structured help.
2. `opencli browser --session foo state` still succeeded. Now the argv
preprocessor throws `BrowserSessionArgvError` when root `browser` is
followed by `--session`, and main.ts catches it and exits with a
user-facing usage error pointing to the positional form.
3. Missing-session error message exposed the internal flag:
`required option '--session <name>' not specified`. Now `getBrowserSession()`
in the action body throws `<session> is a required positional argument:
opencli browser <session> <command>`, and commander no longer guards the
hidden option.
Also (per @WAWQAQ) rename placeholder `<sessionname>` -> `<session>` everywhere
user-facing — shorter, matches CLI convention. The help text "<session> is a
required positional: pass the name of the browser session..." carries the
"name" semantics in description, not in the placeholder itself.
Sync surfaces:
- src/cli.ts — usage line, addOption with hideHelp, descriptions
- src/cli-argv-preprocess.ts — throw on --session form
- src/cli-argv-preprocess.test.ts — refusal test for old form
- src/cli.test.ts — assertions updated for hidden option + new error path
- src/help.ts — read `_usage` private field to respect `.usage()` override
(commander's `.usage()` getter returns auto-generated form if not set,
which would otherwise pollute every namespace's usage string)
- src/main.ts — catch BrowserSessionArgvError, stderr + exit
- README.md / README.zh-CN.md
- docs/guide/browser-bridge.md / docs/zh/guide/browser-bridge.md
- skills/opencli-browser/SKILL.md / skills/opencli-usage/SKILL.md
- CHANGELOG.md
Manual smoke tests (against built dist):
- `opencli browser --help` shows `Usage: opencli browser <session> <command> [options]`
- `opencli browser --help` Options block does NOT show `--session`
- `opencli browser --session foo state` → friendly error, no commander stacktrace
- `opencli browser state` → `<session> is a required positional argument: opencli browser <session> <command>`
- `opencli browser foo state` → parses correctly
* fix: inject <session> into subcommand help paths and drop stale sessions ref
Two follow-up blockers from #1505 review:
1. Subcommand help and structured help still rendered the command path
without the parent's positional. `opencli browser foo state --help`
showed `Usage: opencli browser state [options]`, which would lead
users (and agents reading structured help) to think
`opencli browser state` was a valid invocation. Now:
- `commanderPath()` injects an ancestor's leading-positional placeholder
(extracted from its `.usage()` override) between the ancestor's name
and the next path segment when building paths upward.
- `commandPathFromRoot()` strips placeholder segments (e.g. `<session>`)
from the relative `name` field so agents can still address subcommands
by their leaf name; placeholders remain in the `command` / `usage`
display paths.
- `program.configureHelp({ commandUsage: ... })` is applied recursively
to every descendant of `browser`, because commander does NOT inherit
`configureHelp` into subcommands.
Result:
opencli browser <session> click --help
-> Usage: opencli browser <session> click [target] [options]
Daemon, plugin, adapter, profile namespaces (no `.usage()` override)
are unaffected.
2. `skills/opencli-browser/SKILL.md` still referenced
`opencli browser sessions`, which was removed in #1470. Replaced the
sentence with the underlying invariant ("Bound sessions have no
OpenCLI idle-close timer; the binding lasts until `unbind`, tab close,
window close, or daemon restart") without mentioning the deleted
command.
Tests:
- cli.test.ts: structured help expectations updated to include
`<session>` in command/usage paths (3 tests)
- cli-argv-preprocess.test.ts: 12 tests still green
- 1136/1137 unit+extension green (1 unrelated skip)
- typed-error-lint baseline 189
- silent-column-drop baseline 103
Doctor's job is browser-bridge health diagnosis. The `--no-live` flag
let users skip the connectivity probe (= the core diagnostic), and
`--sessions` listed automation sessions (a separate concern not part of
health). Both flags accreted features that violated the command's
first-principles purpose.
Cleanup chain (removing dead code surfaced by the flag removal):
- `--no-live` / `--sessions` flags removed from `opencli doctor`
- `DoctorOptions.live` / `DoctorOptions.sessions` removed
- `DoctorReport.sessions` removed
- `[SKIP] Connectivity` render branch removed (always-live now)
- `listSessions()` removed (only consumer was doctor)
- `'sessions'` action removed from daemon-client protocol type
- `BrowserSessionInfo` type removed (no remaining consumers)
- extension `handleSessions` action handler removed (1.0.12)
- extension test "reports sessions per session" removed
- `OPENCLI_BROWSER_IDLE_TIMEOUT` test rewired to 'cookies' action
Verification:
- root typecheck + extension typecheck pass
- doctor.test.ts 17/17 pass
- extension/background.test.ts 49/49 pass
- typed-error-lint 189/189 baseline
- silent-column-drop 103/103 baseline
- build + extension build green
Add three local-first messaging CLIs to the External CLI registry so
agents can discover and install them via `opencli external install`:
- `tg-cli` (binary `tg`) — Telegram local sync/search/export via MTProto
- `discord-cli` (binary `discord`) — Discord local sync/search/export
- `wx-cli` (binary `wx`) — WeChat local data CLI
Refresh the External CLI list in skills/opencli-usage/SKILL.md so the
agent-facing skill names stay in sync.
Extends A0 (PR #1404) by dogfooding `installCommanderNamespaceStructuredHelp`
on the four remaining built-in Commander namespaces:
- `opencli daemon --help -f yaml|json`
- `opencli plugin --help -f yaml|json`
- `opencli adapter --help -f yaml|json`
- `opencli profile --help -f yaml|json`
Each emits the same payload shape as `browser`: namespace metadata, every
leaf command's positionals + command_options + description + usage,
namespace_options (empty for these), and program-level global_options.
Agents can fetch every leaf's contract in a single call — no per-leaf
`--help` follow-ups.
Each namespace snapshots its original description at declaration time
because `applyRootSubcommandSummaries(program)` later overwrites
`.description()` with a child-name listing; without the snapshot,
structured help would surface `"restart, status, stop"` instead of
`"Manage the opencli daemon"`. Tests lock the snapshot semantics for
`adapter` explicitly.
Tests: 138/138 (4 new — one per namespace, covering description
preservation, leaf names, positionals, command_options).
Typecheck + build clean.
Why
- `opencli twitter followers --help` rendered:
Arguments:
user
with a blank trailing column. Both humans and agents could not
recover the parameter's purpose without reading source. WAWQAQ
surfaced this directly: "没有说明当后面的 followers [user] [options]
如果都没填的时候,获取的是什么?"
- This is metadata completeness, not stylistic taste. Failing closed
is the only way to keep the help surface trustworthy as adapters
land.
What
- src/build-manifest.ts: add `findManifestMetadataIssues()` that flags
any positional with empty / whitespace-only / missing `help`. Wired
into `main()` after the import-failures gate; build aborts non-zero
with a per-arg report (`site/cmd positional "name" (sourceFile)`).
- src/build-manifest.test.ts: cover the gate (positives + negatives,
scoped strictly to positionals — named flags are intentionally
out-of-scope).
- 18 adapter offenders (16 required + 2 optional) get explicit help
text:
twitter: followers/following/list-add/list-remove/list-tweets/
search/thread
reddit: search/subreddit/user/user-comments/user-posts
douyin: stats/update
bilibili: subtitle
jike: search
Optional positionals (`twitter followers/following [user]`) now
document the omit semantics — fetches the currently logged-in
account.
- CHANGELOG: document the build gate and the offender list.
Out of scope (planned follow-ups)
- Semantic-quality advisory: optional positional help should also
contain `default / omit / current / logged-in / required unless …`
keywords. That belongs to the planned Arg metadata v2 work
(`when_omitted / when_present / value_format` 3-field schema).
- Named-flag `help` quality. Named flags carry the flag name itself
in help, so a missing `help` is not as opaque; if we want to gate
those too, do it as a separate, intentional decision.
Validation
- `npm run build` → 799 entries, clean.
- `npm run typecheck` → clean.
- `npx vitest run --project unit --project adapter` → 257 + 4 files,
all green (build-manifest 13 tests, manifest gate added).
- Smoke: temporarily reverted `followers.js` help to empty → build
aborts with the exact `twitter/followers positional "user" (...)`
line; restored, build is clean again.
- `npm run check:silent-column-drop` and `check:typed-error-lint`
baselines unchanged.
* chore(release): pre-release P0/P1 cleanup
P0 fixes:
- delete src/analysis.ts (179 lines, 0 importers across src/clis/extension)
- remove dead OPENCLI_DIAGNOSTIC negative test assertion
- rename OPENCLI_BROWSER_TIMEOUT to OPENCLI_BROWSER_IDLE_TIMEOUT — the env
controls workspace lease idle release, not command runtime; old name was
misleading and undocumented (no fallback needed)
- add 'fill' to validate.ts KNOWN_STEP_NAMES so adapters using PR #1222's
fill pipeline step do not trip "unknown step name" warnings during validate
P1 fixes:
- BrowserConnect daemon-not-running hint: replace stale "make sure port is
available" with actionable "run opencli doctor / opencli daemon restart"
- TimeoutError hint: lead with --timeout flag, demote env var to secondary
* fix(validate): derive step allowlist from pipeline registry
@pr-monitor flagged the prior "add 'fill' to KNOWN_STEP_NAMES" fix as
treating only the symptom — two parallel hand-maintained lists will keep
drifting whenever a new pipeline step is registered.
Address the root cause: pipeline/registry.ts now exports
`getRegisteredStepNames()` and validate.ts builds KNOWN_STEP_NAMES from
that. Adding a step via `registerStep()` automatically allowlists it.
* test(validate): regression guard for pipeline step allowlist linkage
@pr-monitor follow-up: lock the validate ↔ pipeline registry linkage at
the test layer so future drift is caught immediately.
Changes:
- recompute KNOWN_STEP_NAMES per-call (was const at module load) so
steps registered after validate.ts import (plugins, dynamic registration)
are honoured
- add src/validate.test.ts with 3 cases:
1. every step name from getRegisteredStepNames() exists
2. an adapter using every currently registered step does not warn
3. a step registered at runtime is automatically allowlisted by
validate without any source change to validate.ts
* fix(capabilityRouting): add fill to BROWSER_ONLY_STEPS
Same double-list drift pattern as validate.ts KNOWN_STEP_NAMES (audit
follow-up flagged in this PR's evolution thread). The fill step was
registered in pipeline/registry.ts (PR #1222) but never added to the
browser-only allowlist in capabilityRouting.ts.
Concrete impact:
- shouldUseBrowserSession() didn't recognize a `[{ fill: ... }]` pipeline
as needing a browser, so PUBLIC adapters using fill could end up
without a page and crash inside stepFill at `page!.fillText(...)`
- pipeline/executor.ts's per-step retry policy (BROWSER_ONLY_STEPS gets
2 retries on transient errors, others get 0) skipped fill — losing
retry coverage on a DOM-touching step
Fix:
- add 'fill' to BROWSER_ONLY_STEPS
- add a documenting comment explaining BROWSER_ONLY_STEPS is the
browser-touching subset of registered steps (not the full set)
- export _validateBrowserOnlyStepsAgainstRegistry() so the test layer
catches the inverse drift (browser-only step that no longer exists)
- 3 new tests in capabilityRouting.test.ts:
* pipeline with fill routes to browser session
* BROWSER_ONLY_STEPS subset of registered step names
* fill is in both lists
This addresses @pr-monitor follow-up #3 (audit similar double-list
patterns) for the obvious in-scope candidate. Other candidates outside
this PR's scope: build-manifest serialization vs registry shape, error
code unions vs lint baselines.
* test(validate): use Strategy.PUBLIC enum instead of string cast in regression test
Self-review nit: `strategy: 'public' as never` worked but bypassed the
typed CliOptions union. Use `Strategy.PUBLIC` so the test exercises the
real public API.
* refactor(runtime): unify command timeout into a single --timeout arg
Drop the cli-level `timeoutSeconds` build-time ceiling field. A command
now opts into runtime-enforced timeouts purely by declaring an arg named
`timeout`; the user-facing `--timeout` value (its default or override)
is the single authoritative knob, used both by the adapter polling loop
and by the runtime ceiling (with a 30s padding for return + closeWindow
+ trace export).
Behavior:
- Browser commands without a `--timeout` arg fall back to
OPENCLI_BROWSER_COMMAND_TIMEOUT (default 60s, unchanged).
- Non-browser commands without a `--timeout` arg now run unbounded
rather than against the previously implicit `timeoutSeconds` cap.
Affected commands keep their old caps via newly added `--timeout` args.
- LLM adapters (gemini/claude/deepseek/doubao/qwen/yuanbao ask) keep
their current `--timeout` defaults; the runtime ceiling is now strictly
more generous (userTimeout + 30s vs. the previous 180s cap), so
`--timeout 600` actually buys 600s of polling rather than dying at 180s.
Closes the design discussion that started from PR #1227, which proposed
a per-site `OPENCLI_GEMINI_ASK_TIMEOUT` env var to work around the same
underlying mismatch.
* fix(timeout): wire --timeout arg into chatgpt/gemini image adapter polling
codex-coder review on PR #1364 caught that the new --timeout arg I added
to chatgpt/image and gemini/image only drove the runtime ceiling — the
adapter still hardcoded `const timeout = 120`, so users passing
--timeout 240/600 saw runtime allow 270s/630s but the adapter stop
polling at 120s. That recreated the same single-knob mismatch this PR
was meant to delete.
Also add the browser-path runWithTimeout assertion codex-coder flagged
as missing: a browser command with --timeout default=5 must call
runWithTimeout with timeout: 35; a browser command without --timeout
arg must fall back to DEFAULT_BROWSER_COMMAND_TIMEOUT.
Image adapters now read kwargs.timeout and reject non-positive-integer
values with ArgumentError (no silent fallback). chatgpt/image.test.js
updated to pass an explicit timeout when calling .func directly (the
test bypasses arg coercion).
* fix(runtime): reject invalid timeout ceilings
* fix(timeout): normalize timeout args to integer values
* fix(timeout): preserve remaining command ceilings
* fix(runtime): validate timeout before browser setup
Closes#1334.
Exposes viewport overrides for `opencli browser screenshot` so an adapter or
ad-hoc shell user can render a page at a fixed width and capture the full
scrollable height. The ljg-card HTML to PNG pipeline use case.
Behavior:
- `--width W` only overrides device-metrics width; height is left unchanged.
- `--height H` only overrides height (ignored under `--full-page`).
- `--full-page` keeps the existing `captureBeyondViewport` shortcut.
- `--full-page --width W` first reflows at W, then re-overrides to (W, contentH)
so the captured image reflects the layout at the requested width.
- Override is always cleared in `finally`, including on capture failure.
Keep the owned automation container window warm across lease release. Non-final owned leases close their tab; the final owned lease resets its tab to about:blank as a reusable placeholder. Update browser close wording to describe lease release rather than window closure.
Per WAWQAQ feedback in #OpenCLI thread on the flat "Site adapters (112)" listing:
the bucket conflates real web sites (bilibili, dianping, ...) with desktop apps
(chatgpt-app, chatwise, codex, cursor, discord-app, doubao-app, antigravity, notion).
Group them so agents that fall back to --help can scan by category.
Three buckets, sourced from existing metadata only — no new adapter schema:
- External CLIs: passthrough binaries from loadExternalClis() (docker, gh, vercel, ...)
- App adapters: domain is `localhost` or any non-DNS string (no `.`)
- Site adapters: domain contains `.` (real DNS), or domain is unset (default)
The classifier is one line: `domain.includes('.') ? 'site' : 'app'`. Adapters
without a domain field default to site (most are public web scrapers like
arxiv / wikipedia / spotify / ...).
Verified against the live registry: 7 External CLIs, 8 App adapters
(antigravity, chatgpt-app, chatwise, codex, cursor, discord-app, doubao-app,
notion), 104 Site adapters.
Structured help (-f yaml/json) gains parallel `external_clis` / `app_adapters`
/ `site_adapters` keys; `commands` no longer leaks adapter names.
External CLIs are now hidden from the default Commands listing (mirrors how
site adapters were already filtered) and surfaced in their own section.
Adds opencli convention-audit for batch convention scanning, with structured output, strict mode, docs, and startup isolation from local user/plugin discovery.
Move the Browser Bridge daemon WebSocket out of the MV3 service worker and into an offscreen document. Remove the popup/action UI and obsolete extension log forwarding now that doctor is the diagnostic surface.