Commit Graph

46 Commits

Author SHA1 Message Date
jakevin 2ddf571445 feat: auto-close adapter windows, add OPENCLI_WINDOW_FOCUSED, document config (#915)
* feat: auto-close adapter windows, add OPENCLI_WINDOW_FOCUSED, document config

1. Adapter commands now close the automation window immediately after
   completion instead of waiting for the 30s idle timeout.

2. OPENCLI_WINDOW_FOCUSED=1 opens automation windows in the foreground
   (useful for debugging). Default remains background.

3. Add Configuration section to README (EN/ZH) and opencli-usage skill
   listing all stable user-facing environment variables.

* Fix OPENCLI_WINDOW_FOCUSED to be per-request, not frozen at daemon startup

Move env var read from daemon (startup-time constant) to CLI side
(sendCommandRaw), so it works correctly with the persistent daemon model.
Each request now reads the env var fresh and includes windowFocused in
the command payload.
2026-04-09 21:27:02 +08:00
jakevin 555626f409 feat: replace tabId with targetId as cross-layer page identity (#899)
Use Chrome CDP targetId (UUID) as the canonical page identity across
all layers (extension → daemon → CLI), demoting tabId to an
extension-internal routing detail.

- Add extension/src/identity.ts: bidirectional targetId ↔ tabId mapping
  with lazy refresh via chrome.debugger.getTargets()
- Update protocol: Command.page and Result.page carry targetId
- Update background.ts: resolveCommandTabId() and pageScopedResult()
  helpers; all page-scoped handlers return targetId
- Add sendCommandFull() to daemon-client for responses with page identity
- Update Page class: _page stores targetId, goto/selectTab extract it
- Update record.ts: injectedPages tracks by targetId
- Add extension tests to vitest config and CI test scripts
2026-04-09 12:26:32 +08:00
Elias@Didoo 97d152ef39 fix: retry on No window with id CDP error (#892)
* fix: retry on No window with id CDP error

* test(browser): lock transient window-id retry behavior

---------

Co-authored-by: Yun Xiao <yunxiao@agents.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
2026-04-09 01:23:22 +08:00
jakevin 1662e9a73c refactor: rename operate to browser (#883)
* refactor: rename operate to browser

* fix: preserve browser rename compatibility

* fix: bump generate outcome schema version

* fix: keep generate outcome schema at v1
2026-04-08 21:03:57 +08:00
Ray e82649379b feat(instagram): add post, reel, story, and note publishing (#671)
* Add draft Instagram posting flow

* Refine Instagram post flow

* Add dynamic Instagram posting routes

* Retry transient Instagram private setup failures

* Add Instagram reel posting command

* Add Instagram mixed-media carousel posting

* Unify Instagram post media input

* Add Instagram story posting command

* Add Instagram note publishing command

* fix(instagram): use JSON.stringify for constants in note evaluate string

Replace template literal interpolation of Node-side constants with
JSON.stringify() for consistency with codebase evaluate patterns.
Use bracket notation for dynamic property access instead of template
interpolation into a property chain.

---------

Co-authored-by: jackwener <jakevingoo@gmail.com>
2026-04-04 16:22:22 +08:00
jakevin 2d005d14a8 fix: recover drifted tabs instead of abandoning them (#652) (#715)
When other Chrome extensions (tab managers, new-tab overrides) move
automation tabs to a different window, the Browser Bridge now attempts
to move the tab back to the automation window rather than creating a
new one. This preserves the existing page state and avoids redundant
navigation.

Changes:
- resolveTab(): when a provided tabId has drifted to another window but
  content is still debuggable, use chrome.tabs.move() to bring it back
- handleNavigate(): after navigation completes, detect if the tab drifted
  during navigation and move it back to the session window
- cdp.ts ensureAttached(): log final tab URL and windowId on attach
  failure for better diagnosis of extension conflicts

Closes #652 (partially — addresses tab drift recovery and diagnostics)
2026-04-03 03:48:13 +08:00
jakevin 5fe081b28c perf: optimize browser pipeline — tab query dedup, parallel stealth, incremental snapshots (#713)
* perf: optimize browser pipeline — tab query dedup, parallel stealth, incremental snapshots

- resolveTab() now returns { tabId, tab } so handleNavigate skips redundant chrome.tabs.get()
- goto() fires stealth injection in parallel with navigation instead of sequentially
- snapshot() passes previousHashes to enable incremental diff marking on consecutive calls

* revert: remove stealth parallelization — simplicity over performance
2026-04-03 03:32:24 +08:00
jakevin 0c75ab3f7e perf: reduce round-trips in browser command hot path (#712)
1. eval retry delay: 1000ms → 200ms for SPA navigation errors, 500ms
   for debugger detach. SPA navigations recover within ~100ms, the old
   1000ms delay was unnecessarily long.

2. Window creation: replace fixed 200ms sleep with tab-load poll.
   Listens for chrome.tabs.onUpdated status=complete with 500ms
   fallback cap. about:blank loads in ~20ms, saving ~180ms.

3. bridge.ts _ensureDaemon: single fetchDaemonStatus() call instead of
   two sequential calls (isExtensionConnected + isDaemonRunning both
   called fetchDaemonStatus independently). Saves one HTTP round-trip.

4. goto() post-navigation: coalesce stealth injection + DOM settle into
   a single exec call. Previously two sequential round-trips
   (Node→daemon→WS→extension→CDP each). Saves ~60-160ms per goto().
2026-04-03 03:24:54 +08:00
jakevin cd5da59187 perf: skip blank page on first browser command (#710)
Two changes that eliminate the about:blank → target-domain navigation
on first command execution:

1. Extension: getAutomationWindow() accepts an optional initialUrl.
   When creating a new window, uses the target URL directly instead
   of about:blank. handleNavigate() passes cmd.url through so the
   window starts on the correct domain.

2. CLI: Remove isAlreadyOnDomain() check before pre-nav. Instead,
   always call page.goto(preNavUrl) — the extension's handleNavigate
   already has a fast-path that skips navigation when the tab is
   already at the target URL. This avoids an extra exec round-trip
   (getCurrentUrl eval) on first command.

Net effect: first command saves ~1-3s (one fewer page load),
subsequent commands behave the same (navigate fast-path handles
domain matching efficiently via chrome.tabs.get).
2026-04-03 02:59:15 +08:00
jakevin bb137ce901 feat: add opencli operate — browser control commands for Claude Code skill (#614)
Add `opencli operate` subcommand group with 15+ commands for
step-by-step browser control, designed as a Claude Code skill.
No LLM API key needed — Claude Code IS the LLM.

Commands:
  Navigation: open, back, scroll
  Inspect: state, screenshot, get (title/url/text/value/html/attributes)
  Interact: click, type, select, keys
  Wait: wait selector/text/time
  Extract: eval (execute JS in page context)
  API Discovery: network (auto-captured since last open, --detail N)
  Sedimentation: init (generate adapter scaffold), verify (test adapter)
  Session: close

Infrastructure:
  - CDP passthrough with 22-method allowlist
  - Two-layer retry for extension interference (aggressive for operate:*)
  - Network interceptor auto-injected on operate open
  - node_modules symlink for user TS adapter imports

Skill: skills/opencli-operate/SKILL.md
  - Complete command reference
  - Sedimentation workflow guide (explore → network → init → verify)
  - Adapter strategy guide (PUBLIC/COOKIE/UI)
  - Dual quickstart (AI Agent 1 step / Human 3 steps)
2026-04-02 19:30:35 +08:00
jakevin 773178345d refactor: remove bind-current, restore owned-only browser automation model (#664)
* fix(notebooklm): remove bind-current workflow

* fix: relax notebook ID check in open.ts and clean up idle timeout test

- open.ts: only throw when page kind is not 'notebook'; log a warning
  instead of throwing when the notebook ID doesn't match exactly
- background.test.ts: remove unused tabs[1] setup in idle timeout test
  that was leftover from borrowed-session era

* build: rebuild extension dist after bind-current removal
2026-04-01 23:23:48 +08:00
小左同学 4818871309 Handle foreign extension embeds before debugger attach (#662)
* Handle foreign extension embeds before debugger attach

* fix(extension): recover owned tabs without mutating borrowed tabs

* fix(extension): avoid adopting unrelated tabs

* Revert "fix(extension): avoid adopting unrelated tabs"

This reverts commit 2cba0c19da.

* Revert "fix(extension): recover owned tabs without mutating borrowed tabs"

This reverts commit 69dfadedae.

* fix(extension): avoid mutating tabs before attach

---------

Co-authored-by: jackwener <jakevingoo@gmail.com>
2026-04-01 22:10:30 +08:00
jakevin 7ea3f6d3fb feat(stealth): harden CDP debugger detection countermeasures (#644)
Add 6 new anti-detection patches to stealth.ts and CDP-level debugger
statement neutralization to reduce risk of bot detection on platforms
like Xiaohongshu.

New patches:
- Shared toString disguise via WeakMap (undetectable by anti-bot scripts)
- Anti-debugger statement trap (Function/eval patching + CDP Debugger.setBreakpointsActive)
- Console method fingerprinting defense (re-wrap CDP-bound console methods)
- Window dimension detection defense (outerWidth/outerHeight normalization)
- Performance API entry filtering (remove debugger/devtools entries)
- document.$cdc_ property cleanup (backup for window-level cleanup)
- Iframe contentWindow.chrome consistency
2026-03-31 23:34:37 +08:00
jakevin 57534cf8b3 feat(daemon): replace 5min idle timeout with long-lived daemon model (#641)
* docs: add daemon lifecycle redesign spec

Replace the aggressive 5-minute idle timeout with a long-lived daemon
model that stays running for hours, reducing restart overhead during
development cycles.

* docs: add daemon lifecycle redesign implementation plan

8-task TDD plan for replacing aggressive 5-minute idle timeout with
long-lived daemon model (4h default, dual-condition exit).

* feat(daemon): add DEFAULT_DAEMON_IDLE_TIMEOUT constant (4 hours)

* feat(daemon): replace fixed 5min timeout with dual-condition idle manager (4h default)

* feat(extension): reduce WS reconnect backoff cap from 60s to 5s

* feat(daemon): improve CLI connection-waiting UX with progress messages and 200ms polling

* feat(daemon): add opencli daemon status/stop/restart commands

* test(daemon): add tests for daemon status/stop commands

* fix(daemon): address code review issues — stale constant, restart robustness, timer cleanup, test coverage

* docs: update daemon documentation for new lifecycle and CLI commands

- troubleshooting.md: replace manual curl/pkill with `opencli daemon status/stop/restart`
- browser-bridge.md (en/zh): add Daemon Lifecycle section
- README.md: add `opencli daemon status` to Quick Start
- README.zh-CN.md: add daemon management commands to tips
2026-03-31 22:17:54 +08:00
muqiao215 0204dbb018 feat(notebooklm): add read commands and compatibility layer (#622)
* feat(notebooklm): add read commands and compatibility layer

* review: trim notebooklm artifacts and sync docs

---------

Co-authored-by: qiaoqiao147 <camtup044@gmail.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
2026-03-31 13:19:47 +08:00
James 5925849414 feat(xiaohongshu): use CDP DOM.setFileInputFiles for image upload (#574)
* feat(xiaohongshu): use CDP DOM.setFileInputFiles for image upload

Replace base64 DataTransfer injection with CDP DOM.setFileInputFiles,
which lets Chrome read image files directly from the local filesystem.
This eliminates payload size limits that caused "fetch failed" errors
when uploading large images (>500KB) through the browser bridge.

Changes:
- Add 'set-file-input' action to protocol, extension handler, and CDP executor
- Add Page.setFileInput() method for CLI-side usage
- Rewrite publish image upload to use CDP path, with base64 fallback
  for older extension versions that don't support the new action
- Add clear warning when falling back to base64 with large payloads

Closes #542 (partially — image upload reliability)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: cover cdp file input upload path

* fix: keep image upload on image-only inputs

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
2026-03-29 17:16:55 +08:00
Cjy-CN f9857f8c7b fix: remove invalid state: 'normal' from chrome.windows.create() (#559)
* fix: remove invalid `state: 'normal'` from chrome.windows.create()

Chrome 146+ rejects 'normal' as an invalid value for the `state` parameter
in chrome.windows.create(). This causes the error:

    Error: Invalid value for state

Root cause analysis:
- The Chrome Extensions API documentation states that `state` parameter
  only accepts 'minimized', 'maximized', and 'fullscreen' as input values
- While WindowState enum includes 'normal', it's meant for reading window
  state, not for setting it during creation
- Chrome 146 enforces stricter validation on the `state` parameter
- When `state` is omitted, the window defaults to 'normal' state anyway

Fix: Remove the `state: 'normal'` parameter entirely. The window will
default to normal state without explicitly setting it.

Tested: `opencli doctor` and `opencli bilibili hot` now work correctly
on Chrome 146.0.7680.165.

* build: rebuild dist after removing state: 'normal'

---------

Co-authored-by: jackwener <jakevingoo@gmail.com>
2026-03-28 21:25:31 +08:00
AstroHan 75ddb6319b fix(extension): probe daemon before WebSocket to eliminate console noise (#534)
* fix(extension): probe daemon via HTTP before WebSocket to eliminate console noise

When the daemon is offline, `new WebSocket()` logs uncatchable
ERR_CONNECTION_REFUSED errors to Chrome's extension error page.
Add `probeAndConnect()` that checks daemon reachability with a
silent `fetch(HEAD)` before attempting WebSocket connection.

All three auto-connect paths (initialize, keepalive alarm, eager
reconnect) now go through the probe, eliminating the error noise
entirely.

Closes #505

* refactor(extension): inline probe into connect(), add /ping to daemon

Instead of a separate probeAndConnect() wrapper that all call sites had
to remember to use, bake the HTTP probe directly into connect() itself.
This makes the guard impossible to accidentally skip when adding new
connection paths in the future.

Also adds a dedicated GET /ping endpoint to the daemon (no X-OpenCLI
header required) so the probe has a clear semantic contract instead of
relying on a 403 side-effect from the root path.

- daemon: GET /ping → 200 {ok:true}, no auth needed, placed before the
  X-OpenCLI header check; only chrome-extension:// and no-origin
  requests reach it (origin check is still enforced above)
- background: connect() is now async; probes /ping with a 1 s timeout
  before new WebSocket(); all call sites (initialize, keepalive alarm,
  scheduleReconnect) remain unchanged
- probeAndConnect() removed — no longer needed

* fix(extension/daemon): address review feedback on probe refactor

- protocol.ts: replace DAEMON_HTTP_URL with DAEMON_PING_URL (clearer
  semantics, single source of truth for the health-check URL)
- background.ts: import DAEMON_PING_URL from protocol instead of
  defining a local constant; check res.ok so an unexpected non-200
  response doesn't fall through to WebSocket; annotate all fire-and-
  forget connect() call sites with `void` to make intent explicit
- daemon.ts: add security comment on /ping documenting the timing
  side-channel tradeoff (loopback-only, accepted risk)

---------

Co-authored-by: jackwener <jakevingoo@gmail.com>
2026-03-28 20:15:51 +08:00
AstroHan 5ab3f5d7d5 fix(extension): change automation window state from minimized to normal (#531)
chrome.windows.create rejects state:'minimized' when combined with
width/height (Chrome API constraint). Revert to state:'normal' to fix
the "Invalid value for state" error. The 30s idle timeout from #521
is preserved.

Fixes #526
2026-03-28 10:57:40 +08:00
jakevin a1561e5361 fix(extension): minimize automation window + reduce idle timeout to 30s (#521)
- Create automation window with `state: 'minimized'` so it never
  appears in the user's taskbar or steals visual attention
- Reduce idle timeout from 120s to 30s — window closes quickly after
  the last command finishes, instead of lingering for 2 minutes
- CDP debugger works fine on minimized windows, no functional impact

Fixes the user-visible issue of a blank data:text/html tab appearing
during command execution.
2026-03-28 00:03:44 +08:00
jakevin 7617dff262 feat: zero onboarding, extension version check, and update notifier (#479)
* feat: zero onboarding, extension version check, and update notifier

- Fail-fast guard in execution.ts: when daemon is running but extension
  is not connected, immediately surface a setup guide instead of waiting
  for the 30s connect timeout

- Extension version handshake: extension sends `hello` with its version
  on WebSocket connect; daemon stores it and exposes via /status; CLI
  warns on mismatch in both execution path and `opencli doctor`

- `opencli doctor` now shows extension version inline and reports
  version mismatch as an actionable issue

- Non-blocking npm update checker: registers a process exit hook so the
  update notice appears after command output (same pattern as npm/gh/yarn);
  background fetch writes to ~/.opencli/update-check.json for next run

- postinstall: print Browser Bridge setup instructions after shell
  completion install for first-time global install users

Bug fixes caught in review:
- discover.ts: add AbortController timeout to checkDaemonStatus() fetch,
  move clearTimeout after res.json() to cover body streaming
- daemon.ts: clear extensionVersion and reject pending requests in
  ws.on('error') handler, not just ws.on('close')
- update-check.ts: skip update notice when process exits with non-zero
  code; read cache once at module load to avoid double disk I/O;
  guard isNewer() against NaN from pre-release version strings

* fix: reduce fail-fast timeout to 300ms and guard stderr.write in exit hook
2026-03-27 02:14:37 +08:00
jakevin bf5f327775 fix(extension): improve UX when daemon is not running (#424)
- Show helpful hint in popup when disconnected: "This is normal. The
  extension connects automatically when you run any opencli command."
- Stop eager reconnect after 6 attempts (reaching 60s backoff) to
  reduce ERR_CONNECTION_REFUSED noise in console; keepalive alarm
  still retries every ~24s at low frequency.
2026-03-25 16:19:02 +08:00
jakevin 4812486482 feat(extension): add popup UI, privacy policy, and CSP for Chrome Web Store (#415)
- Add popup.html/popup.js showing daemon connection status
  (Connected / Reconnecting / No daemon connected)
- Add message listener in background.ts to expose WebSocket state
- Add PRIVACY.md with full privacy policy covering all permissions
- Add content_security_policy to manifest.json
- Update description to be clearer for CWS reviewers
2026-03-25 15:07:40 +08:00
Saeed Al Mansouri 0ff28aa0d8 fix(extension): security hardening — tab isolation, URL validation, cookie scope (#409)
* fix(extension): security hardening — tab isolation, URL validation, cookie scope

Addresses issues raised in #399 (Astro-Han's community triage):

1. Tab isolation bypass: resolveTabId now verifies that an explicit tabId
   belongs to the automation window (tab.windowId === session.windowId)
   before accepting it. Tabs from the user's browsing session are rejected.

2. URL scheme allowlist: isDebuggableUrl switched from a blocklist
   (chrome://, chrome-extension://) to an allowlist (http://, https:// only).
   handleNavigate and tabs.new also reject non-http(s) URLs early, blocking
   file://, javascript:, and data: scheme abuse.

3. Cookie scope restriction: handleCookies now requires domain or url.
   Requests with neither are rejected instead of dumping all browser cookies.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(extension): resolve data: URI vs allowlist conflict, plug tabs.select bypass

- Add BLANK_PAGE constant and whitelist it in isDebuggableUrl so
  internal blank tabs are not treated as non-debuggable after the
  blocklist-to-allowlist change.
- Add isSafeNavigationUrl for user-facing URL validation (http/https
  only), keeping it separate from internal isDebuggableUrl.
- Fix tabs.select to verify tab belongs to automation window before
  activating, closing a tab isolation bypass.
- Normalize error message style (-- instead of em dash).

* fix(extension): add try-catch for tabs.select with explicit tabId

Gracefully handle the case where cmd.tabId points to a closed tab
instead of letting the unhandled exception bubble up.

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
2026-03-25 14:03:18 +08:00
jakevin 3b2a51fcd3 fix(extension): revert #377 and cleanly fix same-url navigation timeout (#380)
* Revert "fix(extension): avoid same-url navigation timeout (#377)"

This reverts commit b7ada0e38c.

* fix(extension): avoid same-url navigation timeout

- Add normalizeUrlForComparison for minimal URL canonicalization
  (root slash + default port only; preserves hash and non-root paths)
- Fast-path: skip navigation when tab is already at the target URL
- Rewrite wait logic with finish() pattern to prevent double-resolve
- Handle both same-URL and redirect scenarios in navigation listener
- Add regression tests for same-URL and hash-route distinction
2026-03-24 22:25:29 +08:00
ayotme b7ada0e38c fix(extension): avoid same-url navigation timeout (#377)
* fix(extension): avoid same-url navigation timeout

* review: preserve hash-aware extension navigation

---------

Co-authored-by: huruichen <huruichen@kanzhun.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
2026-03-24 22:05:22 +08:00
jakevin 56d4326646 refactor(extension): reuse async detach() in registerListeners (#328)
Replace inline sync chrome.debugger.detach() in onUpdated listener
with the shared async detach() function for consistent cleanup behavior
across all detach paths.
2026-03-24 02:18:34 +08:00
QSam2023 4c9a2b1fde fix: detach debugger before navigation in browser bridge (#322)
* fix: detach debugger before navigation in browser bridge

* refactor: make detach() async, await all detach calls

- cdp.ts: detach() now async, awaits chrome.debugger.detach()
- background.ts: await detach() in handleNavigate and handleTabs close
- Eliminates theoretical race between detach and subsequent tab operations

Co-authored-by: jackwener <jackwener@gmail.com>

---------

Co-authored-by: bluey_heeler <fragwang231@gmail.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
Co-authored-by: jackwener <jackwener@gmail.com>
2026-03-24 02:14:31 +08:00
jakevin c44bc62b60 chore: code cleanup + extension conflict troubleshooting (#260)
Cleanup:
- Remove redundant double-retry in resolveTabId (was retrying data: URI
  with the same data: URI)
- Fix stale comment (30s → 120s idle timeout)
- Remove verbose debug logging in resolveTabId
- Built extension is now smaller (16.66kB vs 17.18kB)

Extension conflict:
- Add hint to attach-failed error when chrome-extension:// URL is detected
- Add troubleshooting entry for extension conflicts (e.g. youmind, New Tab
  Override) to both README.md and README.zh-CN.md

Ref: #249
2026-03-22 22:18:22 +08:00
jakevin 7f57e76485 fix: treat empty tab URL as debuggable (fixes first-run doctor --live failure) (#259)
When a new automation window is created, the initial tab URL may be
empty briefly while Chrome loads the data: URI. isDebuggableUrl('') was
returning false, causing ensureAttached to reject the tab.

Fix: only reject known non-debuggable URLs (chrome://, chrome-extension://).
Empty/undefined URLs are now treated as debuggable since they represent
tabs still loading.

Also adds 200ms delay after window creation to let Chrome populate the
tab URL.
2026-03-22 22:10:30 +08:00
jakevin 3e91876d13 fix: replace all about:blank with data: URI to prevent New Tab Override interception (#257)
Root cause: getAutomationWindow and resolveTabId used about:blank which
New Tab Override extensions intercept immediately, replacing it with
chrome-extension:// URLs that cannot be debugged.

Changes:
- Window creation: about:blank → data:text/html
- reuseTab fallback: about:blank → data:text/html
- newTab handler: about:blank → data:text/html
- Added diagnostic logging to resolveTabId for debugging
- Synced extension version to 1.2.4

Ref: #249
2026-03-22 22:04:25 +08:00
jakevin 112fdefa8d fix: harden resolveTabId against New Tab Override extension interception (#255)
resolveTabId's reuseTab path now verifies the URL is actually debuggable
after navigating to about:blank. If a New Tab Override extension intercepts
it (setting it back to chrome-extension://), falls back to a data: URI,
then creates a fresh tab as last resort.

This fixes the persistent 'attach failed: Cannot access chrome-extension://'
error for users with New Tab Override extensions installed.

Ref: #249
2026-03-22 21:47:44 +08:00
jakevin 71b2c3961b fix: harden browser automation pipeline (resolves #249) (#251)
- resolveTabId: validate URL even for explicit tabId, fall through to
  auto-resolve when tab is not debuggable or has been closed
- handleNavigate: wait for URL change before checking 'complete' status
  to avoid race condition with stale about:blank
- ensureAttached: pre-check tab URL, verify cached attach with probe,
  invalidate cache on URL change via onUpdated listener
- daemon-client: recognize transient extension errors (disconnected,
  attach failed) as retryable with 1500ms delay; fresh command ID per attempt
- pipeline executor: add per-step retry for browser steps (up to 2 retries
  on transient errors); cleanup automation window on pipeline failure
- page.ts: selectTab/newTab/closeTab properly update/invalidate _tabId
- daemon.ts: add WebSocket ping/pong heartbeat (15s interval, 2-miss disconnect)
- Increase automation window idle timeout from 30s to 120s
- Fix timeout param edge cases in BrowserBridge._ensureDaemon
- Remove unused chalk import; fix trailing import placement

Closes #249
2026-03-22 21:23:21 +08:00
AstroHan fbf051d539 fix(extension): skip chrome-extension:// tabs in resolveTabId fallback (#198)
* fix(extension): skip chrome-extension:// tabs in resolveTabId fallback

Remove the unsafe fallback that returned `tabs[0]` regardless of URL
type. When no web-accessible tab exists in the automation window (e.g.
a New Tab Override extension replaced about:blank with its own
chrome-extension:// page), we now always create a fresh about:blank
tab instead. This prevents chrome.debugger.attach from failing with
"Cannot access a chrome-extension:// URL of different extension".

Fixes #195, fixes #197

* refactor(extension): rename isWebUrl → isDebuggableUrl & reuse tabs in resolveTabId

Improvements over the original fix:

1. Rename isWebUrl() → isDebuggableUrl(): better reflects the intent —
   the function determines whether a URL can be attached via CDP, not
   just whether it's a "web" URL (about:blank is debuggable but not
   really a web URL).

2. Reuse existing non-debuggable tabs: when a New Tab Override extension
   replaces about:blank with chrome-extension://, use chrome.tabs.update()
   to navigate the existing tab to about:blank instead of creating a new
   one. This prevents orphan tab accumulation since chrome.tabs.create()
   may also get intercepted by the same extension.

3. Only fall back to chrome.tabs.create() when the window has zero tabs,
   which is the truly empty-window edge case.

---------

Co-authored-by: jackwener <jakevingoo@gmail.com>
2026-03-21 23:05:06 +08:00
jackwener 44f0bbe94d feat: add workspace-aware browser sessions 2026-03-20 13:41:04 +08:00
jackwener 0ea6e4a15c fix: address review findings and docs cleanup 2026-03-20 12:28:30 +08:00
jackwener f5f7a9500e chore: rename extension to OpenCLI 2026-03-20 11:03:02 +08:00
jackwener b5c1b242e2 fix(extension): use idle-timeout for automation window lifecycle
Replace eager close-window (which caused race conditions when
parallel commands shared the window) with an idle-based timer:

- Window auto-closes 30s after the last command completes
- Each incoming command resets the idle timer
- Consecutive commands reuse the same window (faster)
- No race conditions with parallel execution
- Close-window action kept for explicit cleanup if needed
2026-03-20 00:05:48 +08:00
jackwener 2f6d28a3e9 feat(extension): auto-close automation window after command completes
- Add 'close-window' action to extension protocol and background.ts
- Add Page.closeWindow() method to send close-window command
- browserSession() now closes automation window in cleanup
- Remove domain pre-navigation + 2s wait from main.ts (CDP handles
  cross-domain cookies natively, no same-origin workaround needed)
- Net effect: commands run faster, no stale windows left behind
2026-03-19 23:59:31 +08:00
jackwener fde618063f chore: pre-release cleanup
- Delete unused extension/src/executor.ts (chrome.scripting experiment)
- Remove 15 no-op backward-compat exports from doctor.ts
- Remove getTokenFingerprint no-op from browser/index.ts
- Rename PlaywrightMCP → BrowserBridge across all source files
  (backward-compat alias kept in mcp.ts and browser/index.ts)
- Remove unnecessary host_permissions from extension manifest
- Sync extension package.json version to 0.2.0
- All 14 tests pass
2026-03-19 23:51:36 +08:00
jackwener 89947fee50 feat(extension): isolated automation window
All opencli operations now run in a dedicated Chrome window instead
of hijacking the user's active tab. The automation window:
- Created on first command via chrome.windows.create({ focused: false })
- 1280x900 viewport, auto-cleaned up when closed
- All tabs resolved within this window only
- User's main browsing session is never touched

Tested: twitter trending ✅, zhihu hot ✅
2026-03-19 23:34:27 +08:00
jackwener 59c0d639a5 refactor: fix 9 issues from round 2 code review
Bug fixes:
- #1 /logs?level=error returned 404 — use pathname for route matching
- #2 Duplicate initialization — added 'initialized' guard flag

Should fix:
- #4 Added screenshot() to IPage interface
- #5 Graceful shutdown rejects pending requests before exit
- #6 Use process.execPath instead of 'npx tsx' for faster daemon spawn

Cleanup:
- #7 Removed duplicate 'browser' keyword in package.json
- #8 Removed unused normalizeEvaluateSource import from browser.ts
- #9 Changed dynamic import to static import in intercept.ts
- #10 Added explicit throw at end of sendCommand for clarity

61 tests pass (4 test files). Extension: 10.55KB.
2026-03-19 16:36:06 +08:00
jackwener 3d1f9640ea feat: forward extension console logs to daemon
Extension side:
- Hook console.log/warn/error → forward via WS as { type: 'log', level, msg, ts }
- Original console output preserved (for chrome://extensions debug)

Daemon side:
- Ring buffer (200 entries) stores extension logs
- Logs printed to daemon stderr with emoji prefix (📋/⚠️/❌)
- GET /logs — returns buffered logs (optional ?level= filter)
- DELETE /logs — clears log buffer

Usage:
  curl localhost:19825/logs              # view all logs
  curl localhost:19825/logs?level=error  # errors only
  curl -X DELETE localhost:19825/logs    # clear buffer

Extension build: 10.48KB
2026-03-19 16:21:17 +08:00
jackwener 8e8c4a0229 feat: add exponential backoff reconnect + CDP screenshot support
Exponential backoff:
- Reconnect delay: 2s, 4s, 8s, 16s, ..., capped at 60s
- Resets to base delay on successful connection
- Reduces idle CPU waste vs fixed 3s reconnect

Screenshot via CDP Page.captureScreenshot:
- New 'screenshot' action in protocol (5th action)
- Supports format (png/jpeg), quality, fullPage
- Full-page: uses Emulation.setDeviceMetricsOverride for scroll height
- CLI-side: page.screenshot() with optional file save
- Extension build: 9.81KB (+1.7KB from 8.11KB)

Inspired by bb-browser's architecture patterns.
2026-03-19 16:21:17 +08:00
jackwener 01b8b6b5bf refactor: fix 14 issues from deep code review
P0 Critical:
- #1 Fix double IIFE wrapping: unified wrapForEval() replaces
  normalizeEvaluateSource + ad-hoc wrap in page.evaluate()
- #2 Fix navigate race: check tab.status before addListener,
  reduced timeout 30s→15s

P1 Should Fix:
- #8 Remove unused permissions (scripting, host_permissions, content_scripts)
- #10 Add retry (3x, 500ms) + timeout (30s) to sendCommand()

P2 Cleanup:
- #3 Extract isWebUrl() to safely handle undefined tab.url
- #4 Sanitize maxDepth with Math.max/min bounds
- #6 Delete empty src/daemon/ directory
- #7 Remove dead createJsonRpcRequest + its test
- #9 Remove stale IIFE-mode comment
- #11 Validate body.id in daemon request handler
- #12 Guard ensureAttached: detach+re-attach on 'already attached'
- #14 Extract _tabOpt() helper (removes 13x spread duplication)
- #15 Add verbose warning for unsupported consoleMessages()

All 35 unit tests pass.
2026-03-19 16:21:17 +08:00
jackwener b2fa7daf57 feat: replace @playwright/mcp with lightweight daemon + Chrome Extension
Architecture:
- Micro-daemon (HTTP + WebSocket bridge, ~190 lines, auto-start/idle-exit)
- Chrome MV3 Extension using chrome.debugger CDP (10KB build)
- 5 protocol actions: exec, navigate, tabs, cookies, screenshot
- All DOM ops via JS evaluate — no extension update needed for new features

Key features:
- CDP Runtime.evaluate for JS execution in page context
- Tab management, cookie access via Chrome APIs
- Auto-start daemon on cold boot, idle auto-exit (5min)
- Minimal permissions: debugger, tabs, cookies, activeTab, alarms

Tested: zhihu hot (14.3s), twitter timeline (9.3s)
2026-03-19 16:21:17 +08:00