* [packages/cli]feat: add cloud browser provider support (-p flag)
Adds first-class support for launching cloud browser sessions through
4 providers (driver.dev, hyperbrowser, browseruse, browserless) via
the new `-p`/`--provider` flag, plus supporting registry, restart and
output plumbing.
Provider module
- New `browser/session/provider.rs` with typed `connect_provider` /
`close_provider_session` and per-provider env-var resolution.
- HTTP client built with bounded request (30s) and connect (10s)
timeouts so a hung control-plane can't stall the daemon. Cleanup
client uses tighter 10s/5s budgets.
- Hyperbrowser profile IDs are normalised to UUID v5 so non-UUID
identifiers (e.g. "user-42") map deterministically.
Cloud session lifecycle
- Registry tracks `provider` + `provider_session` so cleanup paths
can release paid sessions on close/restart/error.
- New `find_cloud_session_by_provider(provider, profile)` reuse path
in `start::execute` performs a `Target.getTargets` health probe
before reuse and removes stale entries on failure (mirrors the
existing endpoint reuse path).
- `execute_cloud` cleanup paths funnel through `fail_reserved_cloud_start`
/ `cleanup_provider_session_if_any` so all four error branches
release the provider session — `panic = "abort"` means we cannot
rely on Drop.
- Restart hybrid policy: stateless providers (driver.dev, browseruse)
reuse the original cdp_endpoint + headers so a rotated env var
cannot break a stable session; stateful providers (hyperbrowser,
browserless) re-mint via the connect path. Provider tag is
re-applied to the new entry so observability survives.
Errors and redaction
- New `CliError::ApiUnauthorized` (401/403), `ApiRateLimited` (429),
`ApiServerError` (5xx) variants with stable codes and actionable
hints; rate-limit and server errors are marked retryable.
- `redact_endpoint` now scrubs query parameters whose name matches a
secret key list (apiKey, token, password, …, case-insensitive) in
addition to long path segments. Provider WSS URLs carry the API
key in the query string, so the previous path-only redactor was
leaking credentials in success responses.
- 5 new redaction unit tests cover query, path, case and edge cases.
Config / CLI / output
- `BrowserConfig.provider` + `ACTIONBOOK_BROWSER_PROVIDER` env var
with cloud-mode auto-flip when a provider is set implicitly.
- `browser status` / `browser list` / start responses now surface
the provider name when present.
* [packages/cli]fix: forward provider env vars from CLI client to daemon
The daemon's std::env::var reads its own frozen environment (captured at
daemon-spawn time), not the user's current shell. This silently broke
provider config: rotating an API key in the shell never reached the daemon
until it was killed and restarted.
Add a per-request snapshot mechanism: the CLI client process collects all
DRIVER_DEV_*/HYPERBROWSER_*/BROWSER_USE_*/BROWSERLESS_* env vars via
collect_provider_env_from_process() and ships them in start::Cmd.provider_env
and restart::Cmd.provider_env (#[arg(skip)], serde default). The daemon
reads from this map instead of touching its own env.
For close/restart, snapshot the env onto ProviderSession.provider_env so
the daemon can talk to the provider control plane later, even when the
calling shell no longer has the keys exported.
* [packages/cli]fix: rewrite driver.dev as stateful provider with REST API
The previous implementation hardcoded wss://cdp.driver.dev as a "stateless"
provider, which DNS-resolves to NXDOMAIN. The real driver.dev is stateful:
mint a session via POST https://api.driver.dev/v1/browser/session and use
the per-session distributed cdpUrl returned in the response.
Changes:
- Replace DRIVER_DEV_WS_BASE constant with DRIVER_DEV_API_BASE
- Rewrite connect_driver_dev to POST /v1/browser/session with bearer auth
and parse {sessionId, cdpUrl}; supports country, nodeId, type, proxyUrl,
windowSize, profile body fields
- Add driver.dev case to close_provider_session (DELETE with sessionId)
- DRIVER_DEV_WS_URL escape hatch preserved for tests / private deployments
- Accept both DRIVER_DEV_API_KEY (Actionbook namespace) and DRIVER_API_KEY
(driver.dev's official docs name) via read_driver_dev_api_key
- Broaden PROVIDER_ENV_PREFIXES from "DRIVER_DEV_" to "DRIVER_" so the
fallback official-name keys also reach the daemon
- Add is_driver_dev_auth_failure body sniffer: driver.dev returns HTTP 500
with {"error":"Invalid consumer token"} for bad credentials, which the
generic 5xx → ApiServerError(retryable=true) mapping wrongly classifies.
Reclassify known auth bodies to ApiUnauthorized(retryable=false).
- 3 new unit tests
* [packages/cli]fix: filter dangerous URLs from preserved open_url on restart
restart preserves the registry's tab.url so the new session lands on the
same page. But the registry's tab.url is captured at session-launch time
and only refreshed by list-tabs — after a goto, the in-memory copy is
stale. Worse, driver.dev launches with a data:text/html,<title>... watermark
page that the L3 dangerous-protocol guard blocks on re-navigation.
Drop data:, javascript:, and about:blank from the preserved open_url so
the new session boots to the provider default and the user can goto again
from a clean state.
Also update the stale comment that listed driver.dev as a stateless
provider — after the API rewrite it has a control-plane session that needs
explicit cleanup.
* [packages/cli]fix: redact cdp_endpoint in reuse and local start paths
reuse_running_session emitted entry.ws_url verbatim, which for cloud sessions
contains the raw provider WSS URL with tokens embedded as query params
(e.g. Hyperbrowser's JWT). HYP-3 reproduced this in E2E by reusing a running
session — the JWT showed up plaintext in stdout.
execute_local was inconsistent for the same reason: local CDP URLs don't
carry secrets, but the rule should be uniform — every cdp_endpoint emission
must go through redact_endpoint().
Both sites now route through redact_endpoint(). All four cdp_endpoint JSON
emissions in start.rs are now redacted.
* Fix cloud provider session lifecycle
* [packages/cli]fix: address PR #507 cloud provider review comments
Bundle of PR #507 review fixes for cloud provider session lifecycle:
P1 — correctness / resource leaks
- start.rs: cleanup provider session on race with concurrent close
during startup (codex P1, start.rs:1064). When the placeholder
reserved at the top of execute_cloud is removed while we were busy
minting the remote session, the local provider_session handle was
dropped without stopping the remote browser. Now release the lock
and call cleanup_provider_session_if_any before returning
SESSION_NOT_FOUND.
- start.rs: close stale provider session before reconnecting on
failed health probe (codex P2, start.rs:314). A transient CDP hiccup
would orphan the previous remote session and spin up a duplicate.
- close.rs: introduce Closing state to close out the TOCTOU race
between Phase 1 (clone handle) and Phase 2 (HTTP PUT stop). A second
concurrent close now short-circuits with SESSION_CLOSING; on
provider stop failure the state reverts to the prior status so the
caller can retry.
- registry.rs: find_cloud_session_by_provider ignores WS_URL override
sessions (they have no provider_session handle); is_active() now
excludes both Closing and Closed.
P2 — output / configuration hygiene
- start.rs: endpoint_for_mode() emits local loopback CDP URLs
verbatim (so the caller can actually attach) while still redacting
cloud ws_urls that embed tokens.
- restart.rs: stateful provider restart keeps user-supplied CDP
headers (codex P2, restart.rs:177) instead of clearing them to
vec![], so sessions that needed custom headers for the initial
connect can also reconnect.
- goto.rs: refresh TabEntry.url/title in the registry after a
successful navigation so a restart-preserved open_url reflects the
navigated page, not the launch-time stub.
- Cargo.toml: release profile uses lto = "thin" (full LTO was
SIGKILLing the binary on macOS before commands reached the daemon).
P3 — observability / internals
- provider.rs: each connect_* helper now sets provider_env directly
when constructing ProviderSession, removing the post-hoc assignment
in connect_provider.
- provider.rs: is_driver_dev_auth_failure logs its classification
decision (ApiUnauthorized vs ApiServerError) with a 120-char body
snippet for easier triage.
Verified: cargo check --all-targets clean, cargo test --lib 335/335
passing. Pre-existing clippy warnings (collapsible_if, too_many_args)
are unrelated to this change.
* [packages/cli]style: fix fmt + clippy lints for CI
Addresses `Lint (fmt + clippy)` check failures on PR #507.
fmt: let cargo fmt rewrap a handful of long function signatures,
assert!() args, and closures to the project default width. No
semantic changes.
clippy:
- restart.rs:134 — collapse nested `if let Some(..)` / `if let Err(..)`
into a single `let`-chain condition.
- restart.rs:187 — replace the `if let Some(saved) / else` fallback
with `saved_provider_env.unwrap_or_default()`.
- start.rs:861 — collapse the same nested `if let` pattern in
`cleanup_provider_session_if_any`.
- start.rs:908 — annotate `execute_cloud` with
`#[allow(clippy::too_many_arguments)]`. All 8 arguments are
independent inputs resolved by the caller; folding them into a
struct would just be lint placation.
Verified locally: cargo fmt -- --check, cargo clippy --all-targets
-- -D warnings, and cargo test --lib (335/335) all clean.
* [packages/cli]docs: make -p/--provider self-documenting for agents
Before: \`-p <PROVIDER>\` rendered a single-line help string with no
list of valid values and no hint about the required auth env var.
Agents reading \`browser start --help\` had to guess a name, run the
command, read the runtime error, then guess the env var — 2–3 round
trips per provider.
After: the same field uses a \`PossibleValuesParser\` with per-value
help text, and the doc comment / \`after_help\` block spell out the
conflict and env-var rules up front. One \`--help\` read is enough
for an agent to pick a provider, know what to export, and see a
working command line.
Rendered output:
-p, --provider <PROVIDER>
Cloud browser provider (implies --mode cloud).
... mutual exclusion + stateful-restart note ...
Possible values:
- driver: driver.dev — requires DRIVER_DEV_API_KEY (or DRIVER_API_KEY)
- hyperbrowser: hyperbrowser.ai — requires HYPERBROWSER_API_KEY
- browseruse: browser-use.com — requires BROWSER_USE_API_KEY
Plus an after_help section with export-then-run examples for each
provider and a note about \`browser restart\` minting fresh remote
sessions for provider-backed IDs.
Implementation notes:
- Kept \`Cmd.provider\` as \`Option<String>\` deliberately. Switching to
a \`ValueEnum\` would have required touching \`config.rs\`,
\`restart.rs\`, registry lookups, and three in-repo test setups —
the string form survives round-tripping through IPC, TOML config,
and env merging with no conversion helpers.
- The \`browseruse\` alias for \`browser-use\` is preserved via
\`PossibleValue::aliases\`.
- \`normalize_provider_name\` still runs at \`execute()\` time so
daemon-side IPC input gets the same validation that clap now
enforces for CLI input.
- Clap now rejects invalid \`-p\` values at parse time with
\`[possible values: driver, hyperbrowser, browseruse]\` — the
runtime branch at \`execute()\` remains as an IPC safety net.
* [packages/cli]refactor: drop DRIVER_DEV_API_KEY alias, accept only DRIVER_API_KEY
driver.dev's official docs use the bare `DRIVER_API_KEY` name for the
auth credential. We previously accepted both `DRIVER_DEV_API_KEY` and
`DRIVER_API_KEY` as a transitional alias, but that exposed two names
for the same secret in every surface (--help, error hints, docs) and
confused agents reading the help output.
Keep only `DRIVER_API_KEY`:
- provider.rs: `read_driver_dev_api_key` no longer falls back to the
namespaced name; error message names only `DRIVER_API_KEY`.
- start.rs: after_help and PossibleValue help text drop the alias.
- error.rs: ApiUnauthorized hint names `DRIVER_API_KEY`.
- tests: rewritten to assert the retired name is rejected so a
future silent-fallback regression is caught.
`PROVIDER_ENV_PREFIXES` still includes `DRIVER_` (not `DRIVER_DEV_`),
so `DRIVER_API_KEY` and any `DRIVER_DEV_*` tuning knobs both survive
the CLI→daemon env forward.
* [root]chore: add changeset for CLI v1.4.0 (cloud provider support)
Minor bump for the `-p / --provider` cloud browser provider feature
(driver.dev, hyperbrowser.ai, browser-use.com) plus the stateful
session lifecycle, self-documenting --help, and the DRIVER_API_KEY
alias cleanup shipped on feature/cli-cloud-provider.
* test(daemon): add Windows daemon red coverage (ACT-897)
Add tests and CI configuration for Windows daemon TCP transport:
- config.rs: test_actionbook_home_uses_userprofile_when_home_unset — cross-platform
red test; fails until USERPROFILE fallback is implemented
- server.rs: port_path() stub (path-only) + #[cfg(all(test,windows))] unit tests for
is_daemon_running, send_sigterm, is_pid_alive Windows behaviour
- tests/e2e/windows_daemon.rs: 3 e2e tests ignored on non-Windows, run on Windows CI
- .github/workflows/e2e-windows.yml: windows-latest CI job for unit + e2e tests
* feat(daemon): add Windows TCP localhost IPC support (ACT-897)
Unix behaviour is unchanged (UnixListener/UnixStream, flock).
Windows now uses TCP localhost instead of the missing Unix socket:
server.rs:
- run_daemon(): bind TcpListener on 127.0.0.1:0, write ephemeral port to
daemon.port; use fs2::FileExt for cross-platform exclusive file locking
- is_daemon_running(): TCP connect-probe against daemon.port (Windows)
- send_sigterm(): taskkill /F /PID (Windows)
- is_pid_alive(): delegates to is_daemon_running() TCP probe (Windows)
- handle_connection_inner(): generic AsyncRead+AsyncWrite handler shared by
both platforms; Unix/Windows handle_connection() call it with the
appropriate stream type
- port_path(): daemon.port path (used both platforms for path derivation)
client.rs:
- DaemonClient (Windows): TcpStream-based connect/send_action
- auto_start_daemon_windows(): spawns daemon with DETACHED_PROCESS |
CREATE_NEW_PROCESS_GROUP flags so it survives parent exit
- wait/restart/cleanup helpers mirroring the Unix equivalents
config.rs:
- actionbook_home(): fall back to USERPROFILE when HOME is not set
(standard Windows home directory convention)
Cargo.toml:
- Add fs2 = "0.4" for cross-platform LockFileEx file locking
* style: apply cargo fmt to windows_daemon tests
* fix: serialize USERPROFILE test with config test_lock to prevent env var race
The test_actionbook_home_uses_userprofile_when_home_unset test manipulates
ACTIONBOOK_HOME and HOME env vars without acquiring the shared test_lock,
causing intermittent failures in other config tests that run concurrently.
* fix: gate Unix-only kill() FFI behind #[cfg(unix)] for Windows build
- start.rs: wrap orphan Chrome kill(pid, 0)/kill(pid, 9) in #[cfg(unix)],
add #[cfg(windows)] equivalent using taskkill
- chrome_reaper.rs: gate Duration/Instant imports behind #[cfg(unix)]
since they're only used in the Unix SIGTERM graceful shutdown path
* fix: make drop_session_entry test cross-platform for Windows CI
The test used Unix-only `sleep` and `kill -0` commands. Use `ping` for
a long-lived process on Windows and `tasklist` for liveness checks.
* ci: limit Windows e2e to lifecycle, tab management, and daemon tests
Run only windows_daemon, browser_lifecycle, and tab_management test
modules on Windows CI to avoid timeouts from tests not yet adapted
for Windows. Run sequentially with --test-threads=1.
* ci: narrow Windows e2e to windows_daemon + tab_management
browser_lifecycle tests hang on Windows (close_kills_chrome_process
stuck for 30min). These tests exercise daemon lifecycle edge cases
not yet adapted for Windows. Narrow scope to windows_daemon and
tab_management which cover core IPC and browser operations.
* ci: narrow Windows e2e to windows_daemon tests only
tab_management tests require full Chrome browser interaction and hang
on Windows CI. Only run our Windows-specific daemon IPC tests which
validate the TCP localhost approach without Chrome dependencies.
* fix: avoid pipe-inheritance hang in e2e harness on Windows
On Windows, assert_cmd pipes stdout/stderr. When the CLI spawns a
detached daemon, the daemon inherits those pipe handles via
CreateProcess(bInheritHandles=TRUE). Even after the CLI exits, the
daemon still holds write handles, blocking the test's read_to_end()
forever.
Fix: redirect stdout/stderr to temp files on Windows instead of pipes.
Also make SoloEnv::drop() cross-platform (taskkill on Windows).
* fix: add wait-timeout dev-dependency for Windows e2e harness
The Windows-specific timeout code in the e2e harness uses
wait_timeout::ChildExt directly. It was previously only available
transitively via assert_cmd, which isn't sufficient on Windows.
* fix: add Windows Chrome discovery and fix e2e test subcommand
- find_chrome(): Add Windows paths (Program Files, LOCALAPPDATA) and
use `where` instead of `which` for PATH lookup on Windows
- Fix session_status_works_over_tcp test: use 'status' subcommand
(not 'session-status' which doesn't exist)
* fix: use test env's ACTIONBOOK_HOME for daemon.port path assertion
The port file test was checking the global ~/.actionbook/daemon.port
but the test daemon writes to SoloEnv's isolated temp directory.
The JS wrapper's isVersionOnlyFlag only matched when --version was the
sole argument, so `actionbook browser --version` fell through to the
Rust binary and printed CARGO_PKG_VERSION instead of the npm package
version. Rename to isVersionFlag and use args.some() to intercept
--version/-V regardless of preceding subcommands.
Closes ACT-893
fix: use is_using_default_name flag for locale-independent profile name detection
Instead of comparing against the hardcoded English default "Your Chrome",
check Chrome's `is_using_default_name` boolean in Local State. This
correctly handles non-English Chrome builds (e.g. "Ihr Chrome" in German)
and Chromium-branded builds ("Your Chromium").
New commands added in the last 3 days were missing from help docs:
- Network: network requests, network request <id> (ACT-882)
- Batch: batch-new-tab, batch-snapshot, batch-click
- Setup: --target <agent> quick mode
Updated: handle_browser_help(), handle_help(), SKILL.md command
categories table, and command-reference.md with new sections.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Port ensure_actionbook_profile_display_name from actionbook-rs (0.x.x).
Before launching Chrome, write the profile name "actionbook" to both
Local State and Preferences so Chrome's profile picker shows the name.
Preserves any user-customized name. Only applies to the default
"actionbook" profile.
Closes ACT-894
Extension mode now uses register_extension_tab instead of
Target.attachToTarget for newly discovered tabs. Adds a focused
regression test verifying Extension.listTabs is called and no
Target.attachToTarget is sent.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Instead of returning stale registry data, extension mode now calls
Extension.listTabs for real-time tab enumeration, then reconciles
with the registry the same way local/cloud mode does.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extension mode connects via a Chrome extension bridge that only has
page-level debugging access. Target.getTargets is a browser-level CDP
method not available in this mode, causing CDP_CONNECTION_FAILED.
For extension mode, return tabs from the registry instead of querying
CDP. The registry already tracks all tabs registered during session
startup.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(cli): restore skills install step and --target quick mode in setup
- Add setup/skills.rs porting the `npx skills add actionbook/actionbook`
integration from the previous actionbook-rs CLI. Defines SetupTarget
enum, SkillsResult, and both wizard-flow and quick-mode install paths.
- Wire Step 5 "Install Skills" into the setup wizard (TOTAL_STEPS 4 -> 5),
running after Save. Non-interactive / JSON mode auto-installs with -y;
interactive mode prompts Install / Skip.
- Add quick mode: `actionbook setup --target <agent>` skips the full
wizard and only runs skills install for the specified target. Restores
the CI / one-shot bootstrap path. Standalone target is special-cased
to skip entirely (no agent integration needed).
- Change --target flag from Option<String> to Option<SetupTarget> (clap
ValueEnum, kebab-case values: claude, codex, cursor, windsurf,
antigravity, opencode, standalone, all). Unknown values rejected at
parse time.
- Propagate skills install failure as CliError so CI / non-interactive
callers see a non-zero exit.
Scope: only packages/cli/src/setup/ plus the cli.rs test block. No
changes to browser, daemon, config, error types, or Cargo.toml.
* feat(cli): show API key input and guide extension install to CWS / GitHub Releases
- API key prompt switches from `Password` to `Input` so typed characters
echo by default. Users reported the hidden-by-default behavior felt
broken when pasting. `allow_empty` + the validation loop are unchanged.
- Extension mode now guides the user to install the Chrome extension:
- Primary: Chrome Web Store (recommended, one-click).
- Fallback: manual install from GitHub Releases when CWS is
unavailable (region-blocked, offline, corporate policy). 5-step
Load-unpacked flow covers download -> unzip -> chrome://extensions
-> Developer mode -> Load unpacked.
- Interactive flow asks "Installed from CWS?" -> if no, shows the
GitHub Releases path and asks "Loaded the unpacked extension?"
-> if no, errors with both URLs listed for out-of-band install.
- Non-interactive (`--browser extension`) text output lists both
URLs; JSON payload gains `recommended_install_source`,
`web_store_url`, `fallback_install_source`, `github_releases_url`.
- The GitHub Releases URL uses a tag-search query
`?q=%22Chrome+Extension%22&expanded=true` so users land directly on
extension releases (the repo mixes `actionbook-cli-v*`,
`actionbook-extension-v*`, and `actionbook-dify-plugin-v*` in the
same feed — a plain /releases URL buries the extension zip).
Tests: 292 lib passed. New: `test_chrome_web_store_url_is_canonical`,
`test_github_releases_url_is_extension_filtered`,
`test_apply_extension_mode_records_web_store_hint_in_json`.
* fix(cli): address Codex P1 review feedback on setup skills step
Two P1 issues flagged by Codex automated review on PR #499:
1. setup/skills.rs: JSON mode deadlock risk when `npx skills add`
output exceeds the OS pipe buffer (~16KB on macOS). The code used
`Stdio::piped()` for child stdout/stderr but then called `.status()`
instead of `.output()`, which does not drain the pipes. A verbose
skills install (common on first run) would fill the buffer and block
the child on write, hanging setup indefinitely in CI. Switch to
`Stdio::null()` in JSON mode to discard subprocess output entirely —
we already emit our own JSON status event, so the subprocess output
has no consumer anyway. Also close stdin in JSON mode (`Stdio::null`)
so a stray read attempt can't hang either.
2. setup/mod.rs: quick mode (`setup --target <agent>`) silently exited
with status 0 when npx was not available. `install_skills_for_target`
returns `SkillsAction::Prompted` in that case, but `run_target_only`
only mapped `SkillsAction::Failed` to an error. CI bootstrap relying
on `--target` would silently pass broken runs. Extract a pure helper
`target_only_exit_status` that maps every non-`Installed` outcome to
`CliError::Internal` with a target-specific message; in the `Prompted`
branch the message points at the Node.js install docs and the exact
`npx` command to retry manually. The full-wizard Skills step is
unchanged — there `Prompted` is still a soft prompt because the user
can finish setup without skills.
Add 4 unit tests on `target_only_exit_status` covering all four
`SkillsAction` variants, including a regression guard for the P1 #2
silent-success case.
Tests: 296 lib passed (71 in setup::, 4 new).
* fix(cli): tighten setup target quick mode semantics
* fix(cli): install setup target all for all agents
* style(cli): format setup quick mode tests and JSON output
* chore(cli): add changeset for setup improvements
* docs: update OpenClaw integration guide
- Install section: npm only, removed InstallTabs snippet
- Step 2: removed smoke test (actionbook search)
- Step 3: removed "search -> get -> operate" workflow reference
- Next Steps: updated card description
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: revert steps 2-3 changes, keep only npm install update
Only the installation section should change per updated request.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: update InstallTabs snippet to npm-only install
Update the shared install-tabs.mdx snippet to only show npm install
method. Reverts inline change to openclaw.mdx — keeps the import.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Address Codex review findings on PR #492:
- P1: Text-mode network request listing now includes request_id so
users can drill into browser network request <id> without --json
- P2: record_request_will_be_sent updates in-place when the same
requestId appears again (redirect chains), preventing duplicate
entries and ensuring detail lookup returns the correct final hop
## Summary
- Add `awaitPromise: true` to the `Runtime.evaluate` CDP call in the `eval` command
- Without this flag, evaluating a Promise expression returns the Promise object itself, which serializes to `{}` (no enumerable properties)
- With `awaitPromise: true`, CDP waits for the Promise to resolve and returns the actual value — matching Playwright's behavior
- Add e2e regression test covering `Promise.resolve(42)` and delayed Promise resolution
Cherry-picked from #490 (closed — had stale click.rs/output.rs conflicts with main).
## Test plan
- [x] `eval_json_promise` e2e: `Promise.resolve(42)` returns 42
- [x] `eval_json_promise` e2e: delayed Promise via setTimeout resolves correctly
- [x] `eval_json_number` e2e: sync expressions still work
- [x] CI lint + unit tests green
## Summary
- Fix `browser wait navigation` timing out when navigation completes before
the wait command starts (fast-redirect scenario, ACT-873).
- Add CDP `Page.frameNavigated` event subscription alongside polling for
reliable in-watch navigation detection.
- Use registry `prev_url` as baseline to detect navigation that occurred
between commands.
- Add time-based URL stability (300ms) to distinguish final destinations
from intermediate redirect pages.
- Add 5 unit tests for NavigationDetector and 3 e2e tests covering
fast-redirect, delayed-redirect, and no-navigation timeout scenarios.
Closes ACT-873
* [packages/cli]feat: add batch click support and optimize wait_for_navigation
- Support multiple selectors in click command: click @e1 @e2 @e3
- Execute clicks sequentially with fail-fast on first error
- Batch response includes all results with url_changed/focus_changed per click
- Optimize wait_for_navigation: return immediately on URL change detection, avoid polling for DOM-only interactions
- Format text output for batch clicks (clicks count + target list)
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
* [packages/cli]perf: reduce Runtime.evaluate calls per click from 5 to 2
Merged pre-click (url+focus) and post-click (url+title+focus) state reads
each into a single JS evaluate, eliminating 3 serial main-thread round-trips
per click. Also removed wait_for_navigation's fixed 2s wait — now exits
immediately when URL hasn't changed (DOM-only interactions like card expand).
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
* [packages/cli]perf: return immediately on DOM-only clicks (no navigation)
After first evaluate, if URL is unchanged the click was a DOM-only
interaction — return immediately instead of polling for 2s. Only
continue polling when URL actually changed (real navigation).
Result: DOM-only clicks drop from ~2050ms to ~75ms.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
* [root]docs: update target branch to main in cli-devflow
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
* [root]docs: untrack .docs/cli-devflow.md
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
* [packages/cli]feat: support single-package local distribution via bundled binary
Add fallback in bin/actionbook.js to check for co-located actionbook-native
binary before looking up the platform-specific optional dependency package.
Enables local npm install -g from a single .tgz without separate platform packages.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
* [packages/cli]chore: bump version to 1.0.2-alpha.1
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
* [packages/cli]feat: add batch-click command for bulk DOM interactions
Adds `browser batch-click` as an agent-friendly command for clicking
multiple elements sequentially. Simplified interface (no --new-tab,
--button, --count) that delegates to click::execute internally.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
* [packages/cli]perf: batch-click bypasses JS main thread entirely
batch-click now uses resolve_center_no_scroll (DOM.getBoxModel only)
and skips pre/post state detection. All CDP calls stay on the DOM engine
thread, avoiding the JS main thread bottleneck.
Result: 5 clicks 413ms, 10 clicks 756ms (~75ms/click vs ~5000ms before).
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
* [packages/cli]fix: restore scrollIntoView in batch-click fast path
Keep scrollIntoView so off-screen elements are scrolled into view
before clicking. The perf gain comes from skipping pre/post state
detection (Runtime.evaluate), not from skipping scroll.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
* [packages/cli]feat: add batch-new-tab command for opening multiple tabs at once
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* [packages/cli]chore: bump version to 1.0.3-alpha.1
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(browser): add batch-snapshot command (#479)
Implements browser batch-snapshot (tasks #14, #15):
- Cmd struct with --session, --tabs (1+), and all shared snapshot flags
- execute() loops over tabs, reuses snapshot logic per tab, collects
results with partial success (SESSION_NOT_FOUND bubbles as Fatal,
all other per-tab errors become inline error entries)
- Registers in mod.rs, action.rs, cli.rs (BrowserCommands), router.rs
Co-authored-by: cli-roger-test <cli-roger-test@local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(browser): add batch snapshot e2e coverage
* feat(browser): add batch-snapshot command
Implements browser batch-snapshot (tasks #14, #15):
- Cmd struct with --session, --tabs (1+), and all shared snapshot flags
- execute() loops over tabs, reuses snapshot logic per tab, collects
results with partial success (SESSION_NOT_FOUND bubbles as Fatal,
all other per-tab errors become inline error entries)
- Registers in mod.rs, action.rs, cli.rs (BrowserCommands), router.rs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(browser): add batch snapshot e2e coverage
---------
Co-authored-by: cli-roger-test <cli-roger-test@local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(browser): add batch-new-tab to text output action list
batch-new-tab was missing from the is_action match in format_text,
so "ok browser batch-new-tab" was never emitted. Also add its
format_data_fields arm and partial failure handling.
---------
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
Co-authored-by: cli-roger-test <cli-roger-test@local>
* [packages/cli]feat: restore extension browser mode with bridge relay
Add a WebSocket bridge server inside the daemon that transparently
relays CDP messages between the Chrome extension and CdpSession.
Extension mode was removed in v1.0.0; this restores it with minimal
changes by reusing the existing cloud-mode CDP connection flow.
- daemon/bridge.rs: WS bridge on port 19222, hello handshake with
extension origin validation, bidirectional CDP message relay
- daemon/server.rs: spawn bridge as tokio task (non-fatal on bind fail)
- daemon/registry.rs: store SharedBridgeState for session start access
- browser/session/start.rs: execute_extension() reusing cloud CDP flow
- setup: unblock extension mode in wizard and --browser flag
- CdpSession: zero changes
- Extension side: zero changes
* [packages/cli]fix: use Extension API for tab discovery in extension mode
Extension bridge requires Extension.attachTab before any CDP command
can be relayed. Replaced Target.getTargets + Target.attachToTarget
with Extension.createTab / Extension.attachActiveTab flow. Added
CdpSession.register_extension_tab() to register tabs without CDP
flat-session handshake. Local and cloud modes are unaffected.
* [packages/cli]fix: close tabs and detach debugger in extension mode
Session close and close-tab now use Extension.closeTab / Extension.detachTab
in extension mode instead of Target.closeTarget which the bridge doesn't
support. Added Extension.closeTab to the extension that detaches the
debugger and removes the tab via chrome.tabs.remove. Local and cloud
modes are unaffected.
* [packages/cli]fix: revert extension JS change, use detachTab for close
Extension is loaded from CWS — local JS changes don't take effect.
Reverted Extension.closeTab addition to background.js. Session close
and close-tab now use Extension.detachTab to release the debugger in
extension mode. Tabs are left open since CLI doesn't own the browser.
* [packages/cli]fix: read default browser mode from config instead of hardcoding Local
When --mode is not specified, browser start now reads browser.mode from
config.toml (e.g. mode = "extension") instead of always falling back to
Local mode. This allows users with extension mode configured to connect
to their existing browser without explicitly passing --mode extension.
* [packages/cli]fix: build.rs track branch ref for accurate BUILD_VERSION in worktrees
build.rs only watched .git/HEAD which doesn't change on new commits in a
worktree (it stays as ref: refs/heads/branch-name). This caused stale
BUILD_VERSION, preventing daemon version-mismatch detection when the
npm-installed binary shared the same cached hash.
Now also watches the actual ref file and packed-refs via --git-common-dir
so BUILD_VERSION updates on every new commit.
Also adds a guard in start.rs Local mode section to reject non-Local
modes explicitly instead of silently launching a local Chrome.
* [packages/cli]fix: clippy/fmt fixes and reject concurrent CDP clients in bridge
- Fix rustfmt formatting (collapsed if chains, import order, line width)
- Fix clippy collapsible-if warnings
- Reject concurrent CDP clients in bridge relay to prevent response
channel hijacking between sessions
* [packages/cli]fix: suppress result_large_err for accept_hdr_async callback
The closure signature is dictated by tokio-tungstenite's API and cannot
use Box<Err>. Allow the lint on this specific closure.
* [root]chore: add changeset for CLI v1.2.0 (extension mode restore)
batch-new-tab was missing from the is_action match in format_text,
so "ok browser batch-new-tab" was never emitted. Also add its
format_data_fields arm and partial failure handling.
* test(browser): add render_yaml unit tests
* test(browser): extend render_yaml edge coverage
* feat(browser): save snapshot as Playwright-style YAML
Add render_yaml() to snapshot_transform.rs that converts a flat AXNode
list into a nested Playwright-style YAML DSL string. Format rules:
- Container nodes (children or URL): `- role "name" [ref=eN]:`
- Leaf with ref: `- role "name" [ref=eN]`
- Leaf with value (no ref): `- role "name": value`
- Leaf with name only (no ref): `- role: name`
- URL rendered as child: `- /url: <url>`
- [cursor=pointer] added when cursor_info has cursor:pointer hint
Switch build_output() to call render_yaml() instead of render_content(),
and change snapshot file extension from .txt to .yaml.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(browser): escape name and value in render_yaml leaf nodes
- Use escaped_name (not node.name) in the inline leaf branch
- Escape node.value with the same rules to prevent injection
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(browser): render value inline even when ref is present in render_yaml
Textbox/combobox nodes are interactive and typically have both a ref and
a value. Previously, the ref branch dropped the value entirely. Now the
value is appended inline: `- textbox "Label" [ref=eN]: value`.
Also moves escaped_value computation to the top of the loop so it can be
shared across both the ref and no-ref branches.
Adds unit test for the ref+value path.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(browser): update snapshot e2e for yaml artifacts
- assert data.path ends with .yaml
- snapshot_content() validates .yaml extension before reading
- rename assert_content_has_refs to assert_content_is_yaml_snapshot
- add assertions: no legacy `url=` format, at least one YAML container line
Co-Authored-By: cli-roger-test <cli-roger-test@local>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* style: fix rustfmt formatting in render_yaml tests
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: rustfmt formatting and update link URL test for YAML format
Apply rustfmt-required formatting:
- Expand format!() call with 3+ args to multi-line
- Expand assert!(cond, msg) to multi-line
- Expand assert_eq! with long string to multi-line
Update test_build_output_renders_link_urls_from_ax_properties:
- Old format: `url=https://...` inline
- New YAML format: `- link "Docs" [ref=e1]:` + child `- /url: https://...`
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* style: fix final rustfmt issue in link URL test assertion
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: replace map_or(false) with is_some_and per clippy
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: cli-roger-test <cli-roger-test@local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Keep scrollIntoView so off-screen elements are scrolled into view
before clicking. The perf gain comes from skipping pre/post state
detection (Runtime.evaluate), not from skipping scroll.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
docs: remove search and get commands from help and skills
Search and get commands are being removed from the CLI. This removes
all references from help output, SKILL.md, and command-reference.md.
The workflow now starts directly with browser automation via snapshot.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
batch-click now uses resolve_center_no_scroll (DOM.getBoxModel only)
and skips pre/post state detection. All CDP calls stay on the DOM engine
thread, avoiding the JS main thread bottleneck.
Result: 5 clicks 413ms, 10 clicks 756ms (~75ms/click vs ~5000ms before).
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Adds `browser batch-click` as an agent-friendly command for clicking
multiple elements sequentially. Simplified interface (no --new-tab,
--button, --count) that delegates to click::execute internally.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>