The windows.onRemoved listener was missing workspaceTimeoutOverrides
cleanup, causing stale overrides to persist across sessions when users
manually close the automation window.
@WAWQAQ decided session-expired warning is not needed.
Remove expiredWorkspaces tracking, sessionExpired flag from protocol,
and related CLI-side warning code. Keep per-workspace timeout and
override lifecycle cleanup.
Addresses @codex-coder review blockers:
1. sessionExpired flag was never set because getAutomationWindow()
consumed expiredWorkspaces before handleCommand() could check it.
Fix: use .has() in getAutomationWindow, only .delete() in handleCommand.
2. workspaceTimeoutOverrides was never cleaned up — once set, it
persisted until extension restart. Fix: clear override on idle
timeout expiry, explicit close-window, and borrowed-session detach.
Adds 5 tests covering:
- browser:* uses 10min timeout (not 30s)
- sessionExpired flag is set and consumed correctly
- workspaceTimeoutOverrides cleared on idle expiry
- workspaceTimeoutOverrides cleared on explicit close
- idleTimeout from command applies to workspace override
The global 30s WINDOW_IDLE_TIMEOUT was too aggressive for interactive
`opencli browser` commands where users type manually between invocations.
- browser:*/operate:* workspaces now default to 10 min idle timeout
- Adapter workspaces keep the existing 30s timeout
- Support custom timeout via OPENCLI_BROWSER_TIMEOUT env var (seconds)
or command-level idleTimeout parameter
- Surface sessionExpired warning when a new window is created after
the previous session timed out
- Fix stale comment (said 120s, actual was 30s)
Closes#1058
* fix(xiaoyuzhou): migrate from broken SSR scraping to authenticated API (fixes#1023)
Xiaoyuzhou removed SSR rendering — /podcast/<id> and /episode/<id> pages
now return 404, breaking fetchPageProps() which scraped __NEXT_DATA__.
Migrate podcast, podcast-episodes, episode, and download commands to use
the existing authenticated API client (requestXiaoyuzhouJson) that
transcript.js already uses successfully.
Changes:
- podcast.js: use /v1/podcast/get API endpoint
- podcast-episodes.js: use /v1/podcast/listEpisode API endpoint
- episode.js: use /v1/episode/get API endpoint
- download.js: use /v1/episode/get API endpoint
- utils.js: remove unused fetchPageProps, keep format helpers
- Update all affected tests (download.test.js, utils.test.js)
- Change strategy from PUBLIC to LOCAL (requires credentials)
* fix(xiaoyuzhou): align local strategy contract
* fix(xiaoyuzhou): align local api metadata
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
#1025 merged hot-rank adapters for eastmoney/tdx/ths but the
accompanying docs were missing. This breaks the Doc Check CI
workflow on every PR ('--strict' mode, exits non-zero when
`scripts/check-doc-coverage.sh` finds adapters without docs),
blocking merges across the board.
Adds a doc page per adapter, registers them in the adapters
index table, and adds sidebar entries in the VitePress config.
* feat: add hot stock ranking adapters for eastmoney, tdx, ths
Add three new site adapters for Chinese stock hot rankings:
- eastmoney/hot-rank: 东方财富热股榜
- tdx/hot-rank: 通达信热搜榜
- ths/hot-rank: 同花顺热股榜
All use Strategy.COOKIE browser mode with page.evaluate() DOM scraping.
Each includes co-located tests (13 tests total, all passing).
* fix(tdx,ths): add symbol validation and deduplication in evaluate()
Add seen Set for deduplication and skip entries with empty symbol/name,
matching the pattern already used in eastmoney/hot-rank.js.
* fix: refine hot-rank selectors based on browser inspection
- eastmoney: use table.rank_table tbody tr with td index-based extraction,
fix name from a[title] to avoid post content contamination
- tdx: use div.top-cell[data-code] data attributes for reliable extraction,
add tags column from div.tips-item.gnbk
- ths: use card-based layout selectors, remove price column (not in UI),
extract tags from div.tag.PFSC-R
* fix(hot-rank): align tdx and ths columns with actual output
* fix: register hot stock ranking adapters
---------
Co-authored-by: dengjingren <dengjingren@cn.wilmar-intl.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix: preserve network capture and surface extension mismatch diagnostics
Older Browser Bridge installs can still connect to the daemon while
missing two capabilities we now rely on: the network-capture actions
and the extension version handshake. That created three user-facing
failure modes with real impact:
1. `opencli explore ...` crashed with `Unknown action: network-capture-start`
against an old extension, so exploration stopped before any site
analysis finished.
2. `opencli doctor` and `opencli daemon status` could show a healthy
connection even when the extension never reported a version, which
hid the compatibility problem and sent users toward the wrong fix.
3. After reloading a new extension, `explore` could still report
`Endpoints: 0 total, 0 API` because `handleNavigate()` detached the
debugger before top-level navigation and cleared the active network
capture state right before the page load we needed to observe.
Fix this in two layers:
- Teach `Page` to treat unsupported `network-capture-*` actions as an
old-extension compatibility case. It now warns once, memoizes the
unsupported state, and returns empty capture data instead of throwing.
- Teach `doctor` and `daemon status` to treat "connected but version
unknown" as a warning instead of a healthy state, so version-handshake
failures are visible immediately.
- Preserve the debugger attachment while network capture is armed, so
the initial navigation keeps the capture state alive and the extension
can record requests from the first page load.
Before:
- `opencli explore ...` -> `Error: Unknown action: network-capture-start`
- `opencli doctor` -> `[OK] Extension: connected` / `Everything looks good!`
- `opencli daemon status` -> `Extension: connected` even when the
extension version was missing
- `opencli explore ...` after reloading the extension -> `Endpoints: 0 total, 0 API`
After:
- `opencli explore ...` on an old extension -> warns once and continues
- `opencli doctor` -> `[WARN] Extension: connected (version unknown)`
- `opencli daemon status` -> `Extension: connected (version unknown)`
- `opencli explore ...` on the reloaded extension keeps network capture
armed across navigation instead of clearing it before the page load
* fix: reset network capture flags on closeWindow()
Prevents stale _networkCaptureUnsupported flag from persisting across
sessions when the user reinstalls or reloads the extension mid-session.
* fix: startNetworkCapture returns boolean to prevent false-positive on old extensions
When the extension doesn't support network-capture-*, startNetworkCapture()
now returns false instead of silently resolving. This ensures browser open/
network correctly falls back to the JS interceptor on old extensions.
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix: auto-restart stale daemon and improve connection error messages
When daemon is running but extension never connected (stale daemon started
before extension was installed), the CLI now auto-restarts the daemon to
give the extension a fresh WebSocket endpoint, instead of just waiting
and then telling the user to install the extension.
Also improves error messages across cli.ts, bridge.ts, and doctor.ts to
suggest "opencli daemon stop && opencli doctor" as the quick fix, since
that's what actually resolves the issue.
* fix: version-aware stale daemon detection and improved error messages
- Daemon /status now includes `daemonVersion` field
- bridge.ts: when daemon is running but extension not connected, checks
daemonVersion vs CLI version. Only auto-restarts if version mismatch
(stale daemon from older CLI). Same-version daemon shows improved error
message with "opencli daemon stop && opencli doctor" hint.
- doctor.ts: explicitly identifies stale daemon (version mismatch) in
diagnostics report, shows daemon version in status line
- cli.ts: error message changed to suggest "opencli daemon stop && opencli doctor"
* fix: treat missing daemonVersion as stale, verify shutdown before respawn
- Missing daemonVersion (pre-version daemon) is now treated as stale,
covering the most common user scenario (old daemon without version field)
- After requestDaemonShutdown(), poll until daemon actually stops (port
released) before spawning new one, with 3s timeout
- If shutdown request fails, log warning instead of silently proceeding
- doctor.ts also treats missing daemonVersion as stale with clear message
* fix: fail explicitly when stale daemon replacement fails
- If shutdown request fails or port isn't released within 3s, throw
'Stale daemon could not be replaced' instead of blindly spawning on
an occupied port
- Add tests for all three stale-daemon branches: same-version (no
restart), missing daemonVersion (stale), mismatched version (stale)
* fix: use type-based error dispatch in browserAction instead of string matching
browserAction() now checks `instanceof BrowserConnectError` first and
renders both message and hint, instead of string-matching on message
content. This ensures stale daemon errors ("Stale daemon could not be
replaced") surface the actionable hint to the user.
* feat(grok): add image command for grok.com image generation
Add `opencli grok image <prompt>` which submits a prompt via the existing
grok.com browser session and returns the generated image URLs from the
latest assistant bubble.
Because assets.grok.com URLs are gated by Cloudflare and cannot be
downloaded with a plain HTTP client, the --out flag triggers an in-page
fetch(credentials: 'include') so the browser session's cookies and
referer are attached, then writes the decoded blob to disk.
Flags:
- --new start a fresh chat before sending
- --timeout max seconds to wait for the image (default 240)
- --count minimum number of images to wait for before returning
- --out directory to save downloaded images
Ships with unit tests for the helpers (isOnGrok, normalizeBooleanFlag,
dedupeBySrc, imagesSignature, extFromContentType, buildFilename).
* fix(grok): harden image composer and bubble detection
* fix(grok): harden image flow and docs
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
When ChatGPT macOS app is set to Chinese language, the "Options"
button label becomes "选项". This change checks for both English
and Chinese labels to find the button.
Co-authored-by: mad <mademing@maddeMac-mini.local>
Co-authored-by: Claude <noreply@anthropic.com>
* feat: implement Ref-Backed Locator for browser actions
Introduces a unified target resolution system with fingerprint
verification and structured error diagnostics.
Snapshot phase:
- Each interactive element now gets a fingerprint (tag, role, text,
ariaLabel, id, testId) stored in window.__opencli_ref_identity
- Zero overhead: metadata is already available during DOM walk
Resolution phase (new target-resolver.ts):
- Numeric input → ref path with fingerprint verification
- CSS-like input → querySelectorAll with uniqueness check
- No more silent first-match: ambiguous selectors are rejected
Error model (new target-errors.ts):
- stale_ref: element identity changed since snapshot
- ambiguous: CSS selector matched multiple elements (with candidates)
- not_found: element not in DOM or invalid input
- All errors include actionable hints for AI agents
base-page.ts:
- click() and typeText() now use two-phase resolve-then-act
- Existing CDP fallback for click preserved
* feat: migrate scrollTo to unified resolver pipeline
scrollTo now uses the same two-phase resolve-then-act pattern as
click and typeText, getting fingerprint verification and structured
error diagnostics (stale_ref/ambiguous/not_found) for free.
* fix: address review — stronger fingerprint verification & surface TargetError in CLI
1. Fingerprint verification now uses the full identity vector (tag, id,
testId, ariaLabel, role, text) instead of just tag/role/text. Strong
identifiers (id, testId) are decisive; remaining signals use majority
voting. Fixes false negatives where same-tag elements swapped.
2. browserAction() now renders TargetError with code, hint, and
candidates list instead of just the message string.
* fix: migrate get/select/type-autocomplete to unified resolver
- browser get text/value/attributes now resolve via resolveTargetJs
instead of raw querySelector, getting fingerprint verification and
structured errors for free
- browser select uses selectResolvedJs on __resolved element
- type command's autocomplete detection uses isAutocompleteResolvedJs
on the already-resolved element
- Fix empty-string text prefix match: fp.text="Login" + text="" no
longer falsely passes fingerprint check
- docs/index.md: update feature cards to match README Highlights
- docs/zh/index.md: sync Chinese feature cards
- docs/guide/getting-started.md: align Highlights section
- README.zh-CN.md: rename "为什么是 OpenCLI" to "亮点", align with EN
* fix: remove duplicate extension zip from releases
The release and build-extension workflows were creating both
opencli-extension.zip and opencli-extension-v{version}.zip (identical
content), causing both to be uploaded. Keep only the versioned filename.
* docs: update extension zip filename to versioned format
Update all references from opencli-extension.zip to
opencli-extension-v{version}.zip to match the workflow change.
* feat(mubu): add mubu (mubu.com) adapter with 5 commands
Commands: doc, docs, notes, recent, search.
- Uses COOKIE strategy; API calls via in-page XHR with Jwt-Token
from localStorage (matches the web app's own mechanism).
- Renders node trees to Markdown (default) or plain text;
supports tables, tasks, images, emoji, mentions, strikethrough,
underline, and nested structures.
- notes supports flexible time ranges: single day, month, year,
or custom --from/--to spans, plus a --list overview mode.
- search returns full-text matches with hit count and snippets
for both folders and documents.
* fix(manifest): register mubu commands in runtime manifest
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* refactor: unify OPENCLI_VERBOSE and DEBUG=opencli into one mechanism
Three debug output levels (verbose/debug/diagnostic) was redundant.
Merge DEBUG=opencli into OPENCLI_VERBOSE so `-v` flag controls all
verbose/debug output through a single mechanism.
- log.verbose() now checks both OPENCLI_VERBOSE and DEBUG=opencli
- log.debug() becomes an alias for log.verbose() (backward compat)
- boss/utils.js verbose helper simplified to check OPENCLI_VERBOSE only
- DEBUG=opencli still works as fallback (no breaking change)
* fix(boss): preserve debug fallback for verbose logs
The error path in executeCommand did not call page.closeWindow(),
leaving the automation window open until the extension's idle timer
fires. On Windows, MV3 service worker suspension makes this timer
unreliable, causing windows to linger indefinitely.
Now closeWindow is called after diagnostic collection but before
rethrowing, ensuring the window is closed on both success and failure.
Add missing env vars to both README and README.zh-CN:
- OPENCLI_SKIP_FETCH: skip adapter sync on global install
- OUTPUT: override output format (json/yaml/table)
- DEBUG=opencli: internal debug logging
- DEBUG_SNAPSHOT: DOM snapshot debug output
* fix: clean up stale .yaml adapter files from older versions (#953)
Users upgrading from v1.6.x retain .yaml adapter files in
~/.opencli/clis/ that trigger "Ignoring YAML adapter" warnings on
every run. The hash-based sync only tracks .js files, so these
legacy .yaml files are never cleaned up.
Add a cleanup step (3b) that removes .yaml/.yml files from user
adapter directories when the corresponding site exists in the
official package (i.e., the site has been migrated to .js).
* fix(fetch-adapters): narrow stale yaml cleanup
* feat: decouple extension version from CLI version
Extension and CLI had tightly coupled version numbers (both 1.7.2),
requiring manual sync across 3 files on every release. This decouples
them so each can release independently.
Changes:
- Extension version reset to 1.0.0 with independent versioning
- Extension sends compatRange (e.g. ">=1.7.0") in hello message
so doctor can check CLI/extension compatibility
- Daemon stores and exposes extensionCompatRange via /status
- Doctor uses compatRange for compatibility checks (falls back to
major-version check for older extensions without compatRange)
- Doctor shows extension update availability from cached GitHub
Releases data
- release.yml always builds and attaches extension zip to every
CLI release, so users always find both in the same release page
- build-extension.yml triggers on ext-v* tags (not v*) to avoid
duplicate builds
* fix: version extension release assets
* fix: code audit round 2 — pruneEmptyDirs, evaluateWithArgs, hot-reload, error cause chain
1. pruneEmptyDirs: use path.relative() instead of startsWith() to prevent
false boundary matches on overlapping directory names
2. evaluateWithArgs: add safe evaluate method that auto-serializes args via
JSON.stringify, preventing injection by design
3. Hot-reload: detect mtime changes on user adapter files in daemon mode,
invalidate module cache so edits take effect without restart
4. toEnvelope: preserve error cause chain in verbose mode for better
production debugging
* fix: address review feedback on code audit round 2
- pruneEmptyDirs: resolve() paths before relative() check
- evaluateWithArgs: validate keys are valid JS identifiers
- hot-reload: only bust ESM cache on reload, not first load
- toEnvelope: move cause serialization into toEnvelope itself
so all consumers (AI agents, MCP tools) get cause chain
* fix: address code audit findings (C1-C4, I1, I4, I6)
Security:
- C1: Fix page.evaluate injection in browser type/select commands and
6 adapter files by using JSON.stringify for user input interpolation
- C2: Close WebSocket on CDP connect timeout to prevent resource leak
- C3: Reject CDP connect promise on Page.enable failure instead of
silently swallowing the error
Reliability:
- C4: Guard against corrupted adapter-manifest.json hashes to prevent
false-positive override deletion
- I1: Throw on pre-navigation failure instead of warn-and-continue
- I4: Use Map<string, Promise<void>> for lazy module loading to prevent
concurrent double-imports of the same adapter
Performance:
- I6: Replace O(n) registry alias cleanup with O(k) direct deletion
* fix: address self-review findings on PR #981
- C1: add quotes around CSS selector attribute values in browser
type/select to match other commands (get text/value/attributes)
- C2: clear this._ws in timeout handler to prevent race with open event
- C4: refine corruption guard — treat null/undefined hashes as empty,
only skip sync for truly invalid types (string, number, array)