* feat(browser): add cross-origin iframe support via CDP execution contexts
Enable interaction with cross-origin iframes through CDP's execution
context mechanism, without requiring content scripts or all_frames.
- Track frame execution contexts via Runtime.executionContextCreated events
- Add 'frames' action to list all child frames (including cross-origin)
- Support frameIndex in 'exec' action to evaluate JS in specific frames
- Add Page.frames() and Page.evaluateInFrame() APIs for CLI consumers
- Tag cross-origin iframes with [F0]/[F1] indices in DOM snapshots
- Add Page.getFrameTree to CDP allowlist
Closes#1077
Change-Id: Id03361ddb616912dff3bfa8e59e8b68716de590b
* fix(browser): align cross-origin iframe routing contract
* fix(browser): unify iframe frame-index routing
---------
Co-authored-by: xuezhangying <xuezhangying@bytedance.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
From first principles, `opencli browser` commands exist for AI Agents to
operate websites through the browser skill. Reframe both READMEs to reflect
this: show users how to install the skill into their AI agent and describe
tasks in natural language, rather than listing raw CLI commands.
* feat(twitter): rewrite lists via GraphQL + add list-tweets
The DOM-scraping / detail-click approach in PR #1053 remained fragile
against X's frequent overview-page rendering changes and slow (N+1 page
loads per list). Rewrite `twitter lists` to call
`ListsManagementPageTimeline` GraphQL directly — one request returns all
owned + subscribed lists with id/name/member_count/subscriber_count/mode.
Also add `twitter list-tweets <listId>` for pulling the tweet stream from
a list, completing the read-side chain (lists → pick an id → list-tweets).
- lists: drop positional `user` arg (GraphQL returns only logged-in
user's lists), add `id` column, change followers to exact integer from
subscriber_count.
- list-tweets: same GraphQL pattern as bookmarks/likes (BEARER + ct0 +
dynamic queryId with static fallback + cursor pagination).
- Delete obsolete lists-parser.js and lists.d.ts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(twitter): add list-add / list-remove with Save-button commit
Two new commands to toggle list membership. X's list dialog uses a
"click-to-stage, Save-to-commit" pattern — clicking a row only updates
optimistic UI; the actual POST fires when the user clicks the top-right
"Save" button. Pressing ESC or the close-X silently cancels the change.
Implementation:
- Resolve listId → name via ListsManagementPageTimeline GraphQL, so we
match the dialog row by name (dialog rows have no data-testid listId).
- Open profile page → DOM click "…" menu → "Add/remove from Lists".
- Scroll dialog to locate target row (virtualized list).
- page.nativeClick on row — trusted CDP Input.dispatchMouseEvent fires
React's onclick, flips aria-checked (.click() alone does not suffice;
X ignores non-trusted events for list mutations).
- page.nativeClick on the Save button — commits to server.
- Verify by re-fetching ListsManagementPageTimeline and diffing
member_count: success only if N→N±1. No silent successes.
This fixes the pattern where batch `list-add` calls returned success for
every user but committed zero to the server (optimistic UI lied).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: stabilize twitter list manifest and query ids
* docs: add twitter list command discoverability
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix(zsxq): separate content field from title, remove title truncation
- Split getTopicText to return only title, add getTopicContent for body text
- Remove .slice(0, 120) that was truncating titles
- content field now contains full body text instead of duplicating title
* fix(zsxq): preserve title fallback for body-only topics
---------
Co-authored-by: huzekang <huzekang@opencode.ai>
Co-authored-by: jackwener <jakevingoo@gmail.com>
PR #1059 migrated xiaoyuzhou from SSR scraping to authenticated API.
The E2E tests run without credentials, producing exit code 78
(CONFIG_ERROR). The existing `isExpectedChineseSiteRestriction` guard
only caught FETCH_ERROR, PARSE_ERROR, and NOT_FOUND — not config
errors from missing auth credentials.
Three improvements from the design debt audit:
1. Remove deprecated `tabId` field and `getActiveTabId()` method
- Delete `tabId` from DaemonCommand (daemon-client.ts) and Command (protocol.ts)
- Delete `getActiveTabId()` from IPage interface (types.ts) and Page class (page.ts)
- Update extension resolveCommandTabId() to remove legacy fallback
- Update handleTabs select case to remove tabId check
- The tab→page migration is now complete
2. Unify argument validation into single code path
- Remove `normalizeArgValue()` from commanderAdapter.ts
- Commander adapter now passes raw values to prepareCommandArgs()
- All coercion (bool, int, number) and validation (required, choices)
happens once in coerceAndValidateArgs() in execution.ts
- Eliminates duplicated boolean normalization
3. Remove dead plugin filesystem wrappers
- Delete `promoteDir()` — never called in production code
- Delete `replaceDir()` — thin wrapper over beginReplaceDir, never called
- Remove corresponding test-only exports and tests
- Rename PromoteDirFsOps → ReplaceDirFsOps to match remaining usage
- Transaction infrastructure (runTransaction, beginReplaceDir,
beginReplaceSymlink) retained — used by publishStandalonePlugin
and publishMonorepoPlugins for atomic multi-step operations
* fix(extension): per-workspace idle timeout for browser sessions (#1058)
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: resolve sessionExpired double-delete race and timeout override lifecycle
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
* refactor: remove sessionExpired warning per product decision
@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.
* fix: clean up workspaceTimeoutOverrides on user-initiated window close
The windows.onRemoved listener was missing workspaceTimeoutOverrides
cleanup, causing stale overrides to persist across sessions when users
manually close the automation window.
* 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.