feat: CDP mode, screencast recorder, snapshot perf, multi-instance isolation, mode-aware anti-detection, and CLI/daemon hardening#21
Merged
Conversation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…unctionality - Updated descriptions for CLI commands to clarify usage and parameters, including `fill-form`, `press`, `mouse-down`, `mouse-up`, `wait`, `switch-tab`, and `close-tab`. - Enhanced the `wait` command to support a custom timeout option, allowing users to specify a maximum wait duration. - Adjusted the implementation of the `wait` command to include timeout handling in the underlying logic. - Improved mock setups in tests to reflect changes in command behavior and ensure accurate testing of new functionalities.
- Added a new `--cdp` option to the `search` command, allowing users to connect to an existing browser session instead of launching a new one. - Updated the command's implementation to handle the new `cdp` parameter and resolve the input correctly. - Modified CLI command descriptions to reflect the addition of the `--cdp` option. - Adjusted related tests to verify the correct behavior of the updated `search` command with the new option.
- Enhanced the `fake_read` function in the `TestFindCdpUrl` class to improve browser detection by checking for variations of "chrome" in a case-insensitive manner. - Added conditions to exclude "canary", "unstable", and "beta" versions of Chrome to ensure accurate URL retrieval for the standard Chrome browser.
- Introduced a new `VideoRecorder` class to handle single-stream video recording on the active tab, allowing seamless switching between tabs while maintaining a continuous output file. - Updated CLI commands to reflect the new video recording behavior, including enhanced descriptions for `video-start` and `video-stop`. - Adjusted documentation to clarify that only the active tab is recorded, improving user understanding of the video recording process. - Enhanced tests to validate the new video recording features and ensure proper functionality across different scenarios.
- Modified tests to use absolute paths for screenshot and video stop commands, ensuring consistent behavior across different environments. - Added conditional skips for tests that rely on OS-specific functionality, improving compatibility with Windows. - Refactored filename extraction in snapshot text tests to utilize `os.path.basename` for better cross-platform support.
- Added caching for Playwright browsers in the Windows CI workflow to speed up subsequent runs. - Changed the CI runner from `windows-latest` to `ubuntu-latest` for better compatibility. - Refactored path handling in the `_daemon.py` and `_video_recorder.py` files to improve directory access checks and ensure proper handling of local application data. - Updated tests to use `as_uri()` for generating test URLs, enhancing cross-platform compatibility.
…setup - Updated the README files to include detailed instructions for enabling remote debugging in Chrome 144+ using the in-browser UI and launch flags for earlier versions. - Added platform-specific paths for the `DevToolsActivePort` file to improve user guidance. - Clarified the connection process for `bridgic-browser` with examples and sources for further reading. - Expanded the `CDP_MODE.md` documentation to cover the same remote debugging setup, ensuring consistency across documentation.
P0-1 snapshot perf: extract _batch_get_elements_info JS to a module-level constant; pre-build a role index (one querySelectorAll per unique role instead of per ref); memoize accessible-name / textContent in WeakMaps; chunk the Python payload (500/batch) with asyncio.sleep(0) between chunks so other daemon commands can interleave on large pages (~10x speedup for 1000+ refs). P0-2 click hang: cap click/dblclick/check/uncheck at 10s and fall back to locator.dispatch_event() on PlaywrightTimeoutError - stops the 30s actionability retry loop that froze the CLI on Vue/React SPA pages. P1-3 launch crash loop: wrap launch_persistent_context / launch in _retriable_launch with exponential backoff (0s -> 1s -> 2.5s) for transient "SingletonLock" / "target closed" failures; fail fast on other errors. P2-4 observability: emit matched [CLI-CMD]/[CLI-RESP] log pairs around every _dispatch with timing; warn on commands >60s; log _snapshot_lock wait time when >0.1s. Adds 21 unit tests covering chunking behaviour, fallback semantics, retriable token matching, and dispatch log shape.
…-labels' into feature/cdp-url-support
…three-phase evaluate Previous fix chunked page.evaluate() calls to yield the event loop between chunks, but built the roleIndex (querySelectorAll per role) inside each chunk's JS call. On a 5549-ref page with 12 chunks, the expensive `div:not([role])` selector ran 12× instead of once, causing 95-123s runtimes and DAEMON_RESPONSE_TIMEOUT failures. Fix: split into three phases per snapshot call: - Phase 1 (_BUILD_ROLE_INDEX_JS): one evaluate that runs QSA for all unique roles across the entire batch and stores Element arrays in window.__bridgicRoleIndex. Uses a generation token to prevent stale data from a previous snapshot bleeding into a new one. - Phase 2 (_BATCH_INFO_JS, N chunks): reads window.__bridgicRoleIndex instead of re-scanning the DOM. Falls back to per-chunk QSA if Phase 1 was skipped or failed (defensive). - Phase 3 (_CLEANUP_ROLE_INDEX_JS): deletes window.__bridgic* variables in a finally block to free Element references and prevent cross- snapshot contamination. Results on aiqicha company detail page (5549 refs, 12 chunks): Before: 95-123s → client DAEMON_RESPONSE_TIMEOUT After: 41-50s → completes within 90s limit Remaining ~40-50s is getBoundingClientRect × 5549 layout flushes, an unavoidable DOM API cost. Also updates TestBatchChunking tests to account for the build+cleanup calls (N+2 evaluate calls instead of N), adds 4 new tests: - test_role_index_built_once_regardless_of_chunks - test_build_phase_receives_all_unique_roles - test_cleanup_is_last_evaluate_call - test_cleanup_runs_on_chunk_exception
…d performance Added functionality to pre-warm snapshots in the background after navigation, allowing for instant cache hits on subsequent snapshot requests. Introduced a dedicated prefetch generator to avoid conflicts with the main snapshot generator. Implemented cancellation of in-flight prefetch tasks during navigation or page switches to maintain cache integrity. Updated tests to validate the new pre-filtering behavior for interactive and non-interactive elements.
# Conflicts: # pyproject.toml # uv.lock
…o recorder - cli/_client: compute per-command socket timeout from `timeout`/`seconds` args so long-running `wait` commands do not race the 90s default. - cli/_daemon: (1) cancel in-flight dispatch when the client disconnects mid-request to avoid orphaned tasks against the Browser singleton; (2) reject new connections once shutdown begins so back-to-back close→open does not crash mid-dispatch; (3) force-reset internal handles in `_cdp_reconnect` so `_start()`'s early-return guard cannot silently skip reconnect after a partial close; (4) factor out `_resolve_cdp_url_from_env` and short-circuit pre-resolved ws URLs. - session/_browser: (1) add `_probe_cdp_alive` to skip stale DevToolsActivePort files (crash / kill -9 leftovers); (2) expand Linux CDP scan dirs to cover Snap, Flatpak, Edge, Chrome Beta; (3) broaden system-Chrome detection for macOS `~/Applications` and Linux Chromium-based binaries; (4) canonicalize host in port-mode URL and always rewrite ws URL netloc for SSH-tunnel / port-forward cases. - session/_download: expose `detach_from_page` for page-scoped cleanup in CDP borrowed-context mode. - session/_snapshot: key the per-snapshot role index / caches on a generation token so concurrent snapshot tasks on the same page do not clobber each other's window state. - session/_video_recorder: (1) drain ffmpeg stderr into a capped buffer so a full pipe cannot deadlock `stdin.drain()`; (2) harden ffmpeg path discovery (X_OK check, Windows fallback, clearer error). - tests: add unit + integration coverage for all of the above.
- Added a pre-close cancellation of in-flight snapshot prefetch to prevent errors from stale tasks during reconnect. - Implemented a TCP probe to verify the reachability of WebSocket targets, providing clearer error messages for stale CDP URLs. - Improved the handling of malformed DevToolsActivePort files to ensure robust URL generation. - Enhanced scroll race detection in CDP evaluations to prevent executing JavaScript on incorrect elements. - Added regression tests to ensure proper behavior during video recording and CDP interactions, including handling of closing states and error scenarios.
Add explicit Optional[str] annotation plus pyright ignore on the None branch so basedpyright stops flagging the validation raise as dead code. The helper _read_devtools_active_port can legitimately return None for malformed files, so the check is intentional.
- Updated the SDK and CLI to use `cdp` instead of `cdp_url` for connecting to existing Chrome instances via CDP. - Adjusted documentation and examples to reflect the new parameter name. - Ensured backward compatibility by updating relevant methods and tests to handle the new naming convention.
- Added a skip condition for the test that checks for non-executable ffmpeg binaries, specifically for Windows environments, to avoid false failures due to OS limitations.
- snapshot: release _snapshot_lock before awaiting in-flight prefetch to avoid a self-deadlock against _pre_warm_snapshot's commit path; add regression test. - snapshot: include STRUCTURAL_NOISE_ROLES in interactive pre-filter and run precise visibility checks on INTERACTIVE_ROLES leaf controls so off-viewport buttons/links no longer leak into viewport-only snapshots. - snapshot: only consult interactive_map when the ref is actually in it; missing entries fall back to suffix heuristics instead of forcing a false negative. - download: track in-flight per-page download tasks and cancel them on detach/close so files are not written after teardown; swallow CancelledError instead of reporting download.failure(). - video: remove context-level 'page' listener auto-subscribe; recording now hot-swaps only on bridgic-initiated tab switches. Reap ffmpeg via wait_for(wait()) on CDP setup failure to avoid zombies. - cli: redact CDP URL path/query in the browser-closed hint so tokens are not echoed back to the user. - docs: dedupe the active-tab bullet in CDP_MODE.md.
Previously _get_dropdown_option_locators tried locator.locator("option")
first, which matched the hidden native <select> Arco / Element Plus /
AntD host inside their trigger for a11y / form posting. Clicks on those
options are silent no-ops: select_dropdown_option_by_ref would return
"Selected option: X" while the UI stayed unchanged.
Reworked resolution:
- gate the native path on tagName == "select"
- for custom comboboxes, prefer aria-controls / aria-owns with a
visibility filter at the option level (not container level, so
virtualized portals with 0x0 shells still work)
- ascend one level to cover AntD / Arco / Element Plus class patterns
when the aria-controls target contains only hidden ghost options
- filter [role='option'] descendants strictly visible so nested shadow
<option> can't win
- keep the single-visible-listbox fallback
Adds a Section 4 shadow-select fixture and two integration tests covering
the options listing and the actual selection-applied path.
…(H02/H03/M01)
H02 — CDP reconnect across Chrome restart
- Daemon clears _cdp_resolved + _closing before _start() so reconnect
re-resolves the ws URL against the live Chrome (fresh browser UUID
instead of the stale one that 404s).
- CLI client no longer pre-resolves --cdp; the raw port / "auto" /
http value is forwarded to the daemon so reconnect can re-probe.
- CDP scan path also re-probes /json/version for UUID freshness.
- _is_browser_closed_error walks __cause__ so TargetClosedError
wrapped by asyncio.gather still classifies as BROWSER_CLOSED.
H03 — click fallback budget
- Dispatch-event fallback gets an explicit FALLBACK_DISPATCH_TIMEOUT_MS
(2s) so unstable / disabled elements fail fast instead of pegging
the main click budget for 10s and pressing the wrong target.
M01 — evaluate_javascript arrow-fn CDP parity
- _maybe_wrap_arrow_fn normalises bare arrow expressions into Runtime
function calls, matching Playwright's Launch-mode semantics so the
same user code works in CDP borrowed mode.
- asyncio.gather exception context primed via raise-from so originals
surface in trace instead of being lost.
Tests
- Unit: browser / cdp_reconnect / cli / find_cdp_url coverage for the
new paths.
- Integration: test_click_fallback.py (H03 shake/stable fixtures);
test_cdp_borrowed_mode arrow-fn parametrize (M01 parity).
Docs
- docs/CDP_MODE.md Connection drops section reflects reconnect
re-resolution and the input-form reconnect matrix.
QA tooling (scripts/qa/) committed so clean-clone CI can run the P0
suite and CLI coverage sweep; tmp-upload.txt / cli-full-coverage.html
stay ignored via the prior chore commit.
Version bumped to 0.0.5.dev6.
…ion (task.md P0 gaps) Three task.md P0 scenarios the existing integration suite did not exercise, consolidated into already-present test files (no new per-scenario file proliferation): - §5.2 TestActionTools::test_click_disabled_button_raises_without_firing_handler Clicking <button disabled> must raise; dispatch_event fallback must not sneak the onclick past (guards H03 / M01 regression). - §8.1 TestNavigationTools::test_ref_invalidated_after_go_back go_back() drops the snapshot cache; reusing a pre-navigation ref must raise instead of silently landing on a same-role element on the restored page. - §2.3 test_cdp_lifecycle::test_cdp_close_does_not_kill_remote_chrome bridgic "close" in CDP mode must be pure-disconnect: the borrowed Chrome process keeps running and a subsequent --cdp open reattaches to the same pid. Plus an internal cleanup: Chrome lifecycle helpers (pick_free_port, launch_chrome, wait_for_chrome, kill_chrome) sunk into tests/integration/_chrome_utils.py so every CDP integration test shares one implementation.
Covers 3 link modes (persistent / ephemeral / CDP attach) x 2 display modes (headless / headed) plus a stealth=off smoke, all against real browsers. - run-mode-matrix.sh: top-level orchestrator for V1..V7 with auto free-port selection for CDP variants and per-variant user_data_dir for V7 to avoid stealth flag-set collisions. - run-cli-full-coverage.sh: accept BRIDGIC_QA_* env vars to inject --headed / --clear-user-data / --cdp per variant; V1 default preserved. - run-sdk-coverage.py: cover 11 SDK methods not exposed through the CLI (get_snapshot, get_element_by_ref, scroll_to_text, switch_to_page, etc.) for V1 / V3 / V5. - render-mode-matrix-report.py: aggregate per-variant TSVs to a single Markdown matrix report with summary and failure details. - mode-matrix-scenarios.md: document per-variant semantics, prerequisites, expected N/A commands, and known limitations. - CLAUDE.md: surface the mode-matrix entrypoint. - cli-command-matrix.md: record the 7-variant execution baseline (584 data points across CLI + SDK, 0 FAIL, 5 expected N/A).
…etonLock crashes Headed and headless Chromium can't safely share the same profile dir, so launch_persistent_context now always lands in <base>/headed or <base>/headless (applies to both the default ~/.bridgic/bridgic-browser/user_data and a custom user_data_dir). The public Browser.user_data_dir property is unchanged. Also fixes a pre-existing test bug: test_navigate_to_empty_url_raises_invalid_input forgot to mock async_playwright, so navigate_to's _ensure_started() was silently launching real Chromium and writing to the real profile dir on every unit run.
Verifies restore_storage_state writes localStorage and cookies under each origin's own scope, not under the currently navigated page's origin.
Allow users to override the root state directory via BRIDGIC_HOME env var so multiple independent CLI daemon instances can run in parallel, each with its own socket, logs, user data, and config paths. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…wser_closed_error - _browser.py: reset self._closing = False at top of _start() so a Browser instance can be reused after close() + restart via _ensure_started() - _daemon.py: rewrite _is_browser_closed_error as iterative loop with seen-set guard to prevent stack overflow on circular exception chains Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…one, CDP session leak, video context, download timeout, probe timeout 1. evaluate_javascript() CDP path now checks exceptionDetails and raises OperationError instead of silently returning the error description 2. go_back()/go_forward() non-CDP path checks Playwright return value; raises StateError(NO_HISTORY_ENTRY) when None (no history) 3. _apply_debugger_skip_pauses uses try/finally to guarantee CDPSession detach even on CancelledError 4. start_video() uses page.context instead of self._context to avoid None race during concurrent close 5. wait_for_download uses deadline-based single timeout instead of double timeout that could wait 2x the expected duration 6. _probe_cdp_alive default timeout now uses _timeouts.CDP_PROBE_S instead of hardcoded 2.0s Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Update Features, Snapshot tool description, and Browser component sections to use $BRIDGIC_HOME notation consistently. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ADMEs Document the storage-save/storage-load workflow for sharing login sessions across browser instances and across headed/headless modes.
page.go_back() returns None for both "no history" and same-document navigations (e.g. anchor hash changes). Compare URL before/after to distinguish the two cases.
Adds the CDP-mode skill reference (`skills/bridgic-browser/references/cdp-mode.md`) covering when to use CDP, how to enable it on Chrome, and the connection forms (CLI / SDK / env). Updates the existing CLI / SDK / env-vars references and SKILL.md index to point at the new section. Adds an API probe test (`tests/integration/test_opener_api_probe.py`) that validates Playwright's opener / page-event / close-event guarantees — foundation for the upcoming owned-page tracking design.
…anup, webdriver removal, IDL truncation
Code (bridgic/browser/session/_stealth.py + _browser.py):
- R1: clean `HeadlessChrome` from UA — context `user_agent` fallback +
CDP `Emulation.setUserAgentOverride` (anchored CDP session keeps the
override sticky across navigations; `Network.*` variant drops UAD
metadata silently in Chromium 145+, so we use Emulation). Headless-only.
- R2: `plugins`/`mimeTypes` get native `PluginArray`/`MimeTypeArray`
prototype identity via `setPrototypeOf`; `item(i)` does `i >>> 0`
uint32 truncation per Web IDL §3.2.4 (fixes incolumitas overflowTest
where `plugins.item(4294967296)` must equal `plugins[0]`).
- R3: `Worker` / `SharedWorker` / `serviceWorker.register` constructor
wrap injects worker-stealth code via `importScripts` before the
worker's first instruction (race-proof). Gated to top-frame; further
filtered to blob:/data:/same-origin URLs only — cross-origin URLs
pass through unchanged so libraries like reCAPTCHA v3 (which uses
cross-origin gstatic.com workers) don't hang on CORS-failed
importScripts. Worker stealth body covers `webdriver`, `deviceMemory`,
`hardwareConcurrency`, `languages`, `vendor`/`vendorSub`/`productSub`,
WebGL UNMASKED_VENDOR/RENDERER, and console.* Error stringify.
- R4 + R6: `delete Navigator.prototype.webdriver` (Web IDL §3.7.6
guarantees configurable: true). Replaces the previous undefined-getter
approach which kept the descriptor present, leaving `'webdriver' in
navigator` returning true (an internally inconsistent fingerprint).
Headless via init script; headed via `_arm_r6_webdriver_delete` —
`page.evaluate` on the main frame plus `framenavigated` re-apply,
deliberately not `add_init_script` (would propagate to Cloudflare
iframes).
- R5-lite: `console.{log,debug,info,warn,error,trace}` pre-stringify
any Error argument before forwarding. Defeats CDP-attach detection
via `error.stack` getter trap (deviceandbrowserinfo
`isAutomatedWithCDP*` test class). In top-frame guard inside
anti-devtools-detector script, also in worker stealth body.
Iframe-safety invariant (the red line throughout this work):
"Any patch that can propagate into a cross-origin iframe MUST be
gated to `self._headless`."
Mode-aware gating: R1 / R3 (post-spawn worker hook) / R5-lite are
headless-only via `_browser.py:_start()` `if self._headless` checks;
the in-script Worker constructor wrap has its own
`if (window === window.top)` guard; R6 in headed uses per-page
`page.evaluate` (per-frame execution context) instead of
`add_init_script` (which would fan out to all frames).
Verified end-to-end on the CloakBrowser-published benchmark
(tests/test_stealth.py):
bot.sannysoft.com | 0/57 fail (both modes)
bot.incolumitas.com | 0 fail (both modes — overflowTest +
WEBDRIVER both fixed; CloakBrowser still
shows 1 fail on WEBDRIVER)
browserscan.net | 0 abnormal / 19 normal (both modes)
demo.fingerprint.com | Pass (headed) — TLS-blocked in headless
reCAPTCHA v3 | score 0.9 (both modes)
Plus manual verification: Cloudflare Turnstile (chat-auto-team.pages.dev)
issues a 1114-byte token, x.com manual login reaches /home with all
three logged-in selectors present, blog.aepkill.com devtools-detector
reports `status: close`.
Documentation:
- README.md / README_zh.md: new "Anti-Detection" / "反检测" section
with benchmark table + 24-vector coverage list. Removed the
CloakBrowser-specific reference sections per scope cleanup.
- docs/INTERNALS.md: extends "Stealth JS Init Script — Patched
Properties" with new vectors; adds "Mode-aware Stealth Design"
chapter with decision flow + per-mode patch matrix; documents
CDP UA cleanup gotchas and worker constructor wrap (incl. the
same-origin filter rationale).
- docs/KNOWN_LIMITATIONS.md: new entries for TLS fingerprint
(headless), `isAutomatedWithCDPInWebWorker` (headed), and Worker
constructor wrap (incl. same-origin filter).
- CLAUDE.md: adds "Iframe-safety rule (CRITICAL)" subsection with the
per-mechanism matrix and a 5-point checklist for reviewing future
stealth patches.
Browser now maintains an internal `_owned_pages: Set[Page]` so all public
tab ops (get_pages / get_tabs / switch_tab / close_tab / _close_page
fallback) only see pages bridgic itself spawned or adopted via opener
relationship. In CDP borrowed mode the user's pre-existing tabs and any
Cmd+click / Cmd+T / address-bar tabs they later open stay invisible to
bridgic — preventing an LLM driving bridgic from switching to, reading,
or closing private work tabs.
Mechanism:
- `context.on("page", _on_new_page)` listener schedules `_maybe_adopt_page`
which queries `await page.opener()` and adopts iff opener is already
owned. `_mark_owned` is idempotent and registers a per-page on-close
pruner.
- Popup-follow: when an adopted popup's opener is `self._page`, the active
page moves to the popup (mirrors Chrome foreground-promotion UX).
Disable via `Browser(auto_follow_popups=False)`.
- `_close_page` four-tier fallback when closing the active page:
opener → focus_stack LRU → owned-first by context order → None.
- CDP-borrowed `_switch_self_page_to` migrates DownloadManager attachment
so popups don't leak downloads into the user's Chrome default path.
- Triple `_closing` guards in adoption listener prevent race during
shutdown; navigate_to recovery path uses `_recover_page_in_existing_context`
to ensure the rebuilt page is correctly marked owned (fix for a real
ghost-page bug found during manual testing).
Adoption is governed by Chromium's CDP `Target.attachedToTarget.openerId`,
which is populated by foreground-tab navigations (programmatic click, user
plain left-click, `window.open()` with user gesture) and cleared by
background-tab navigations (Cmd/Ctrl+click, middle-click). Verified by
manual 12-case fixture + counter-experiment that:
- `rel="noopener"` / `rel="noreferrer"` / `window.open(...,'noopener')` do
NOT prevent adoption — they only suppress JS-level `window.opener`, not
the CDP-level relationship. bridgic reads CDP, so these are ineffective.
- Adoption is role-agnostic, behavior-driven: Playwright-dispatched click
with `modifiers=['Meta']` is indistinguishable from a real Cmd+click,
so `bridgic key-down Meta + click` produces an orphan tab bridgic
cannot see (real but rarely-useful capability — not promoted as API).
- Cmd+T / address bar / history tabs have no opener at all and never
enter the owned set.
Tests:
- Unit: 1374 passing. New coverage in test_browser_methods.py for owned
membership (U1-U18), close-fallback chain, popup adoption, race
guards under _closing, navigate-to recovery, get_tabs hint with
hidden user tabs.
- New integration suite test_owned_pages.py (I1-I10): user-tab
invisibility, popup adoption via opener, popup-close fallback,
download manager follows popup in borrowed mode, non-CDP all-visible
fallback chain, opener-API probe.
- Pre-existing test_cdp_borrowed_mode.py and test_cdp_cli_full.py are
module-level skipped — old API contract (user tabs accessible) no
longer holds; replaced by test_owned_pages.py with new-semantics
coverage.
- Mode-matrix V5 (CDP attach × headless): FAIL=0.
Docs:
- INTERNALS.md: full Owned-page Tracking section with adoption truth
table (2-condition rule: opener populated + opener owned), Why
Cmd-click strips openerId (Chromium NEW_FOREGROUND_TAB vs
NEW_BACKGROUND_TAB browser-process paths), and tradeoffs.
- CDP_MODE.md / skills/cdp-mode.md: user-facing ✅/❌ adoption table
including why `rel=noopener` is ineffective at CDP layer.
- CLAUDE.md, README.md, README_zh.md, sdk-guide.md, SKILL.md: synced
with Cmd+click vs plain left-click distinction.
Replace _maybe_wrap_arrow_fn — which only handled top-level arrow-literal expressions — with _wrap_js_for_cdp_eval, mirroring Playwright's internal normalizeEvaluationExpression + utilityScript.evaluate. Arbitrary JS now round-trips byte-identically between CDP-borrowed Runtime.evaluate and non-CDP page.evaluate(str): - Indirect-eval via globalThis.eval(src) yields V8 REPL completion-value semantics — IIFEs with trailing `;`, statement lists, class declarations, let/const blocks, template literals, async/await all parse and return the same as page.evaluate. - Auto-call when the eval result is a function (matches Playwright's isFunction === undefined branch). - Pre-emptively rewrite Window / Document / Node / Error instances into JSON-safe substitutes so common host-object returns stay interchangeable. Bumps version dev11 → dev13. New integration test tests/integration/test_evaluate_cdp_parity.py runs a 28-row parity matrix against a real Chrome Runtime.evaluate session.
…ting
Chrome 138+ honors the user's "Ask where to save each file" preference even
when CDP Browser.setDownloadBehavior(allowAndName, downloadPath=...) is
sent via a browser-level session — the Save As dialog still pops. The fix
is to send that exact command via a PAGE-level CDP session attached to
bridgic's own tab (BrowserContext.new_cdp_session(self._page)); Chrome
treats that as target-scoped and bypasses the user pref. agent-browser's
Some(session_id) argument is the same trick.
Because allowAndName writes files under GUID names, add CdpDownloadRenamer
that subscribes to Browser.downloadWillBegin / downloadProgress on the
same page session, snapshots the suggestedFilename per GUID, and renames
<dir>/<guid> → <dir>/<real name> on completion. Filename sanitization
strips path separators, Windows-forbidden chars, control bytes, and
truncates to 255 bytes. Conflicts resolve to "name (1).ext", "name (2).ext".
Download path resolution for CDP-borrowed mode now follows:
explicit Browser(downloads_path=...) / config >
CLI client's per-command CWD (mirrors curl -O ergonomics) >
~/Downloads
The CLI client puts os.getcwd() in every socket request; the daemon sets
Browser._pending_client_cwd before dispatch so the first lazy-start L1
sees it. The daemon skips its auto-default downloads_path=~/Downloads in
CDP mode — otherwise that default is indistinguishable from a user
configuration and silently wins over CWD priority.
Empirical alternatives tried & rejected (table in CLAUDE.md): Browser
session with/without allowAndName, Page.setDownloadBehavior(allow), and
Browser.setDownloadBehavior with defaultBrowserContextId from
Target.getBrowserContexts (Chrome rejects: "Failed to find browser
context for id").
Docs touched: CLAUDE.md (new Downloads section + 3-mode pipeline table +
empirically-tried-alternatives matrix), README.md / README_zh.md
(download path matrix), docs/API.md (Downloads pipeline overview +
CdpDownloadRenamer note), docs/INTERNALS.md (clarify
_switch_self_page_to DownloadManager swap is now a no-op safety net in
CDP-borrowed mode), docs/KNOWN_LIMITATIONS.md ("Show in Folder" applies
to all pipelines), skills env-vars (downloads_path per-mode behavior).
Tests: 17-case test_cdp_download_renamer.py (sanitize / lifecycle /
rename / conflict / canceled / set_default_dir / robustness); updated
TestCdpDownloadBehaviorOverride to assert allowAndName + eventsEnabled
+ page-session routing; new TestEffectiveCdpDownloadsPath and
TestUpdateCdpDownloadsPath.
Five new integration tests in test_owned_pages.py lock the SDK-exit cleanup contract in CDP borrowed mode: user's pre-existing tabs survive bridgic's disconnect, every bridgic-created tab (initial page, new_tab, adopted popup) is reaped — no residue, no accumulation across sessions, even when user code crashes mid-task. Scenarios (each pairs an independent Playwright probe that diffs Chrome's target multiset before vs after the `async with Browser(...)`): I11 clean exit reaps bridgic tabs I12 mid-task RuntimeError still triggers reap I13 three consecutive sessions — no per-session drift I14 popup adopted via `<a target=_blank>` reaped at SDK exit I15 user manually closes bridgic's tab → SDK exit still clean New _chrome_snapshot helper: read-only Counter of every page URL in the connected Chrome via a separate connect_over_cdp + close() (disconnect only). The Counter diff is each scenario's primary assertion. Why these tests didn't already exist: older mock-based tests asserted `page.close.assert_not_called()` — implementation-bound, silently locked the leak in. The real-business-code regression (skills.sh scraper leaking a list tab on every SDK exit) surfaced the gap; the contract is what should have been tested all along. Fixture stability: chrome_with_user_tabs now passes --password-store=basic + --use-mock-keychain so Playwright's bundled Chromium doesn't silently hang on macOS's keychain prompt during headless launch (no-op on Linux/Windows CI). Tests: tests/integration/test_owned_pages.py — 15 passed in 20s (I1-I10 unchanged + I11-I15 new).
`self._cdp_download_session` is a PAGE-scoped CDP session (attached via `BrowserContext.new_cdp_session(self._page)`). The previous teardown order closed every owned page first, then tried to send `Browser.setDownloadBehavior(default)` on that now-dead session — every borrowed-mode close logged: [CDP pre-close] Browser.setDownloadBehavior failed: CDPSession.send: Target page, context or browser has been closed. Move L3 restore + renamer detach + page-session detach to before the page.close() loop. L2 orphan rescue stays where it is (it only needs to run before `browser.close()` destroys the artifactsDir, independent of page lifecycle). After the fix the same close path logs: [CDP pre-close] restored Chrome native download behavior No behavior change for owned/launch/persistent modes.
`test_wrapper_parity_with_playwright` flaked on Windows GitHub Actions
runners with TimeoutExpired on the first parametrized case ("spread
args (1 arg)"). Symptoms:
- 28/29 cases passed, only the first (cold-start) case timed out.
- Hit exactly the 10s subprocess.run timeout.
- macOS/Linux runs reliably <500ms per case.
Root cause is Windows-specific cold-start latency: Defender real-time
scan + I/O contention on the runner can push the first `node.exe`
spawn to 5-15s. Subsequent invocations are warm and back to normal.
Bump the cap to 30s. Warm-path runtime is unchanged; the cap only
kicks in on cold runners.
…cklist Replace the specific chat-auto-team.pages.dev redeem page with a generic "Cloudflare-Turnstile-protected page" placeholder. The methodology (3-site headed verification covering Cloudflare / server-side detection / devtools probe) is unchanged; reviewers pick their own Cloudflare-protected target.
tielei
approved these changes
May 18, 2026
Previous 10s → 30s bump (commit 7c9ec68) did not stabilize the suite. Latest Windows CI run failed two parametrized cases ("spread args (1 arg)" + "plain expr") at exactly the 30s ceiling, including the trivial "document ? 42 : 0" probe that completes in <100ms locally. Hitting the budget on a near-empty JS payload rules out cold-start / JIT and points at Windows Defender real-time scan + node.exe spawn queueing on GitHub Actions runners — a per-spawn cost that simply raising the timeout cannot solve (next failure would just creep up to 60s, 120s, …). Skip the whole class on win32 instead. Rationale: - `_wrap_js_for_cdp_eval` is pure Python string composition; its cross-platform behavior is identical, so macOS/Linux CI fully covers the regression risk. - JS semantic parity against the real V8 engine is independently verified end-to-end in tests/integration/test_evaluate_cdp_parity.py against actual Chrome. - The Windows-specific node-spawn flake provides no signal we don't already have elsewhere. Also reverts the 30s timeout knob to the original 10s (now that Windows skips, the cap only governs macOS/Linux behavior, where 10s has always been comfortable).
NiceCode666
added a commit
to dreamluyao/bridgic-browser
that referenced
this pull request
May 19, 2026
…ile-download-failure Resolved 5 conflicts that came up after dev landed PR bitsky-tech#21 (CDP attach) and PR bitsky-tech#25 (deps installer): - bridgic/browser/session/_download.py: kept dev's _pending_waiters Future-list + per-page task tracking for wait_for_download(), while also keeping PR bitsky-tech#28's _completed_queue used by the new wait_for_next_download() — they cover different call sites and now coexist. Also drained _completed_queue inside clear_history() so a subsequent wait_for_next_download() can't yield a stale completion. - README.md / README_zh.md: combined PR bitsky-tech#28's "always-on DownloadManager" description with dev's two-pipeline / path-matrix CDP docs; merged the two code samples into one block that demonstrates wait_for_next_download() in non-CDP mode and contrasts the CDP-borrowed flow. - docs/API.md: kept PR bitsky-tech#28's new SDK methods and corrected the download_manager / downloaded_files rows for the merged behavior (manager is always created but not attached in CDP-borrowed mode). - skills/bridgic-browser/references/cli-sdk-api-mapping.md: kept dev's updated video-stop semantics and appended PR bitsky-tech#28's download workflow row. make test-quick: 1249 passed. Tool count = 69.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
New features
Browser(cdp=...)SDK arg and--cdpCLI flag onopen/search. Accepts a port number (9222),ws:///wss://URL,http://host:portdiscovery endpoint, orautoto scan local Chrome / Chromium / Brave / Edge / Chrome-Beta profiles (incl. Snap & Flatpak on Linux). Works around Playwright's_mainContext()hang on pre-existing tabs by driving title / navigation / element evaluation / page metrics through rawCDPSessioncalls.VideoRecorder(session/_video_recorder.py) replaces Playwright's built-inrecord_video_dir. CDP screencast frames are piped intoffmpeg(VP8, 5 Mbps, CRF 4). Two-phase shutdown (prepare_stopwhile Chrome is alive →finalizeafter exit), single continuous output stream that hot-swaps the screencast source whenever bridgic's active tab moves (viaswitch_tab/new_tab/navigate_to/close_tab). Independent background tabs or unowned popups do not trigger a switch.BRIDGIC_HOMEmulti-instance isolation — configurable root state directory viaBRIDGIC_HOMEenv var. All derived paths (run info, socket, logs, tmp, config, user data) follow the override, enabling multiple independent CLI daemon instances in parallel.wait --timeout— configurable appearance/disappearance timeout on thewaitcommand.SnapshotGeneratorruns in the background after navigation so the nextget_snapshot()is an instant cache hit. In-flight prefetch is canceled on navigate / page-switch / close to preserve cache integrity.<base>/headedvs<base>/headless) to prevent ChromiumSingletonLockcrashes when switching modes.Browsermaintains an internal_owned_pagesset so all public tab ops (get_tabs/switch_tab/close_tab) only show pages bridgic itself spawned or adopted viaPage.opener(). In CDP borrowed mode the user's pre-existing tabs stay invisible, and any tabs the user later opens via Cmd+click / Ctrl+click / middle-click (Chromium's background-tab path stripsopenerId) / Cmd+T / address bar (no opener at all) are also unseen — a deliberate privacy boundary that prevents an LLM driving bridgic from switching to, reading, or closing the user's private work tabs. NewBrowser(auto_follow_popups=False)parameter keepsself._pagefixed on the original tab when an adopted popup spawns. Decision rule, Chromium internals, and the full ✅/❌ truth table are documented indocs/INTERNALS.md(Adoption truth table + "Why Cmd-click strips openerId") anddocs/CDP_MODE.md.curl -Oergonomics —Browser.setDownloadBehavior(allowAndName, downloadPath=..., eventsEnabled=true)is sent over a page-level CDP session attached to bridgic's tab (BrowserContext.new_cdp_session(self._page)); browser-level routing was empirically shown to NOT bypass Chrome's "Ask where to save each file" preference on Chrome 138+ (agent-browser uses the sameSome(session_id)page-routing trick). NewCdpDownloadRenamer(session/_cdp_download_renamer.py) subscribes toBrowser.downloadWillBegin/downloadProgresson the same page session and renames<dir>/<guid>→<dir>/<real name>on completion (filename sanitization strips path separators / Windows-forbidden chars / control bytes / >255 byte; conflicts resolve toname (1).extChrome-style). Effective-path priority: explicitBrowser(downloads_path=...)/bridgic-browser.jsonconfig > the CLI client's per-command CWD (every socket request now carriesos.getcwd(); the daemon hot-swaps viaupdate_cdp_downloads_pathbefore dispatch) >~/Downloads. The daemon's auto-defaultdownloads_path=~/Downloadsis suppressed in CDP mode so it can't shadow the CWD priority chain. Empirically-tried-and-rejected alternatives (inCLAUDE.md—Page.setDownloadBehavior(allow),Browser.setDownloadBehaviorover a browser session with eitheralloworallowAndName, passingdefaultBrowserContextIdfromTarget.getBrowserContextswhich Chrome rejects with "Failed to find browser context for id") are documented.Anti-detection (mode-aware)
tests/test_stealth.py) — 5/5 passing in headed mode, 4/5 in headless (the only headless miss isdemo.fingerprint.com/web-scraping, a server-side TLS fingerprint check unreachable from JS).user_agentfallback + CDPEmulation.setUserAgentOverride(withuserAgentMetadata.brands). Anchored CDP session keeps the override sticky across navigations;Network.*variant silently drops UAD metadata in Chromium 145+ so we deliberately use Emulation.navigator.plugins/mimeTypes+ Web IDL §3.2.4 uint32 truncation initem(i)(fixesincolumitasoverflowTestwhereplugins.item(4294967296) === plugins[0]).importScriptsbefore the worker's first instruction. Same-origin filter (blob:/data:/ same-origin only) prevents CORS breakage on cross-origin worker URLs (e.g. reCAPTCHA'sgstatic.comworker).delete Navigator.prototype.webdriverinstead of an undefined-getter, achieving'webdriver' in navigator === false(Web IDL §3.7.6 guaranteesconfigurable: true). Headless via init script; headed via per-pagepage.evaluate+framenavigatedre-apply on the main frame only.console.{log,debug,info,warn,error,trace}Error pre-stringify — defeats CDP-attach detection that useserror.stackgetter traps (deviceandbrowserinfoisAutomatedWithCDP*).add_init_scriptruns in all frames, including Cloudflare Turnstile's challenge iframe). Headed mode uses targetedpage.evaluate+ same-origin filters to keep cross-origin iframes pristine. Documented as a hard rule inCLAUDE.mdwith a 5-point review checklist.Performance
session/_snapshot.py) — extract_batch_get_elements_infoJS to a module-level constant, split into (1) one-shot role-index build viaquerySelectorAllper unique role intowindow.__bridgicRoleIndex, (2) N×500-ref batch info reads that consume the index, (3)finally-cleanup. Per-snapshot state is keyed by a generation token so concurrent snapshots on the same page cannot clobber each other. ~10× speed-up on pages with 1000+ refs; fixes a regression where large pages (5549 refs) timed out at 95–123 s → 41–50 s.asyncio.gatherparallelization forpage.title()inget_all_page_descs,is_visible()in_prefer_visible_locators, snapshot +page_infoinget_full_page_info, and per-page close inclose().evaluate_javascriptparity withpage.evaluate—_wrap_js_for_cdp_evalmirrors Playwright'snormalizeEvaluationExpression+utilityScript.evaluateso arbitrary JS strings (IIFEs with trailing;, statement lists, class declarations,let/constblocks, template literals, async/await) round-trip byte-identically between CDP-borrowed and non-CDP modes. Replaces the narrower_maybe_wrap_arrow_fn. 28-case parity matrix verified intests/integration/test_evaluate_cdp_parity.py.Bug fixes (CR follow-ups)
_closingflag not reset on restart —_start()now resetsself._closing = False, fixing Browser instance reuse afterclose()._is_browser_closed_errorstack overflow — rewritten from recursion to iterative loop withseenset guard against circular exception chains.evaluate_javascript()CDP path swallowed JS exceptions — now checksexceptionDetailsand raisesOperationErrorinstead of returning the error description as a string.go_back()/go_forward()silent failure — non-CDP path now checks Playwright return value; raisesStateError(NO_HISTORY_ENTRY)whenNone(no history entry)._apply_debugger_skip_pauses—try/finallyguarantees detach even onCancelledError.start_video()race with concurrent close — usespage.contextinstead ofself._contextto avoidNoneduring teardown.wait_for_downloaddouble timeout — switched to deadline-based single timeout; actual wait no longer exceeds the user-specified duration. Note:wait_for_downloadis unsupported in CDP-borrowed mode (Playwright's per-contextdownloadevent does not fire when downloads are routed away fromartifactsDir); use non-CDP / CDP-owned mode for programmatic wait, or polldownloads_pathafter the click in CDP-borrowed mode._probe_cdp_alivetimeout mismatch — default now uses_timeouts.CDP_PROBE_Sinstead of hardcoded2.0s.CLI freeze / hang fixes (from daemon.log forensics)
click/dblclick/check/uncheckat 10 s and fall back tolocator.dispatch_event()onPlaywrightTimeoutErrorinstead of letting the 30 s actionability retry loop freeze the CLI on Vue / React SPAs.launch_persistent_context/launchin_retriable_launchwith exponential backoff (0 → 1 → 2.5 s) for transientSingletonLock/target closedfailures; fail fast on all other errors.[CLI-CMD]/[CLI-RESP]log pairs with timing around every_dispatch; warn on commands > 60 s; log_snapshot_lockwait time when > 100 ms.Daemon hardening (
cli/_daemon.py,cli/_client.py)timeout/secondsargs so long-runningwaitcommands don't race the 90 s default.Browsersingleton).close→opencannot crash mid-dispatch._cdp_reconnectso_start()'s early-return guard cannot silently skip reconnect after a partial close._resolve_cdp_url_from_env; short-circuit already-resolvedws://URLs.~/Downloadsis now scoped to non-CDP mode only. In CDP mode the daemon leavesdownloads_pathunset so the new CDP path-resolution chain (explicit > client CWD >~/Downloads) drives behavior.os.getcwd(); the daemon setsBrowser._pending_client_cwdbefore dispatch and callsupdate_cdp_downloads_path()to re-target CDPsetDownloadBehaviorwhen the effective path changes.CDP detection hardening (
session/_browser.py)_probe_cdp_aliveskips staleDevToolsActivePortfiles left by crashes /kill -9.~/Applicationsand Linux Chromium-based binaries.ws://netloc so SSH-tunneled / port-forwarded targets work.DevToolsActivePortfiles.evaluate()prevents executing JS on the wrong element.VideoRecorder hardening (
session/_video_recorder.py)ffmpegstderr into a capped buffer to prevent a full pipe from deadlockingstdin.drain().X_OKcheck, Windows fallback, clearer error on missing binary).Config hardening (
_config.py)BRIDGIC_BROWSER_JSONenv var instead of silently failing later.Refactoring
Set[Page]+ parallel LRUList[Page]focus stack (replaces an earlier mid-branch refactor that removed all ownership bookkeeping)._mark_ownedis idempotent and registers a per-pageon("close")pruner;_maybe_adopt_pageruns from acontext.on("page")listener and adopts iffawait page.opener()is already owned. Triple_closingguards prevent races during shutdown._close_pagefour-tier fallback:closed_page.opener()→_focus_stacktop →get_pages()[0]→None(latter triggers existingnavigate_toauto-recreate branch, which now correctly marks the rebuilt page owned — fixed a real ghost-page bug found in manual testing).close()remains a pure disconnect (no tabs killed), butstart_video/ snapshot / tab ops are now scoped to owned pages only — matching the privacy-boundary intent._switch_self_page_to(new_page)consolidates "move self._page" side effects (focus stack push,_invalidate_page_state,_switch_video_to_page). A legacyDownloadManager.detach_from_page(old) + attach_to_page(new)swap is retained in CDP-borrowed mode as a no-op safety net —DownloadManageris no longer the active CDP-borrowed download pipeline (CdpDownloadRenameris, bound to bridgic's originalself._page's CDP session). Downloads triggered from a followed popup fall back to Playwright's per-context override (artifactsDir) and rely on the L2 rescue net on close.Docs & infra
docs/CDP_MODE.md(remote-debugging setup, Chrome 144+ UI + launch flags,DevToolsActivePortpaths per OS).docs/INTERNALS.md(CDP bypass internals, snapshot generation token, video recorder pipeline, download pipeline notes).docs/KNOWN_LIMITATIONS.md— including the Chrome "Show in Folder" bug (Chromium #324282051) which is triggered bysetDownloadBehavior(eventsEnabled: true)and therefore affects all three download pipelines.CLAUDE.md— new### Downloadssection with 3-mode pipeline table, effective-path priority chain, empirically-tried-alternatives matrix, CdpDownloadRenamer design notes, and caveats.downloads_pathset/unset → effective path).--cdp,--timeout,BRIDGIC_HOME, multi-instance isolation,downloads_pathper-mode behavior, command descriptions).ubuntu-latestfor compatibility; Playwright browser caching added.Stats
93 files changed, +23,809 / −2,038 across 67 commits. (Stats are approximate — additional CDP eval-parity, silent-downloads, and lifecycle-test commits were appended on top; see the diff for authoritative numbers.)
Test plan
Unit & integration coverage added across the change surface:
make test-quick(unit) passes — 1247+ tests covering CDP URL resolution, reconnect, daemon env plumbing, client timeout, config, detect-chrome, snapshot chunking / role-index / cleanup, video recorder, CLI commands, BRIDGIC_HOME constants, click fallback, CDPevaluate_javascriptparity, CDP-borrowed download behavior + filename rename.test_cdp_borrowed_mode,test_cdp_cli_full,test_cdp_lifecycle,test_cli_close_race,test_cli_long_wait,test_click_fallback,test_snapshot_concurrency,test_evaluate_cdp_parity(28-case JS round-trip parity matrix against real Chrome).test_constants(BRIDGIC_HOME),test_video_recorder_frame_ts,test_cdp_reconnect,test_daemon_cdp_env,test_detect_chrome,test_cli_client_timeout,test_cdp_download_renamer(17 cases: sanitize / lifecycle / rename / conflict / canceled / set_default_dir / robustness).tests/test_stealth.py) — sannysoft (0/57 fail, both modes), incolumitas (0 fail, both modes —WEBDRIVERandoverflowTestboth fixed), browserscan (0 abnormal, both modes), demo.fingerprint.com web-scraping (PASS in headed), reCAPTCHA v3 (score 0.9, both modes)./homewith all logged-in selectors present; blog.aepkill.com devtools-detector reportsstatus: close. All in headed mode.tests/integration/test_owned_pages.py(10 cases: user-tab invisibility, popup adoption via opener, popup-close fallback chain, download manager follows popup in borrowed CDP, non-CDP all-visible mode, opener API probe). New unit coverage (U1-U18 intest_browser_methods.py) for owned set membership, close-fallback four-tier order, popup adoption identity check, race guards under_closing, navigate-to recovery, andget_tabshidden-tab hint. Pre-existingtest_cdp_borrowed_mode.py/test_cdp_cli_full.pyare module-level skipped (old "user tabs accessible" contract replaced by new-semantics coverage). Mode-matrix V5 (CDP attach × headless): FAIL=0.bridgic click <ref>adopts; user plain left-click on<a target="_blank">/window.open()adopts (regardless ofrel="noopener"/rel="noreferrer"/window.open(...,'noopener')— these only suppress JS-levelwindow.opener, not CDP-levelopenerId); user Cmd+click / Ctrl+click / middle-click → NOT adopted;bridgic key-down Meta + click + key-up→ also NOT adopted (Playwright keyboard state propagates intoInput.dispatchMouseEvent.modifiers, role-agnostic). HTTPS source vsfile://source has no effect — only the click path matters.cd <dir> && bridgic-browser open --cdp auto <github-release> && bridgic-browser click @<whl-ref>landsbridgic_browser-0.0.4-py3-none-any.whl(146441 B, real filename) directly in<dir>with no Save As dialog, no GUID residue, and a[CdpDownloadRenamer] <guid-prefix> → <real-name>daemon-log entry. Verified across all four mode/caller combinations: CLI+CDP (CWD), CLI+non-CDP (~/Downloadsvia DownloadManager), SDK+CDP (explicitdownloads_path), SDK+non-CDP (explicitdownloads_pathvia DownloadManager).bridgic-browser open https://example.com --cdp 9222against a locally-running Chrome.bridgic-browser search "test" --cdp autoauto-discovers a running browser.bridgic-browser video-start→ browse →bridgic-browser video-stop ./out.webmproduces a playable file that includes tab switches.bridgic-browser wait --timeout 5 "Submit"respects the custom timeout.bridgic-browser closein CDP mode disconnects without killing the remote Chrome.BRIDGIC_HOME=/tmp/b2 bridgic-browser open https://example.comlaunches independent daemon instance.🤖 Generated with Claude Code