feat(rebase): fork base on upstream v0.24.0 native architecture
- Rebased onto upstream/main (v0.24.0, full Rust native) - Renamed package to agent-browser-stealth, version 0.24.0-fork.1 - Preserved fork-specific: abs alias, extensions/tab-group-cdp, .husky hooks - Removed upstream-only: docs/, packages/dashboard, examples/, benchmarks/ - Simplified pnpm workspace to root-only - Added [[bin]] section to keep binary name as "agent-browser" Track 1 of native-stealth migration. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
82eadcee41
commit
6addc80aa1
@@ -0,0 +1,8 @@
|
||||
if [ "${SKIP_CLAWHUB_SYNC:-0}" = "1" ]; then
|
||||
echo "Skipping ClawHub sync (SKIP_CLAWHUB_SYNC=1)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
pnpm run clawhub:sync || {
|
||||
echo "ClawHub sync failed. Push continues. Run 'pnpm run clawhub:sync' manually after fixing login/network."
|
||||
}
|
||||
-934
@@ -1,934 +0,0 @@
|
||||
# agent-browser
|
||||
|
||||
## 0.27.0
|
||||
|
||||
<!-- release:start -->
|
||||
### New Features
|
||||
|
||||
- **React introspection** - First-class React DevTools integration with new `react tree`, `react inspect <fiberId>`, `react renders start|stop`, and `react suspense` commands for full component-tree visibility, per-fiber props/hooks/state inspection, render profiling with mount/re-render counts and change details, and Suspense boundary classification with root-cause grouping and recommendations. React DevTools hook is vendored (MIT) and embedded in the binary with zero runtime dependencies (#1257)
|
||||
- **Web Vitals** - New `vitals [url]` command that reports Core Web Vitals (LCP, CLS, TTFB, FCP, INP) plus React hydration phases for any page (#1257)
|
||||
- **SPA navigation** - New `pushstate <url>` command for client-side SPA navigations without a full page load (#1257)
|
||||
- **Init scripts and feature flags** - New `--init-script <path>` flag (repeatable; env `AGENT_BROWSER_INIT_SCRIPTS`) to register scripts before first navigation, and `--enable <feature>` flag (repeatable; env `AGENT_BROWSER_ENABLE`) for built-in init scripts such as `react-devtools` (#1257)
|
||||
- **Network route resource type filter** - `network route` now accepts `--resource-type <csv>` to filter intercepted requests by CDP resource type (#1257)
|
||||
- **cURL cookie import** - `cookies set --curl <file>` auto-detects JSON, cURL, and Cookie-header formats for bulk cookie import (#1257)
|
||||
- **Dashboard proxy support** - The observability dashboard now works from proxied origins via a same-origin proxy, enabling deployment behind reverse proxies and path-based routing (#1111)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed **`doctor` command** generating duplicate check ids when called multiple times in the same process (#1330)
|
||||
|
||||
### Infrastructure
|
||||
|
||||
- Switched npm publishing to **trusted publishing** via GitHub Actions OIDC, removing the need for manually managed npm tokens (#1273)
|
||||
|
||||
### Contributors
|
||||
|
||||
- @ctate
|
||||
- @quuu
|
||||
- @shaper
|
||||
- @ThomasK33
|
||||
<!-- release:end -->
|
||||
|
||||
## 0.26.0
|
||||
|
||||
### New Features
|
||||
|
||||
- **`doctor` command** - Added `agent-browser doctor` for one-shot diagnosis of an install. Checks environment, Chrome, running daemons, config files, security, providers, and network connectivity; auto-cleans stale daemon sidecar files on every run; and performs a live headless launch test. Supports `--offline` to skip network probes, `--quick` to skip the launch test, `--fix` for opt-in repairs (install missing Chrome, close version-mismatched daemons, prune expired state files), and `--json` for structured output (#1254)
|
||||
- **Stable tab ids and labels** - Tabs now have stable string ids like `t1`, `t2`, `t3` that don't shift when other tabs close or popups appear. Tabs can be created with a memorable label via `tab new --label <name> [<url>]`, and labels are interchangeable with `t<N>` ids everywhere a tab ref is accepted (`tab <id|label>`, `tab close <id|label>`). Bare-integer input is rejected with a teaching error so agents can't mistake stable handles for positional indices (#892, #1249, #1250)
|
||||
- **`core` skill** - Renamed the built-in `agent-browser` skill to `core` and replaced its ~40-line discovery stub with a ~420-line usage guide covering the core snapshot-ref-act loop, reading, interacting, waiting, common workflows, troubleshooting, and global flags. `agent-browser skills get core` now returns content agents can use directly; `--full` adds references and templates. Added a `hidden:` frontmatter flag so the original `agent-browser` stub stays reachable for `npx skills add` discovery without polluting `skills list` (#1253)
|
||||
- **JSON Schema for config files** - Added `agent-browser.schema.json` describing every config option with types and descriptions, enabling IDE autocomplete and validation when referenced via `$schema` in `agent-browser.json` or `~/.agent-browser/config.json`. The schema is served from the docs site at `https://agent-browser.dev/schema.json` (#1242, #1248)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed **`--state` / `AGENT_BROWSER_STATE`** not actually loading saved browser state (cookies and localStorage) at launch. The flag had been fully plumbed through parsing, env propagation, and validation since the native Rust rewrite, but the load step was never wired up. Storage state now loads after launch across all four paths: explicit launch, auto-connect, provider, and local Chrome (#1241)
|
||||
|
||||
### Documentation
|
||||
|
||||
- `--help` output now shows the **skills** section first so agents discover `skills get core` (the canonical usage guide) before the core command list (#1251)
|
||||
|
||||
### Contributors
|
||||
|
||||
- @ctate
|
||||
- @DJRHails
|
||||
- @michael-farah
|
||||
- @tomdale
|
||||
|
||||
## 0.25.5
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed **`--auto-connect` CDP discovery** preferring HTTP endpoint discovery over the DevToolsActivePort websocket path, which could fail on some setups. The CLI now reads the websocket path from DevToolsActivePort first and only falls back to HTTP discovery (#1218)
|
||||
- Fixed **recording context viewport** not inheriting the active viewport dimensions, causing recordings to use default resolution instead of the configured viewport (#1208)
|
||||
- Fixed **`get box` and `get styles`** printing no data in text mode (#1231, #1233)
|
||||
- Fixed **active page changing** when closing or removing earlier tabs. The previously focused page is now preserved correctly (#1220)
|
||||
|
||||
### Contributors
|
||||
|
||||
- @ctate
|
||||
- @jin-2-kakaoent
|
||||
- @officialasishkumar
|
||||
|
||||
## 0.25.4
|
||||
|
||||
### New Features
|
||||
|
||||
- **`skills` command** - Added `agent-browser skills` command for discovering and installing agent skills, with built-in evaluation support for testing skills against live browser sessions (#1225, #1227)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed **custom viewport dimensions** not being used in streaming frame metadata and image resolution (#1033)
|
||||
- Fixed **`--ignore-https-errors`** not being re-applied to recording contexts, causing TLS errors during screen recordings (#1178)
|
||||
- Fixed **duplicate option numbering** in the auth skill documentation (#1161)
|
||||
|
||||
### Documentation
|
||||
|
||||
- The docs site header now **dynamically fetches** the GitHub star count (#1202)
|
||||
|
||||
### Contributors
|
||||
|
||||
- @ctate
|
||||
- @jin-2-kakaoent
|
||||
- @juniper929
|
||||
- @Marshall-Sun
|
||||
|
||||
## 0.25.3
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed **hidden radio/checkbox inputs missing from snapshot refs** when a `<label>` wraps a `display:none` `<input type="radio">` or `<input type="checkbox">`. Chrome excludes these inputs from the accessibility tree entirely, making it impossible for AI agents to identify radio buttons and checkboxes via refs. Hidden inputs inside elements are now detected during cursor-interactive scanning and their parent nodes are promoted to the correct role with proper name and checked state (#1085)
|
||||
|
||||
### Documentation
|
||||
|
||||
- Added **clickable heading anchors** to the docs site, making it easy to link directly to any section (#1175)
|
||||
|
||||
### Contributors
|
||||
|
||||
- @ctate
|
||||
- @jin-2-kakaoent
|
||||
- @hyunjinee
|
||||
|
||||
## 0.25.2
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed **Chrome being killed after ~10s idle on Linux** caused by `PR_SET_PDEATHSIG` tracking the blocking thread that spawned Chrome rather than the daemon process. When Tokio reaped the idle thread, the kernel sent SIGKILL to Chrome even though the daemon was still alive. Orphan cleanup is handled by the existing process-group kill in `ChromeProcess::kill()` (#1157, #1173)
|
||||
|
||||
### Contributors
|
||||
|
||||
- @ctate
|
||||
|
||||
## 0.25.1
|
||||
|
||||
### Improvements
|
||||
|
||||
- **Embedded dashboard** - The observability dashboard is now bundled directly into the CLI binary using `rust-embed`, eliminating the need for `dashboard install`. The dashboard is available immediately after installing agent-browser (#1169)
|
||||
|
||||
### Contributors
|
||||
|
||||
- @ctate
|
||||
|
||||
## 0.25.0
|
||||
|
||||
### New Features
|
||||
|
||||
- **AI chat command** - Added `chat` command for AI-powered browser automation. Supports single-shot mode (`chat "open google.com"`) and an interactive REPL. The AI agent can execute any agent-browser command via tool calls. Requires `AI_GATEWAY_API_KEY`. Configure the model with `--model` or `AI_GATEWAY_MODEL` (#1160, #1163)
|
||||
- **Dashboard AI chat** - The observability dashboard now includes a built-in AI chat interface for conversational browser control alongside live session views (#1160, #1163)
|
||||
- **`snapshot --urls`** - New `-u`/`--urls` flag to include href URLs for link elements in snapshot output, giving agents direct access to link targets without additional queries (#1160)
|
||||
- **Batch argument mode** - The `batch` command now accepts commands as inline arguments in addition to reading from stdin, simplifying single-invocation multi-command workflows (#1160)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed **`getByRole`** matching wrong elements (e.g. `<link>` stylesheet elements instead of `<a>` anchors) by rewriting the implementation to use the CDP accessibility tree with ref-based element resolution instead of CSS selectors (#1145)
|
||||
- Fixed **`upload` command** not supporting accessibility tree refs (`@eN`) for file upload element selection (#1156)
|
||||
- Fixed **`AGENT_BROWSER_DEFAULT_TIMEOUT`** not being applied to `wait` commands. The environment variable now propagates to all wait variants (`wait`, `wait --url`, `wait --text`, `wait --load`, `wait --fn`, `wait --download`) (#1153)
|
||||
- Fixed **dashboard download** error handling with improved retry logic for more reliable dashboard installation (#1154)
|
||||
|
||||
### Tests
|
||||
|
||||
- Fixed CI test failures on Windows and E2E (#1165)
|
||||
|
||||
### Contributors
|
||||
|
||||
- @ctate
|
||||
- @jin-2-kakaoent
|
||||
- @hyunjinee
|
||||
|
||||
## 0.24.1
|
||||
|
||||
### New Features
|
||||
|
||||
- **Chrome profile login state reuse** - `--profile <name>` now resolves Chrome profile names (e.g. `Default`, `Profile 1`) and copies the profile to a temp directory to reuse login state, cookies, and extensions without modifying the original. Added `profiles` command to list available Chrome profiles with `--json` support (#1131)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed **`--ignore-https-errors`** not passing `--ignore-certificate-errors` as a Chrome launch flag, causing TLS errors like `ERR_SSL_PROTOCOL_ERROR` to be rejected at the network layer before CDP could intervene (#1132)
|
||||
- Fixed **orphaned Chrome processes** on daemon exit by spawning Chrome in its own process group and killing the entire group on shutdown. On Linux, `PR_SET_PDEATHSIG` ensures Chrome is killed even if the daemon is OOM-killed (#1137)
|
||||
- Fixed **CDP attach hang on Chrome 144+** when connecting to real browser sessions. Targets paused waiting for the debugger after attach are now resumed with `Runtime.runIfWaitingForDebugger` (#1133)
|
||||
- Fixed **stale daemon after upgrade** silently reusing the old daemon process with broken CDP behavior. The daemon now writes a `.version` sidecar file and auto-restarts on version mismatch (#1134)
|
||||
- Fixed **stale daemon/socket recovery** where `close --all` failed to clean up zombie daemons and stale files. Unreachable daemons are now force-killed and orphaned socket/pid files are removed (#1136)
|
||||
- Fixed **idle timeout** not being respected because the sleep future was recreated on every select loop iteration, preventing the deadline from being reached (#1110)
|
||||
- Fixed **browser not relaunching** when launch options change (e.g. adding extensions to `config.json`) between consecutive launch commands (#996)
|
||||
- Fixed **`auto_launch()`** not honouring `AGENT_BROWSER_PROVIDER` for cloud providers, causing non-launch commands to fall back to local Chrome instead of connecting via the provider API (#1126)
|
||||
- Fixed **HAR capture missing API requests** under heavy traffic by increasing the CDP broadcast buffer from 256 to 4096 events, reducing the drain interval from 500ms to 100ms, and enabling network tracking in cross-origin iframes (#1135)
|
||||
|
||||
### Tests
|
||||
|
||||
- Fixed **`e2e_relaunch_on_options_change`** launching headed Chrome on CI where no display is available. The test now stays headless and only changes extensions to trigger the relaunch (#996)
|
||||
- Fixed **`e2e_auth_login`** flake by reducing the SPA render delay from 1200ms to 800ms, giving more headroom within the selector wait window on slower CI runners
|
||||
|
||||
### Contributors
|
||||
|
||||
- @ctate
|
||||
- @desenmeng
|
||||
- @jin-2-kakaoent
|
||||
- @snese
|
||||
|
||||
## 0.24.0
|
||||
|
||||
### New Features
|
||||
|
||||
- **AWS Bedrock AgentCore provider** - Added AWS Bedrock AgentCore as a cloud browser provider. Connect with `--provider agentcore` or `AGENT_BROWSER_PROVIDER=agentcore`. Uses lightweight manual SigV4 signing for authentication with support for the full AWS credential provider chain (environment variables, AWS CLI, SSO, IAM roles). Configure with `AGENTCORE_REGION`, `AGENTCORE_PROFILE_ID`, and `AGENTCORE_BROWSER_ID` environment variables. Returns session ID and Live View URL in the launch response (#397)
|
||||
|
||||
### Documentation
|
||||
|
||||
- Added AgentCore provider page to docs site, README options table, SKILL.md, and dashboard provider icons (#1120)
|
||||
|
||||
### Contributors
|
||||
|
||||
- @ctate
|
||||
- @pahud
|
||||
|
||||
## 0.23.4
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed **daemon hang on Linux** caused by a `waitpid(-1)` race condition in the SIGCHLD handler that stole exit statuses from Rust's `Child` handles, leaving the daemon in a broken state. Replaced the global signal handler with targeted crash detection via the existing drain interval (#1098)
|
||||
|
||||
## 0.23.3
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed **drag and drop** not working because `mouseMoved` events during the drag omitted the `buttons` bitmask, causing the browser to see `event.buttons === 0` and never fire `dragstart`/`dragover`/`drop` (#1087)
|
||||
|
||||
## 0.23.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 3c942e2: ### New Features
|
||||
|
||||
- **Dashboard session creation** - Sessions can now be created directly from the dashboard UI. A new session dialog provides a unified selector grid for local engines (Chrome, Lightpanda) and cloud providers (Browserbase, Browserless, Browser Use, Kernel) with async creation, loading state, and error display (#1092)
|
||||
- **Dashboard provider icons** - The session sidebar now shows the provider or engine icon for each session, making it easy to identify which backend a session is using (#1092)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed **Browser Use** provider using an intermediate API call instead of connecting directly via WSS (`wss://connect.browser-use.com`), which caused connection failures (#1092)
|
||||
- Fixed **Browserbase** provider not sending an explicit JSON body and `Content-Type` header, causing session creation to fail (#1092)
|
||||
- Fixed **provider navigation** hanging because `wait_for_lifecycle` waited for page load events that remote providers may not emit. Navigation with `--provider` now automatically sets `waitUntil=none` (#1092)
|
||||
- Fixed **remote CDP connections** timing out by increasing the CDP connect timeout from 10s to 25s for cloud providers (#1092)
|
||||
- Fixed **zombie daemon processes** not being cleaned up when a provider connection fails during session creation from the dashboard (#1092)
|
||||
|
||||
## 0.23.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fbcab37: ### New Features
|
||||
|
||||
- **Auto-dismissal for alert and beforeunload dialogs** - JavaScript `alert()` and `beforeunload` dialogs are now automatically accepted to prevent the agent from blocking indefinitely. `confirm` and `prompt` dialogs still require explicit `dialog accept/dismiss` commands. Disable with `--no-auto-dialog` flag or `AGENT_BROWSER_NO_AUTO_DIALOG` environment variable (#1075)
|
||||
- **Puppeteer browser cache fallback** - Chrome discovery now searches `~/.cache/puppeteer/chrome/` (or `PUPPETEER_CACHE_DIR`) for Chrome binaries, so users with an existing Puppeteer installation can use agent-browser without a separate install step (#1088)
|
||||
- **Console output improvements** - `console.log` of objects now shows the actual object preview (e.g. `{userId: "abc", count: 42}`) instead of `"Object"`. JSON output includes a raw `args` array for programmatic access (#1040)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed **same-document navigation** (e.g. SPA hash routing) hanging forever because `wait_for_lifecycle` waited for a `Page.loadEventFired` that never fires on same-document navigations (#1059)
|
||||
- Fixed **save_state** only capturing cookies and localStorage for the current origin, silently dropping cross-domain data (e.g. SSO/CAS auth cookies). Now uses `Network.getAllCookies` and collects localStorage from all visited origins (#1064)
|
||||
- Fixed **externally opened tabs** not appearing in `tab list` when using `--cdp` mode. Tabs opened by the user or another CDP client are now detected and tracked (#1042)
|
||||
- Fixed **dashboard server** not picking up installed files without a restart. `dashboard install` now takes effect immediately on a running server (#1066)
|
||||
- Fixed **Windows Chrome extraction** failing because zip path normalization used forward slashes while the extraction code expected backslashes (#1088)
|
||||
|
||||
## 0.23.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 0f0f300: ### New Features
|
||||
|
||||
- **Observability dashboard** - Added a local web UI (`dashboard`) that shows live browser viewports, command activity feeds, console output, network requests, storage, and extensions for all sessions. Manage it with `dashboard start`, `dashboard stop`, and `dashboard install`. The dashboard runs as a standalone background process and all sessions stream to it automatically (#1034)
|
||||
- **Runtime stream management** - Added `stream enable`, `stream disable`, and `stream status` commands to control WebSocket streaming at runtime. Streaming is now always enabled by default; `AGENT_BROWSER_STREAM_PORT` overrides the port instead of toggling the feature (#951)
|
||||
- **Close all sessions** - Added `close --all` flag to close every active browser session at once
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed **Lightpanda engine** compatibility (#1050)
|
||||
- Fixed **Windows daemon TCP bind** failing when Hyper-V reserves the port by falling back to an OS-assigned port and writing it to a `.port` file (#1041)
|
||||
- Fixed **Windows dashboard relay** using Unix socket instead of TCP (#1038)
|
||||
- Fixed **radio/checkbox elements** being dropped from compact snapshot tree because the `ref=` check required a leading `[` that those elements lack (#1008)
|
||||
|
||||
## 0.22.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- eb64ca4: ### Bug Fixes
|
||||
|
||||
- **Re-apply download behavior on recording context** - Fixed an issue where downloads were silently dropped in recording contexts because `Browser.setDownloadBehavior` set at launch only applied to the default context. The download behavior is now re-applied when a new recording context is created (#1019)
|
||||
- **Reap zombie Chrome process and fast-detect crash for auto-restart** - Added a non-blocking process-exit check before attempting CDP connection checks. This prevents a 3-second CDP timeout when Chrome has already crashed or exited, enabling faster detection and auto-restart of the browser (#1023)
|
||||
- **Route keyboard `type` through text input** - Fixed keyboard `type` subaction to correctly route through the text input handler, and added support for an `insertText` subaction using `Input.insertText` (#1014)
|
||||
- **Handle `--clear` flag in `console` command** - Fixed the `console` command to accept and process a `clear` parameter, allowing console event history to be cleared (#1015)
|
||||
|
||||
## 0.22.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- a098197: ### New Features
|
||||
|
||||
- **Dialog status command** - Added `dialog status` command to check whether a JavaScript dialog is currently open (#999)
|
||||
- **Dialog warning field** - Command responses now include a `warning` field when a JavaScript dialog is pending, indicating the dialog type and message (#999)
|
||||
|
||||
### Improvements
|
||||
|
||||
- **Standard proxy environment variables** - The proxy setting now automatically falls back to standard environment variables (`HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, and their lowercase variants), with `NO_PROXY`/`no_proxy` respected for bypass rules (#1000)
|
||||
- **Font packages for `--with-deps`** - Installing with `--with-deps` now includes CJK and emoji font packages on Linux (Debian, RPM, and yum-based distros) to prevent missing glyphs when rendering international content (#1002)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed `state show` always failing with "Missing 'path' parameter" due to a mismatched JSON field name (`filename` → `path`) (#994)
|
||||
- Fixed `console` command returning only `Done` due to a JSON field name mismatch in the response (#986)
|
||||
- Fixed browser-domain CDP events being dropped during downloads due to a `sessionId` mismatch (#998)
|
||||
- Fixed proxy authentication by handling credentials via the CDP `Fetch.authRequired` event rather than passing them inline (#1000)
|
||||
|
||||
## 0.22.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 3a3317b: ### Bug Fixes
|
||||
|
||||
- Fixed **modifier key chords** (e.g. `Control+a`, `Shift+Enter`, `Control+Shift+a`) not being handled correctly when using `press`. Modifier keys (`Alt`, `Control`/`Ctrl`, `Meta`/`Cmd`, `Shift`) are now parsed and forwarded as CDP modifier bitmasks rather than treated as part of the key name (#980)
|
||||
- Fixed **query parameters being dropped** from `--cdp` HTTP URLs (e.g. `http://host:9222?mode=Hello`). Query strings are now preserved and forwarded to the remote CDP endpoint (#982)
|
||||
|
||||
## 0.22.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- be30bc9: ### New Features
|
||||
|
||||
- **Cross-origin iframe support** - Added support for snapshots and interactions within cross-origin iframes via `Target.setAutoAttach` (#949)
|
||||
- **Network request detail and filtering** - Added `network request <requestId>` command to view full request/response detail, and new filtering options for `network requests` including `--type` (e.g. `xhr,fetch`), `--method` (e.g. `POST`), and `--status` (e.g. `2xx`, `400-499`) (#935)
|
||||
|
||||
### Improvements
|
||||
|
||||
- **Snapshot usability** - Reduced AI cognitive load by filtering semantic noise from snapshot output; cursor-interactive elements are now included by default, making the `-C` flag unnecessary (#968)
|
||||
- **Upgrade command** - Improved robustness of installation method detection in the upgrade command (#960)
|
||||
- **Target tracking** - Enhanced target tracking and page information handling for more reliable browser session management (#969)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed **viewport dimensions** being reported incorrectly in streaming status messages and screencast (#952)
|
||||
- Fixed **`find` command** flags such as `--exact` and `--name` leaking into fill values when used with fill actions (#955)
|
||||
- Fixed **state commands** incorrectly starting the daemon when no `session_name` is provided (#677, #964)
|
||||
- Fixed **auto-connect** triggering when the daemon is already running, preventing duplicate connections (#971)
|
||||
- Fixed **Enter key press** not working by adding a text field to `keyDown` events (#972)
|
||||
- Fixed **download command** to properly handle absolute paths and correctly click target elements (#970)
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- The `-C` / `--cursor` flag for `snapshot` is deprecated; cursor-interactive elements are now included by default and the flag has no additional effect (#968)
|
||||
|
||||
### Documentation
|
||||
|
||||
- Updated `README.md` with new `network requests` filtering options and `network request <requestId>` command usage
|
||||
- Removed references to the deprecated `-C` / `--cursor` snapshot flag from docs and command reference
|
||||
|
||||
## 0.21.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- aed466b: ### Bug Fixes
|
||||
|
||||
- **Auth login readiness** - `agent-browser auth login` now navigates with `load`, waits for usable login form selectors, and uses staged username detection (targeted email/username selectors first, then broad text-input fallback). This reduces SPA timing failures, avoids false matches on unrelated text fields, and prevents `networkidle` hangs on pages with continuous background requests.
|
||||
|
||||
## 0.21.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 6daad22: ### Bug Fixes
|
||||
|
||||
- **WebSocket keepalive for remote browsers** - Added WebSocket Ping frames and TCP `SO_KEEPALIVE` to prevent CDP connections from being silently dropped by intermediate proxies (reverse proxies, load balancers, service meshes) during idle periods (#936)
|
||||
- **XPath selector support** - Fixed element resolution to correctly handle the `xpath=` selector prefix (#908)
|
||||
|
||||
### Performance
|
||||
|
||||
- **Fast-path for identical snapshots** - Short-circuits the Myers diff algorithm when comparing a snapshot to itself, avoiding unnecessary computation in retry and loop workloads where repeated identical snapshots are common (#922)
|
||||
|
||||
### Documentation
|
||||
|
||||
- Migrated page metadata from MDX files to `layout.tsx` (#904)
|
||||
- Added search functionality and color improvements to docs (#927)
|
||||
- Fixed desktop browser list in the iOS comparison table (#926)
|
||||
- Created a new `providers/` section with dedicated provider pages (#928)
|
||||
|
||||
## 0.21.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 757626f: ### Bug Fixes
|
||||
|
||||
- **Deduplicate text content in snapshots** - Fixed an issue where duplicate text content appeared in page snapshots (#909)
|
||||
- **Native mouse drag state** - Fixed incorrect raw native mouse drag state not being properly tracked across `down`, `move`, and `up` events (#872)
|
||||
- **Chrome headless launch failures** - Fixed browser launch failures caused by the `--enable-unsafe-swiftshader` flag in Chrome headless mode (#915)
|
||||
- **Origin-scoped `--headers` persistence** - Restored correct persistence of origin-scoped headers set via `--headers` across navigation commands (#894)
|
||||
- **Relative URLs in WebSocket domain filter** - Fixed handling of relative URLs in the WebSocket domain filter script (#624)
|
||||
|
||||
## 0.21.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 1e7619d: ### New Features
|
||||
|
||||
- **HAR 1.2 network capture** - Added commands to capture and export network traffic in HAR 1.2 format, including accurate request/response timing, headers, body sizes, and resource types sourced from Chrome DevTools Protocol events (#864)
|
||||
- **Built-in `upgrade` command** - Added `agent-browser upgrade` to self-update the CLI; automatically detects your installation method (npm, Homebrew, or Cargo) and runs the appropriate update command (#898)
|
||||
|
||||
### Documentation
|
||||
|
||||
- Added `upgrade` command to the README command reference and installation guide
|
||||
- Added a dedicated **Updating** section to the README with usage instructions for `agent-browser upgrade`
|
||||
|
||||
## 0.21.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- c6de80b: ### New Features
|
||||
|
||||
- **`batch` command** -- Execute multiple commands from stdin in a single invocation. Accepts a JSON array of string arrays and returns results sequentially. Supports `--bail` to stop on first error and `--json` for structured output (#865)
|
||||
- **iframe support** -- CLI interactions and snapshots now traverse into iframe content, enabling automation of cross-frame pages (#869)
|
||||
- **`network har start/stop` command** -- Capture and export network traffic in HAR 1.2 format (#874)
|
||||
- **WebSocket fallback for CDP discovery** -- When HTTP-based CDP endpoint discovery fails, the CLI now falls back to a WebSocket connection automatically (#873)
|
||||
|
||||
### Improvements
|
||||
|
||||
- **`--full`/`-f` refactored to command-level flag** -- Moved from a global flag to a per-command flag for clearer scoping (#877)
|
||||
- **Enhanced Chrome launch** -- Added `--user-data-dir` support and configurable launch timeout for more reliable browser startup (#852)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed `/json/list` fallback when `/json/version` endpoint is unavailable, improving compatibility with non-standard CDP implementations (#861)
|
||||
- Fixed daemon liveness detection for PID namespace isolation (e.g. `unshare`). Uses socket connectivity as the sole liveness check instead of `kill(pid, 0)`, which fails when the caller cannot see the daemon's PID (#879)
|
||||
- Fixed Ubuntu dependency install accidentally removing system packages (#884)
|
||||
|
||||
## 0.20.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- c0d4cf6: ### New Features
|
||||
|
||||
- **Idle timeout for daemon auto-shutdown** - Added `--idle-timeout` CLI flag (and `AGENT_BROWSER_IDLE_TIMEOUT_MS` environment variable) to automatically shut down the daemon after a period of inactivity. Accepts human-friendly formats such as `10s`, `3m`, `1h`, or raw milliseconds (#856)
|
||||
- **Cursor-interactive elements in snapshot tree** - Cursor-interactive elements are now embedded directly into the snapshot tree for richer context (#855)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed **remote host support** in CDP discovery, enabling connection to browsers running on non-local hosts (#854)
|
||||
- Fixed **CDP flag propagation** to the daemon process, ensuring reliable CDP reconnection across sessions (#857)
|
||||
- Fixed **Windows auto-connect profiling** to correctly handle browser connection on Windows (#835, #840)
|
||||
- Fixed **Windows transient error detection** by recognising Windows-specific socket error codes (`os error 10061` connection refused, `os error 10054` connection reset) during daemon reconnection attempts
|
||||
|
||||
## 0.20.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- eda956b: ### Bug Fixes
|
||||
|
||||
- **Network idle detection for cached pages** - Fixed an issue where `poll_network_idle` could return immediately when no network events were observed (e.g. pages served from cache). The idle timer is now only satisfied after a consistent **500 ms idle period** has elapsed, preventing false-positive idle detection. The core polling logic has also been extracted into a standalone `poll_network_idle` function to improve testability (#847)
|
||||
|
||||
## 0.20.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 5fa2396: ### Bug Fixes
|
||||
|
||||
- Fixed **`snapshot -C`** and **`screenshot --annotate`** hanging when connected over WSS (WebSocket Secure) due to sequential CDP round-trips per interactive element (#842)
|
||||
|
||||
### Performance
|
||||
|
||||
- **`snapshot -C` (cursor-interactive mode)** now batches CDP calls instead of issuing N×2 sequential round-trips per cursor-interactive element, preventing timeouts on high-latency WSS connections (#842)
|
||||
- **`screenshot --annotate`** now batches element queries, reducing completion time from potentially 20–40s (e.g. 50+ buttons over WSS) to within expected bounds (#842)
|
||||
|
||||
## 0.20.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 4b5fc78: ### Bug Fixes
|
||||
|
||||
- **Material Design checkbox/radio parity** - Restored Playwright-parity behavior for `check`/`uncheck` actions on Material Design controls. These components hide the native `<input>` off-screen and use overlay elements that intercept coordinate-based clicks; the actions now detect this pattern and fall back to a JS `.click()` to correctly toggle state. Also improves `ischecked` to handle nested hidden inputs and ARIA-only checkboxes (#837)
|
||||
- **Punctuation handling in `type` command** - Fixed incorrect virtual key (VK) codes being used for punctuation characters (e.g. `.`, `@`) in the `type` action, which previously caused those characters to be dropped or mistyped (#836)
|
||||
|
||||
## 0.20.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- a3d9662: ### Bug Fixes
|
||||
|
||||
- **Restored WebSocket streaming** - Fixed broken WebSocket streaming in the native daemon by keeping the **StreamServer** instance alive so the broadcast channel remains open, and ensuring CDP session IDs and connection status are correctly propagated to stream clients (#826)
|
||||
- **Filtered internal Chrome targets** - Fixed auto-connect discovery incorrectly attempting to attach to Chrome-internal pages (e.g. `chrome://`, `chrome-extension://`, `devtools://` URLs), which could cause unexpected connection failures (#827)
|
||||
|
||||
## 0.20.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 51d9ab4: ### Bug Fixes
|
||||
|
||||
- **Appium v3 iOS capabilities** - Added `appium:` vendor prefix to iOS capabilities (e.g., `appium:automationName`, `appium:deviceName`, `appium:platformVersion`) to comply with the Appium v3 WebDriver protocol requirements (#810)
|
||||
- **Snapshot `--selector` scoping** - Fixed `snapshot --selector` so that the output is properly scoped to the matched element's subtree rather than returning the full accessibility tree. The selector now resolves the target DOM node's backend IDs and filters the accessibility tree to only include nodes within that subtree (#825)
|
||||
|
||||
## 0.20.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- daf7263: ### Bug Fixes
|
||||
|
||||
- Fixed **video duration** being reported incorrectly when using real-time ffmpeg encoding for screen recording (#812)
|
||||
- Removed obsolete **`BrowserManager` TypeScript API** references that no longer reflect the current CLI-based usage model (#821)
|
||||
|
||||
### Documentation
|
||||
|
||||
- Updated README to replace outdated **`BrowserManager` programmatic API** examples with the current CLI-based approach using `execSync` and `agent-browser` commands (#821)
|
||||
- Removed the **Programmatic API** section covering `BrowserManager` screencast and input injection methods, which are no longer part of the public API (#821)
|
||||
|
||||
## 0.20.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 25a1526: ### New Features
|
||||
|
||||
- **Brave Browser support** - Added auto-discovery of Brave Browser for CDP connections on macOS, Linux, and Windows. The agent will now automatically detect and connect to Brave alongside Chrome, Chromium, and Canary installations (#817)
|
||||
|
||||
### Improvements
|
||||
|
||||
- **Postinstall message** - The post-install message now detects existing Chrome installations on the system. If a compatible browser is found, it confirms the path and notes it will be used automatically instead of prompting an install. If no browser is detected, the warning is clearer and mentions that installation can be skipped when using `--cdp`, `--provider`, `--engine`, or `--executable-path` (#815)
|
||||
|
||||
## 0.20.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fa91c22: ### Bug Fixes
|
||||
|
||||
- **Stale accessibility tree reference fallback** - Fixed an issue where interacting with an element whose **`backend_node_id`** had become stale (e.g. after the DOM was replaced) would fail with a `Could not compute box model` CDP error. Element resolution now re-queries the accessibility tree using role/name lookup to obtain a fresh node ID before retrying the operation (#806)
|
||||
|
||||
## 0.20.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fc091d2: ### Bug Fixes
|
||||
|
||||
- **Daemon panic on broken stderr pipe** - Replaced all `eprintln!` calls with `writeln!(std::io::stderr(), ...)` wrapped in `let _ =` to silently discard write errors, preventing the daemon from panicking when the parent process drops the stderr pipe during Chrome launch (#802)
|
||||
|
||||
## 0.20.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- e2ebde2: ### Bug Fixes
|
||||
|
||||
- **Broadcast channel lag handling** - Fixed an issue where **broadcast channel lag** errors were incorrectly treated as stream closure, causing premature termination of event listeners in reload, response body, download, and navigation wait operations. Lagged messages are now skipped and the loop continues instead of breaking (#797)
|
||||
|
||||
### Improvements
|
||||
|
||||
- Removed unused **pnpm setup** steps from the `global-install` CI job, simplifying the workflow configuration (#798)
|
||||
|
||||
## 0.20.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- e365909: ### Bug Fixes
|
||||
|
||||
- **Chrome launch retry** - Chrome will now retry launching up to 3 times with a 500ms delay between attempts, improving resilience against transient startup failures (#791)
|
||||
- **Remote CDP snapshot hang** - Resolved an issue where snapshots would hang indefinitely over remote CDP (WSS) connections by removing WebSocket message and frame size limits to accommodate large responses (e.g. `Accessibility.getFullAXTree`), accepting binary frames from remote proxies such as Browserless, and immediately clearing pending commands when the connection closes rather than waiting for the 30-second timeout (#792)
|
||||
|
||||
## 0.20.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 944fa01: ### New Features
|
||||
|
||||
- **Linux musl (Alpine) builds** - Added pre-built binaries for **linux-musl** targeting both **x64** and **arm64** architectures, enabling native support for Alpine Linux and other musl-based distributions without requiring glibc (#784)
|
||||
|
||||
### Improvements
|
||||
|
||||
- **Consecutive `--auto-connect` commands** - Added support for issuing multiple consecutive `--auto-connect` commands without requiring a full browser relaunch; external connections are now correctly identified and reused (#786)
|
||||
- **External browser disconnect behavior** - When using `--auto-connect` or `--cdp`, closing the agent session now disconnects cleanly without shutting down the user's browser process
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **Restored `refs` dict in `--json` snapshot output** - The `refs` map containing role and name metadata for referenced elements is now correctly included in JSON snapshot responses (#787)
|
||||
- Fixed e2e test assertions for `diff_snapshot` and `domain_filter` to correctly reflect expected behavior (#783)
|
||||
- Fixed Chrome temp-dir cleanup test failing on Windows (#766)
|
||||
|
||||
## 0.20.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- bd05917: ### Bug Fixes
|
||||
|
||||
- Fixed **AX tree deserialization** to accept integer `nodeId` and `childIds` values for compatibility with Lightpanda, which sends numeric IDs where Chrome sends strings (#775)
|
||||
- Fixed **misleading SIGPIPE comment** to accurately describe the default Rust SIGPIPE behavior and why it is reset to `SIG_DFL` (#776)
|
||||
- Fixed **WebM recording output** to use the VP9 codec (`libvpx-vp9`) instead of H.264, producing valid WebM files; also adds a padding filter to ensure even frame dimensions (#779)
|
||||
|
||||
## 0.20.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 235fa88: ### Full Native Rust
|
||||
|
||||
- **100% native Rust** -- Removed the entire Node.js/Playwright daemon. The Rust native daemon is now the only implementation. No Node.js runtime or Playwright dependency required. (#754)
|
||||
- **99x smaller install** -- Install size reduced from 710 MB to 7 MB by eliminating the Node.js dependency tree.
|
||||
- **18x less memory** -- Daemon memory usage reduced from 143 MB to 8 MB.
|
||||
- **1.6x faster cold start** -- Cold start time reduced from 1002ms to 617ms.
|
||||
- **Benchmarks** -- Added benchmark suite comparing native vs Node.js daemon performance.
|
||||
- **Chromium installer hardened** -- Fixed zip path traversal vulnerability in Chrome for Testing installer.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed `--headed false` flag not being respected in CLI (#757)
|
||||
- Fixed "not found" error pattern in `to_ai_friendly_error` incorrectly catching non-element errors (#759)
|
||||
- Fixed storage local key lookup parsing and text output (#761)
|
||||
- Fixed Lightpanda engine launch with release binaries (#760)
|
||||
- Hardened Lightpanda startup timeouts (#762)
|
||||
|
||||
## 0.19.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 56bb92b: ### New Features
|
||||
|
||||
- **Browserless.io provider** -- Added browserless.io as a browser provider, supported in both Node.js and native daemon paths. Connect to remote Browserless instances with `--provider browserless` or `AGENT_BROWSER_PROVIDER=browserless`. Configurable via `BROWSERLESS_API_KEY`, `BROWSERLESS_API_URL`, and `BROWSERLESS_BROWSER_TYPE` environment variables. (#502, #746)
|
||||
- **`clipboard` command** -- Read from and write to the browser clipboard. Supports `read`, `write <text>`, `copy` (simulates Ctrl+C), and `paste` (simulates Ctrl+V) operations. (#749)
|
||||
- **Screenshot output configuration** -- New global flags `--screenshot-dir`, `--screenshot-quality`, `--screenshot-format` and corresponding `AGENT_BROWSER_SCREENSHOT_DIR`, `AGENT_BROWSER_SCREENSHOT_QUALITY`, `AGENT_BROWSER_SCREENSHOT_FORMAT` environment variables for persistent screenshot settings. (#749)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed `wait --text` not working in native daemon path (#749)
|
||||
- Fixed `BrowserManager.navigate()` and package entry point (#748)
|
||||
- Fixed extensions not being loaded from `config.json` (#750)
|
||||
- Fixed scroll on page load (#747)
|
||||
- Fixed HTML retrieval by using `browser.getLocator()` for selector operations (#745)
|
||||
|
||||
## 0.18.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 942b8cd: ### New Features
|
||||
|
||||
- **`inspect` command** - Opens Chrome DevTools for the active page by launching a local proxy server that forwards the DevTools frontend to the browser's CDP WebSocket. Commands continue to work while DevTools is open. Implemented in both Node.js and native paths. (#736)
|
||||
- **`get cdp-url` subcommand** - Retrieve the Chrome DevTools Protocol WebSocket URL for the active page, useful for external debugging tools. (#736)
|
||||
- **Native screenshot annotate** - The `--annotate` flag for screenshots now works in the native Rust daemon, bringing parity with the Node.js path. (#706)
|
||||
|
||||
### Improvements
|
||||
|
||||
- **KERNEL_API_KEY now optional** - External credential injection no longer requires `KERNEL_API_KEY` to be set, making it easier to use Kernel with pre-configured environments. (#687)
|
||||
- **Browserbase simplified** - Removed the `BROWSERBASE_PROJECT_ID` requirement, reducing setup friction for Browserbase users. (#625)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed Browserbase API using incorrect endpoint to release sessions (#707)
|
||||
- Fixed CDP connect paths using hardcoded 10s timeout instead of `getDefaultTimeout()` (#704)
|
||||
- Fixed lone Unicode surrogates causing errors by sanitizing with `toWellFormed()` (#720)
|
||||
- Fixed CDP connection failure on IPv6-first systems (#717)
|
||||
- Fixed recordings not inheriting the current viewport settings (#718)
|
||||
|
||||
## 0.17.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 94cd888: Added support for device scale factor (retina display) in the viewport command via an optional scale parameter. Also added webview target type support for better Electron application compatibility, and the pages list now includes target type information.
|
||||
|
||||
## 0.17.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 94521e7: ### New Features
|
||||
|
||||
- **Lightpanda browser engine support** - Added `--engine <name>` flag to select the browser engine (`chrome` by default, or `lightpanda`), implying `--native` mode. Configurable via `AGENT_BROWSER_ENGINE` environment variable (#646)
|
||||
- **Dialog dismiss command** - Added support for `dismiss` subcommand in dialog command parsing (#605)
|
||||
|
||||
### Improvements
|
||||
|
||||
- **Daemon startup error reporting** - Daemon startup errors are now surfaced directly instead of showing an opaque timeout message (#614)
|
||||
- **CDP port discovery** - Replaced broken hand-rolled HTTP client with `reqwest` for more reliable CDP port discovery (#619)
|
||||
- **Chrome extensions** - Extensions now load correctly by forcing headed mode when extensions are present (#652)
|
||||
- **Google Translate bar suppression** - Suppressed the Google Translate bar in native headless mode to avoid interference (#649)
|
||||
- **Auth cookie persistence** - Auth cookies are now persisted on browser close in native mode (#650)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed native auth login failing due to incompatible encryption format (#648)
|
||||
|
||||
### Documentation
|
||||
|
||||
- Improved snapshot usage guidance and added reproducibility check (#630)
|
||||
- Added `--engine` flag to the README options table
|
||||
|
||||
### Performance
|
||||
|
||||
- Added benchmarks to the CLI codebase (#637)
|
||||
|
||||
## 0.16.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 7d2c895: Fixed an issue where the --native flag was being passed to child processes even when not explicitly specified on the command line. The flag is now only forwarded when the user explicitly provides it, consistent with how other CLI flags like --allow-file-access and --download-path are handled.
|
||||
|
||||
## 0.16.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 01ac557: Added AGENT_BROWSER_HEADED environment variable support for running the browser in headed mode, and improved temporary profile cleanup when launching Chrome directly. Also includes documentation clarification that browser extensions work in both headed and headless modes.
|
||||
|
||||
## 0.16.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- c4180c8: Improved Chrome launch reliability by automatically detecting containerized environments (Docker, Podman, Kubernetes) and enabling --no-sandbox when needed. Added support for discovering Playwright-installed Chromium browsers and enhanced error messages with helpful diagnostics when Chrome fails to launch.
|
||||
|
||||
## 0.16.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 05018b3: Added experimental native Rust daemon (`--native` flag, `AGENT_BROWSER_NATIVE=1` env, or `"native": true` in config). The native daemon communicates with Chrome directly via CDP, eliminating Node.js and Playwright dependencies. Supports 150+ commands with full parity to the default Node.js daemon. Includes WebDriver backend for Safari/iOS, CDP protocol codegen, request tracking, frame context management, and comprehensive e2e and parity tests.
|
||||
|
||||
## 0.15.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 62241b5: Fixed Windows compatibility issues including proper handling of extended-length path prefixes from canonicalize(), prevention of MSYS/Git Bash path translation that could mangle arguments, and improved daemon startup reliability. Also added ARM64 Windows support in postinstall shims and expanded CI testing with a full daemon lifecycle test on Windows.
|
||||
|
||||
## 0.15.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 6aea316: Documentation site improvements and internal tooling updates including enhanced code blocks, mobile navigation, and docs chat components. CLI connection and output handling refinements. Skill creator reference documentation and scripts have been reorganized.
|
||||
|
||||
## 0.15.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 7bd8ce9: Added support for chrome:// and chrome-extension:// URLs in navigation and recording commands. These special browser URLs are now preserved as-is instead of having https:// incorrectly prepended.
|
||||
|
||||
## 0.15.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 2e38882: - Added security hardening: authentication vault, content boundary markers, domain allowlist, action policy, action confirmation, and output length limits.
|
||||
- Added `--download-path` flag (and `AGENT_BROWSER_DOWNLOAD_PATH` env / `downloadPath` config key) to set a default download directory.
|
||||
- Added `--selector` flag to `scroll` command for scrolling within specific container elements.
|
||||
|
||||
## 0.14.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- b7665e5: - Added `keyboard` command for raw keyboard input -- type with real keystrokes, insert text, and press shortcuts at the currently focused element without needing a selector.
|
||||
- Added `--color-scheme` flag and `AGENT_BROWSER_COLOR_SCHEME` env var for persistent dark/light mode preference across browser sessions.
|
||||
- Fixed IPC EAGAIN errors (os error 35/11) by adding backpressure-aware socket writes, command serialization, and lowering the default Playwright timeout to 25s (configurable via `AGENT_BROWSER_DEFAULT_TIMEOUT`).
|
||||
- Fixed remote debugging (CDP) reconnection.
|
||||
- Fixed state load failing when no browser is running.
|
||||
- Fixed `--annotate` flag warning appearing when not explicitly passed via CLI.
|
||||
|
||||
## 0.13.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- ebd8717: Added new diff commands for comparing snapshots, screenshots, and URLs between page states. You can now run visual pixel diffs against baseline images, compare accessibility tree snapshots with customizable depth and selectors, and diff two URLs side-by-side with optional screenshot comparison.
|
||||
|
||||
## 0.12.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 69ffad0: Add annotated screenshots with the new --annotate flag, which overlays numbered labels on interactive elements and prints a legend mapping each label to its element ref. This enables multimodal AI models to reason about visual layout while using the same @eN refs for subsequent interactions. The flag can also be set via the AGENT_BROWSER_ANNOTATE environment variable.
|
||||
|
||||
## 0.11.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- c6fc7df: Added documentation for command chaining with && across README, CLI help output, docs, and skill files, explaining how to efficiently chain multiple agent-browser commands in a single shell invocation since the browser persists via a background daemon.
|
||||
|
||||
## 0.11.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 5dc40b4: Added configuration file support with automatic loading from user and project directories, new profiler commands for Chrome DevTools profiling, computed styles getter, browser extension loading, storage state management, and iOS device emulation. Expanded click command with new-tab option, improved find command with additional actions and filtering options, and enhanced CDP connection to accept WebSocket URLs. Documentation has been significantly expanded with new sections for configuration, profiling, and proxy support.
|
||||
|
||||
## 0.10.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 1112a16: Added session persistence with automatic save/restore of cookies and localStorage across browser restarts using --session-name flag, with optional AES-256-GCM encryption for saved state data. New state management commands allow listing, showing, renaming, clearing, and cleaning up old session files. Also added --new-tab option for click commands to open links in new tabs.
|
||||
|
||||
## 0.9.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 323b6cd: Fix all Clippy lint warnings in the Rust CLI: remove redundant import, use `.first()` instead of `.get(0)`, use `.copied()` instead of `.map(|s| *s)`, use `.contains()` instead of `.iter().any()`, use `then_some` instead of lazy `then`, and simplify redundant match guards.
|
||||
|
||||
## 0.9.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- d03e238: Added support for custom executable path in CLI browser launch options. Documentation site received UI improvements including a new chat component with sheet-based interface and updated dependencies.
|
||||
|
||||
## 0.9.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 76d23db: Documentation site migrated to MDX for improved content authoring, added AI-powered docs chat feature, and updated README with Homebrew installation instructions for macOS users.
|
||||
|
||||
## 0.9.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ae34945: Added --allow-file-access flag to enable opening and interacting with local file:// URLs (PDFs, HTML files) by passing Chromium flags that allow JavaScript access to local files. Added -C/--cursor flag for snapshots to include cursor-interactive elements like divs with onclick handlers or cursor:pointer styles, which is useful for modern web apps using custom clickable elements.
|
||||
|
||||
## 0.9.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 9d021bd: Add iOS Simulator and real device support for mobile Safari testing via Appium. New CLI commands include `device list` to show available simulators, `tap` and `swipe` for touch interactions, and the `--device` flag to specify which iOS device to use. Configure with `-p ios` provider flag or `AGENT_BROWSER_PROVIDER=ios` environment variable.
|
||||
|
||||
## 0.8.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 17dba8f: Add --stdin flag for eval command to read JavaScript from stdin, enabling heredoc usage for multiline scripts
|
||||
- daeede4: Add --stdin flag for the eval command to read JavaScript from stdin, enabling heredoc usage for multiline scripts. Also fix binary permission issues on macOS/Linux when postinstall scripts don't run (e.g., with bun).
|
||||
|
||||
## 0.8.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 0dc36f2: Add --stdin flag for eval command to read JavaScript from stdin, enabling heredoc usage for multiline scripts
|
||||
|
||||
## 0.8.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 2771588: Added base64 encoding support for the eval command with -b/--base64 flag to avoid shell escaping issues when executing JavaScript. Updated documentation with AI agent setup instructions and reorganized the docs structure by consolidating agent mode content into the installation page.
|
||||
|
||||
## 0.8.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- d24f753: Fixed browser launch options not being passed correctly when using persistent profiles, ensuring args, userAgent, proxy, and ignoreHTTPSErrors settings now work properly. Added pre-flight checks for socket path length limits and directory write permissions to provide clearer error messages when daemon startup fails. Improved error handling to properly exit with failure status when browser launch fails.
|
||||
|
||||
## 0.8.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- d75350a: Improved daemon connection reliability by adding automatic retry logic for transient errors like connection resets, broken pipes, and temporary resource unavailability. The CLI now cleans up stale socket and PID files before starting a new daemon, and includes better detection of daemon responsiveness to handle race conditions during shutdown.
|
||||
|
||||
## 0.8.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- cb2f8c3: Fixed version synchronization to automatically update Cargo.lock alongside Cargo.toml during releases, and made the CLI binary executable. This ensures the Rust CLI version stays in sync with the npm package version.
|
||||
|
||||
## 0.8.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 759302e: Fixed "Daemon not found" error when running through AI agents (e.g., Claude Code) by resolving symlinks in the executable path. Previously, npm global bin symlinks weren't being resolved correctly, causing intermittent daemon discovery failures.
|
||||
|
||||
## 0.8.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 4116a8a: Replaced shell-based CLI wrappers with a cross-platform Node.js wrapper to enable npx support on Windows. Added postinstall logic to patch npm's bin entry on global installs, allowing the native binary to be invoked directly with zero overhead. Added CI tests to verify global installation works correctly across all platforms.
|
||||
|
||||
## 0.8.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 7e6336f: Fixed the Windows CMD wrapper to use the native binary directly instead of routing through Node.js, improving startup performance and reliability. Added retry logic to the CI install command to handle transient failures during browser installation.
|
||||
|
||||
## 0.8.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 8eec634: Improved release workflow to validate binary file sizes and ensure binaries are executable after npm install. Updated documentation site with a new mobile navigation system and added v0.8.0 changelog entries. Reformatted CHANGELOG.md for better readability.
|
||||
|
||||
## v0.8.0
|
||||
|
||||
### New Features
|
||||
|
||||
- **Kernel cloud browser provider** - Connect to Kernel (https://kernel.sh) for remote browser infrastructure via `-p kernel` flag or `AGENT_BROWSER_PROVIDER=kernel`. Supports stealth mode, persistent profiles, and automatic profile find-or-create.
|
||||
- **Ignore HTTPS certificate errors** - New `--ignore-https-errors` flag for working with self-signed certificates and development environments
|
||||
- **Enhanced cookie management** - Extended `cookies set` command with `--url`, `--domain`, `--path`, `--httpOnly`, `--secure`, `--sameSite`, and `--expires` flags for setting cookies before page load
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed tab list command not recognizing new pages opened via clicks or `target="_blank"` links (#275)
|
||||
- Fixed `check` command hanging indefinitely (#272)
|
||||
- Fixed `set device` not applying deviceScaleFactor - HiDPI screenshots now work correctly (#270)
|
||||
- Fixed state load and profile persistence not working in v0.7.6 (#268)
|
||||
- Screenshots now save to temp directory when no path is provided (#247)
|
||||
|
||||
### Security
|
||||
|
||||
- Daemon and stream server now reject cross-origin connections (#274)
|
||||
|
||||
## 0.7.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- a4d0c26: Allow null values for the screenshot selector field. Previously, passing a null selector would fail validation, but now it is properly handled as an optional value.
|
||||
|
||||
## 0.7.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 8c2a6ec: Fix GitHub release workflow to handle existing releases. If a release already exists, binaries are uploaded to it instead of failing.
|
||||
|
||||
## 0.7.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 957b5e5: Fix binary permissions on install. npm doesn't preserve execute bits, so postinstall now ensures the native binary is executable.
|
||||
|
||||
## 0.7.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 161d8f5: Fix native binary distribution in npm package. Native binaries for all platforms (Linux x64/arm64, macOS x64/arm64, Windows x64) are now correctly included when publishing.
|
||||
|
||||
## 0.7.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 6afede2: Fix native binary distribution in npm package
|
||||
|
||||
Native binaries for all platforms (Linux x64/arm64, macOS x64/arm64, Windows x64) are now included in the npm package. Previously, the release workflow published to npm before building binaries, causing "No binary found" errors on installation.
|
||||
|
||||
## 0.7.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Fix native binary distribution in npm package. Native binaries for all platforms (Linux x64/arm64, macOS x64/arm64, Windows x64) are now included in the npm package. Previously, the release workflow published to npm before building binaries, causing "No binary found" errors on installation.
|
||||
|
||||
## 0.7.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 316e649: ## New Features
|
||||
|
||||
- **Cloud browser providers** - Connect to Browserbase or Browser Use for remote browser infrastructure via `-p` flag or `AGENT_BROWSER_PROVIDER` env var
|
||||
- **Persistent browser profiles** - Store cookies, localStorage, and login sessions across browser restarts with `--profile`
|
||||
- **Remote CDP WebSocket URLs** - Connect to remote browser services via WebSocket URL (e.g., `--cdp "wss://..."`)
|
||||
- **Download commands** - New `download` command and `wait --download` for file downloads with ref support
|
||||
- **Browser launch configuration** - New `--args`, `--user-agent`, and `--proxy-bypass` flags for fine-grained browser control
|
||||
- **Enhanced skills** - Hierarchical structure with references and templates for Claude Code
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- Screenshot command now supports refs and has improved error messages
|
||||
- WebSocket URLs work in `connect` command
|
||||
- Fixed socket file location (uses `~/.agent-browser` instead of TMPDIR)
|
||||
- Windows binary path fix (.exe extension)
|
||||
- State load and path-based actions now show correct output messages
|
||||
|
||||
## Documentation
|
||||
|
||||
- Added Claude Code marketplace plugin installation instructions
|
||||
- Updated skill documentation with references and templates
|
||||
- Improved error documentation
|
||||
@@ -1,4 +0,0 @@
|
||||
# Vercel Sandbox credentials
|
||||
SANDBOX_VERCEL_TOKEN=
|
||||
SANDBOX_VERCEL_TEAM_ID=
|
||||
SANDBOX_VERCEL_PROJECT_ID=
|
||||
@@ -1,2 +0,0 @@
|
||||
node_modules/
|
||||
results.json
|
||||
@@ -1,76 +0,0 @@
|
||||
# agent-browser Daemon Benchmarks
|
||||
|
||||
Compares command latency and system metrics between the **Node.js daemon** (published npm version) and the **Rust native daemon** (built from source), running inside a [Vercel Sandbox](https://vercel.com/docs/sandbox) microVM.
|
||||
|
||||
## What it measures
|
||||
|
||||
**Command latency** -- per-scenario timing with warmup, multiple iterations, and stddev:
|
||||
|
||||
- `navigate` -- page load round-trip
|
||||
- `snapshot` -- accessibility tree generation
|
||||
- `screenshot` -- viewport capture
|
||||
- `evaluate` -- JavaScript execution
|
||||
- `click` -- element interaction
|
||||
- `fill` -- form input
|
||||
- `agent-loop` -- snapshot/click/snapshot cycle (typical AI agent pattern)
|
||||
- `full-workflow` -- realistic 7-command sequence
|
||||
|
||||
**System metrics** -- collected while the daemon is running:
|
||||
|
||||
- Cold start time (daemon spawn + browser launch)
|
||||
- Binary size and total distribution size (including browser download)
|
||||
- Daemon RSS and peak RSS (separated from browser process memory)
|
||||
- Browser RSS (Chrome processes, same for both daemons)
|
||||
- Daemon CPU time
|
||||
- Process counts
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js 18+
|
||||
- pnpm
|
||||
- Vercel Sandbox credentials (token, team ID, project ID)
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
cd benchmarks
|
||||
pnpm install
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Fill in your Vercel Sandbox credentials in `.env`:
|
||||
|
||||
```
|
||||
SANDBOX_VERCEL_TOKEN=your_token
|
||||
SANDBOX_VERCEL_TEAM_ID=your_team_id
|
||||
SANDBOX_VERCEL_PROJECT_ID=your_project_id
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
pnpm bench # 10 iterations, 1 warmup, 8 vCPUs
|
||||
pnpm bench -- --iterations 20 # more iterations for tighter stats
|
||||
pnpm bench -- --warmup 2 # extra warmup iterations
|
||||
pnpm bench -- --json # write results.json
|
||||
pnpm bench -- --branch main # build native from a different branch
|
||||
pnpm bench -- --vcpus 16 # more vCPUs (faster Rust build)
|
||||
```
|
||||
|
||||
## How it works
|
||||
|
||||
1. Creates a Vercel Sandbox (Amazon Linux, configurable vCPUs)
|
||||
2. Installs Chromium system dependencies
|
||||
3. **Phase 1 -- Node.js daemon**: installs `agent-browser` from npm (last version with the Node daemon), runs all scenarios, collects metrics
|
||||
4. **Phase 2 -- Rust native daemon**: installs Rust toolchain, clones the repo, runs `cargo build --release`, replaces the binary, runs the same scenarios, collects metrics
|
||||
5. Prints comparison tables and optionally writes `results.json`
|
||||
|
||||
## Interpreting results
|
||||
|
||||
**Command latency** is dominated by Chrome (CDP round-trips), not the daemon. Both daemons are thin relays between the CLI and Chrome, so per-command speedups are typically small. The stddev column helps distinguish real differences from noise.
|
||||
|
||||
**Where the native daemon wins** is in cold start (no Node.js runtime to boot), daemon memory (single Rust binary vs V8 heap), and distribution size (no Playwright dependency).
|
||||
|
||||
The **daemon RSS** metric isolates the daemon process memory from Chrome. This is the apples-to-apples comparison -- both daemons talk to the same Chrome, but Node.js adds ~140 MB of V8 overhead while the Rust daemon uses ~7 MB.
|
||||
|
||||
**Distribution size** includes the daemon plus its browser download. The Node version includes the npm package + Playwright's bundled Chromium. The Rust version is just the binary + Chrome for Testing.
|
||||
@@ -1,900 +0,0 @@
|
||||
/**
|
||||
* Node.js Daemon vs Rust Native Daemon benchmark.
|
||||
*
|
||||
* Compares the last published npm version (Node.js daemon) against the
|
||||
* Rust-only build from a given branch, running real agent-browser commands
|
||||
* inside a Vercel Sandbox.
|
||||
*
|
||||
* Captures:
|
||||
* - Command latency (per-scenario, with warmup + measured iterations + stddev)
|
||||
* - Cold start time (first launch to daemon ready)
|
||||
* - Daemon memory (RSS, peak RSS) separated from browser memory
|
||||
* - Daemon CPU time
|
||||
* - Process tree (daemon + browser children)
|
||||
* - Binary and distribution size on disk
|
||||
*
|
||||
* Usage:
|
||||
* pnpm bench # default: 10 iterations, 1 warmup
|
||||
* pnpm bench -- --iterations 20 # override iterations
|
||||
* pnpm bench -- --warmup 2 # override warmup count
|
||||
* pnpm bench -- --json # write results.json
|
||||
* pnpm bench -- --branch my-branch # override native branch (default: ctate/native-2)
|
||||
* pnpm bench -- --vcpus 8 # sandbox vCPUs (default: 8, higher = faster Rust build)
|
||||
*/
|
||||
|
||||
import { Sandbox } from "@vercel/sandbox";
|
||||
import { readFileSync, writeFileSync } from "fs";
|
||||
import { scenarios, type Scenario } from "./scenarios.js";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Env
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function loadEnv() {
|
||||
try {
|
||||
const content = readFileSync(".env", "utf-8");
|
||||
for (const line of content.split("\n")) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith("#")) continue;
|
||||
const eq = trimmed.indexOf("=");
|
||||
if (eq === -1) continue;
|
||||
const key = trimmed.slice(0, eq);
|
||||
let val = trimmed.slice(eq + 1);
|
||||
if (
|
||||
(val.startsWith('"') && val.endsWith('"')) ||
|
||||
(val.startsWith("'") && val.endsWith("'"))
|
||||
) {
|
||||
val = val.slice(1, -1);
|
||||
}
|
||||
process.env[key] = val;
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
loadEnv();
|
||||
|
||||
const credentials = {
|
||||
token: process.env.SANDBOX_VERCEL_TOKEN!,
|
||||
teamId: process.env.SANDBOX_VERCEL_TEAM_ID!,
|
||||
projectId: process.env.SANDBOX_VERCEL_PROJECT_ID!,
|
||||
};
|
||||
|
||||
if (!credentials.token || !credentials.teamId || !credentials.projectId) {
|
||||
console.error(
|
||||
"Missing credentials. Set SANDBOX_VERCEL_TOKEN, SANDBOX_VERCEL_TEAM_ID, SANDBOX_VERCEL_PROJECT_ID in .env",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CLI args
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function parseArgs() {
|
||||
const args = process.argv.slice(2);
|
||||
let iterations = 10;
|
||||
let warmup = 1;
|
||||
let json = false;
|
||||
let branch = "ctate/native-2";
|
||||
let vcpus = 8;
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i] === "--iterations" && args[i + 1]) {
|
||||
iterations = parseInt(args[++i], 10);
|
||||
} else if (args[i] === "--warmup" && args[i + 1]) {
|
||||
warmup = parseInt(args[++i], 10);
|
||||
} else if (args[i] === "--json") {
|
||||
json = true;
|
||||
} else if (args[i] === "--branch" && args[i + 1]) {
|
||||
branch = args[++i];
|
||||
} else if (args[i] === "--vcpus" && args[i + 1]) {
|
||||
vcpus = parseInt(args[++i], 10);
|
||||
}
|
||||
}
|
||||
|
||||
return { iterations, warmup, json, branch, vcpus };
|
||||
}
|
||||
|
||||
const config = parseArgs();
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Constants
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const TIMEOUT_MS = 30 * 60 * 1000;
|
||||
const REPO_URL = "https://github.com/vercel-labs/agent-browser.git";
|
||||
|
||||
const CHROMIUM_SYSTEM_DEPS = [
|
||||
"nss",
|
||||
"nspr",
|
||||
"libxkbcommon",
|
||||
"atk",
|
||||
"at-spi2-atk",
|
||||
"at-spi2-core",
|
||||
"libXcomposite",
|
||||
"libXdamage",
|
||||
"libXrandr",
|
||||
"libXfixes",
|
||||
"libXcursor",
|
||||
"libXi",
|
||||
"libXtst",
|
||||
"libXScrnSaver",
|
||||
"libXext",
|
||||
"mesa-libgbm",
|
||||
"libdrm",
|
||||
"mesa-libGL",
|
||||
"mesa-libEGL",
|
||||
"cups-libs",
|
||||
"alsa-lib",
|
||||
"pango",
|
||||
"cairo",
|
||||
"gtk3",
|
||||
"dbus-libs",
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Sandbox helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type SandboxInstance = InstanceType<typeof Sandbox>;
|
||||
|
||||
async function run(
|
||||
sandbox: SandboxInstance,
|
||||
cmd: string,
|
||||
args: string[],
|
||||
): Promise<string> {
|
||||
const result = await sandbox.runCommand(cmd, args);
|
||||
const stdout = await result.stdout();
|
||||
const stderr = await result.stderr();
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(
|
||||
`Command failed (exit ${result.exitCode}): ${cmd} ${args.join(" ")}\n${stderr || stdout}`,
|
||||
);
|
||||
}
|
||||
return stdout;
|
||||
}
|
||||
|
||||
async function shell(sandbox: SandboxInstance, script: string): Promise<string> {
|
||||
return run(sandbox, "sh", ["-c", script]);
|
||||
}
|
||||
|
||||
async function shellSafe(sandbox: SandboxInstance, script: string): Promise<string> {
|
||||
const result = await sandbox.runCommand("sh", ["-c", script]);
|
||||
return (await result.stdout()).trim();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Stats
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface Stats {
|
||||
avgMs: number;
|
||||
stddevMs: number;
|
||||
minMs: number;
|
||||
maxMs: number;
|
||||
p50Ms: number;
|
||||
samples: number[];
|
||||
}
|
||||
|
||||
function computeStats(samples: number[]): Stats {
|
||||
const sorted = [...samples].sort((a, b) => a - b);
|
||||
const sum = sorted.reduce((a, b) => a + b, 0);
|
||||
const avg = sum / sorted.length;
|
||||
const variance =
|
||||
sorted.reduce((acc, v) => acc + (v - avg) ** 2, 0) / sorted.length;
|
||||
return {
|
||||
avgMs: Math.round(avg),
|
||||
stddevMs: Math.round(Math.sqrt(variance)),
|
||||
minMs: sorted[0],
|
||||
maxMs: sorted[sorted.length - 1],
|
||||
p50Ms: sorted[Math.floor(sorted.length / 2)],
|
||||
samples: sorted,
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Metrics collection
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface ProcessMetrics {
|
||||
pid: number;
|
||||
rssKb: number;
|
||||
vszKb: number;
|
||||
cpuPercent: number;
|
||||
memPercent: number;
|
||||
cpuTimeSec: number;
|
||||
command: string;
|
||||
}
|
||||
|
||||
interface DaemonMetrics {
|
||||
coldStartMs: number;
|
||||
binarySizeBytes: number;
|
||||
distributionSizeBytes: number;
|
||||
daemonProcesses: ProcessMetrics[];
|
||||
browserProcesses: ProcessMetrics[];
|
||||
daemonRssKb: number;
|
||||
browserRssKb: number;
|
||||
daemonPeakRssKb: number;
|
||||
daemonCpuTimeSec: number;
|
||||
totalCpuTimeSec: number;
|
||||
}
|
||||
|
||||
async function findDaemonPids(
|
||||
sandbox: SandboxInstance,
|
||||
_session: string,
|
||||
): Promise<number[]> {
|
||||
// The daemon process name is "agent-browser" but session/daemon flags are
|
||||
// env vars, not command-line args, so we can't grep them from `ps`.
|
||||
// Instead, find all agent-browser processes that look like long-running daemons
|
||||
// (not short-lived CLI invocations -- those exit immediately).
|
||||
const raw = await shellSafe(
|
||||
sandbox,
|
||||
`pgrep -x agent-browser 2>/dev/null || true`,
|
||||
);
|
||||
if (!raw) {
|
||||
// Fallback: broader match on process name
|
||||
const fallback = await shellSafe(
|
||||
sandbox,
|
||||
`pgrep -f 'agent-browser' 2>/dev/null | head -5 || true`,
|
||||
);
|
||||
if (!fallback) return [];
|
||||
return fallback.split("\n").map(Number).filter(Boolean);
|
||||
}
|
||||
return raw.split("\n").map(Number).filter(Boolean);
|
||||
}
|
||||
|
||||
async function collectProcessMetrics(
|
||||
sandbox: SandboxInstance,
|
||||
pid: number,
|
||||
): Promise<ProcessMetrics | null> {
|
||||
const raw = await shellSafe(
|
||||
sandbox,
|
||||
`ps -p ${pid} -o pid=,rss=,vsz=,%cpu=,%mem=,cputime=,comm= 2>/dev/null || true`,
|
||||
);
|
||||
if (!raw) return null;
|
||||
|
||||
const parts = raw.trim().split(/\s+/);
|
||||
if (parts.length < 7) return null;
|
||||
|
||||
// Parse cputime "HH:MM:SS" or "MM:SS" to seconds
|
||||
const timeParts = parts[5].split(":").map(Number);
|
||||
let cpuTimeSec = 0;
|
||||
if (timeParts.length === 3) {
|
||||
cpuTimeSec = timeParts[0] * 3600 + timeParts[1] * 60 + timeParts[2];
|
||||
} else if (timeParts.length === 2) {
|
||||
cpuTimeSec = timeParts[0] * 60 + timeParts[1];
|
||||
}
|
||||
|
||||
return {
|
||||
pid: Number(parts[0]),
|
||||
rssKb: Number(parts[1]),
|
||||
vszKb: Number(parts[2]),
|
||||
cpuPercent: Number(parts[3]),
|
||||
memPercent: Number(parts[4]),
|
||||
cpuTimeSec,
|
||||
command: parts.slice(6).join(" "),
|
||||
};
|
||||
}
|
||||
|
||||
async function getPeakRssKb(
|
||||
sandbox: SandboxInstance,
|
||||
pid: number,
|
||||
): Promise<number> {
|
||||
const raw = await shellSafe(
|
||||
sandbox,
|
||||
`cat /proc/${pid}/status 2>/dev/null | grep VmHWM | awk '{print $2}' || echo 0`,
|
||||
);
|
||||
return Number(raw) || 0;
|
||||
}
|
||||
|
||||
async function getChildPids(
|
||||
sandbox: SandboxInstance,
|
||||
pid: number,
|
||||
): Promise<number[]> {
|
||||
const raw = await shellSafe(
|
||||
sandbox,
|
||||
`pgrep -P ${pid} 2>/dev/null || true`,
|
||||
);
|
||||
if (!raw) return [];
|
||||
return raw.split("\n").map(Number).filter(Boolean);
|
||||
}
|
||||
|
||||
async function getAllDescendantPids(
|
||||
sandbox: SandboxInstance,
|
||||
pid: number,
|
||||
): Promise<number[]> {
|
||||
const all: number[] = [];
|
||||
const queue = [pid];
|
||||
while (queue.length > 0) {
|
||||
const current = queue.shift()!;
|
||||
all.push(current);
|
||||
const children = await getChildPids(sandbox, current);
|
||||
queue.push(...children);
|
||||
}
|
||||
return all;
|
||||
}
|
||||
|
||||
async function collectDaemonMetrics(
|
||||
sandbox: SandboxInstance,
|
||||
session: string,
|
||||
coldStartMs: number,
|
||||
binarySizeBytes: number,
|
||||
distributionSizeBytes: number,
|
||||
): Promise<DaemonMetrics> {
|
||||
// Find daemon PIDs -- the agent-browser process itself
|
||||
const daemonPids = await findDaemonPids(sandbox, session);
|
||||
|
||||
// Also find the full process tree (daemon + Chrome children)
|
||||
let allPids: number[] = [];
|
||||
for (const pid of daemonPids) {
|
||||
const descendants = await getAllDescendantPids(sandbox, pid);
|
||||
allPids.push(...descendants);
|
||||
}
|
||||
allPids = [...new Set(allPids)];
|
||||
|
||||
// If no daemon PIDs found via pgrep, fall back to grabbing all
|
||||
// agent-browser and chrome processes for metrics
|
||||
if (allPids.length === 0) {
|
||||
const fallback = await shellSafe(
|
||||
sandbox,
|
||||
`ps -eo pid,comm | grep -E 'agent-browser|chrome' | grep -v grep | awk '{print $1}' || true`,
|
||||
);
|
||||
if (fallback) {
|
||||
allPids = fallback.split("\n").map(Number).filter(Boolean);
|
||||
}
|
||||
}
|
||||
|
||||
const daemonProcs: ProcessMetrics[] = [];
|
||||
const browserProcs: ProcessMetrics[] = [];
|
||||
let daemonPeakRssKb = 0;
|
||||
|
||||
for (const pid of allPids) {
|
||||
const metrics = await collectProcessMetrics(sandbox, pid);
|
||||
if (!metrics) continue;
|
||||
|
||||
const isBrowser = /chrome|chromium/i.test(metrics.command);
|
||||
if (isBrowser) {
|
||||
browserProcs.push(metrics);
|
||||
} else {
|
||||
daemonProcs.push(metrics);
|
||||
const peak = await getPeakRssKb(sandbox, pid);
|
||||
daemonPeakRssKb = Math.max(daemonPeakRssKb, peak);
|
||||
}
|
||||
}
|
||||
|
||||
const daemonRssKb = daemonProcs.reduce((sum, p) => sum + p.rssKb, 0);
|
||||
const browserRssKb = browserProcs.reduce((sum, p) => sum + p.rssKb, 0);
|
||||
const daemonCpuTimeSec = daemonProcs.reduce((sum, p) => sum + p.cpuTimeSec, 0);
|
||||
const allProcs = [...daemonProcs, ...browserProcs];
|
||||
const totalCpuTimeSec = allProcs.reduce((sum, p) => sum + p.cpuTimeSec, 0);
|
||||
|
||||
return {
|
||||
coldStartMs,
|
||||
binarySizeBytes,
|
||||
distributionSizeBytes,
|
||||
daemonProcesses: daemonProcs,
|
||||
browserProcesses: browserProcs,
|
||||
daemonRssKb,
|
||||
browserRssKb,
|
||||
daemonPeakRssKb,
|
||||
daemonCpuTimeSec,
|
||||
totalCpuTimeSec,
|
||||
};
|
||||
}
|
||||
|
||||
async function getBinarySize(
|
||||
sandbox: SandboxInstance,
|
||||
): Promise<number> {
|
||||
// Follow symlinks to get the real binary/script size
|
||||
const raw = await shellSafe(
|
||||
sandbox,
|
||||
`stat -L -c %s "$(readlink -f "$(which agent-browser)")" 2>/dev/null || echo 0`,
|
||||
);
|
||||
return Number(raw) || 0;
|
||||
}
|
||||
|
||||
async function getDistributionSize(
|
||||
sandbox: SandboxInstance,
|
||||
mode: DaemonMode,
|
||||
): Promise<number> {
|
||||
if (mode === "node") {
|
||||
// Total size of the npm package + Playwright browser
|
||||
const npmPkg = await shellSafe(
|
||||
sandbox,
|
||||
`du -sb "$(npm root -g)/agent-browser" 2>/dev/null | awk '{print $1}' || echo 0`,
|
||||
);
|
||||
const pwBrowser = await shellSafe(
|
||||
sandbox,
|
||||
`du -sb "$HOME/.cache/ms-playwright" 2>/dev/null | awk '{print $1}' || echo 0`,
|
||||
);
|
||||
return (Number(npmPkg) || 0) + (Number(pwBrowser) || 0);
|
||||
} else {
|
||||
// Rust binary + Chrome for Testing (checks multiple possible cache paths)
|
||||
const binary = await shellSafe(
|
||||
sandbox,
|
||||
`stat -L -c %s "$(readlink -f "$(which agent-browser)")" 2>/dev/null || echo 0`,
|
||||
);
|
||||
const chrome = await shellSafe(
|
||||
sandbox,
|
||||
[
|
||||
`size=0`,
|
||||
`for d in "$HOME/.cache/agent-browser" "$HOME/.cache/ms-playwright" "$HOME/.agent-browser/chrome"; do`,
|
||||
` if [ -d "$d" ]; then size=$(du -sb "$d" 2>/dev/null | awk '{print $1}'); break; fi`,
|
||||
`done`,
|
||||
`echo $size`,
|
||||
].join("; "),
|
||||
);
|
||||
return (Number(binary) || 0) + (Number(chrome) || 0);
|
||||
}
|
||||
}
|
||||
|
||||
function formatBytes(bytes: number): string {
|
||||
if (bytes >= 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
||||
if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
return `${bytes} B`;
|
||||
}
|
||||
|
||||
function formatKb(kb: number): string {
|
||||
if (kb >= 1024) return `${(kb / 1024).toFixed(1)} MB`;
|
||||
return `${kb} KB`;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Scenario runner
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type DaemonMode = "node" | "native";
|
||||
|
||||
function daemonEnv(mode: DaemonMode): Record<string, string> {
|
||||
return { AGENT_BROWSER_SESSION: `bench-${mode}` };
|
||||
}
|
||||
|
||||
async function agentBrowser(
|
||||
sandbox: SandboxInstance,
|
||||
args: string[],
|
||||
mode: DaemonMode,
|
||||
): Promise<void> {
|
||||
const result = await sandbox.runCommand({
|
||||
cmd: "agent-browser",
|
||||
args,
|
||||
env: daemonEnv(mode),
|
||||
});
|
||||
if (result.exitCode !== 0) {
|
||||
const stderr = await result.stderr();
|
||||
const stdout = await result.stdout();
|
||||
throw new Error(
|
||||
`agent-browser ${args.join(" ")} failed (exit ${result.exitCode}): ${stderr || stdout}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function timedAgentBrowser(
|
||||
sandbox: SandboxInstance,
|
||||
args: string[],
|
||||
mode: DaemonMode,
|
||||
): Promise<number> {
|
||||
const start = Date.now();
|
||||
const result = await sandbox.runCommand({
|
||||
cmd: "agent-browser",
|
||||
args,
|
||||
env: daemonEnv(mode),
|
||||
});
|
||||
const elapsed = Date.now() - start;
|
||||
if (result.exitCode !== 0) {
|
||||
const stderr = await result.stderr();
|
||||
const stdout = await result.stdout();
|
||||
throw new Error(
|
||||
`agent-browser ${args.join(" ")} failed (exit ${result.exitCode}): ${stderr || stdout}`,
|
||||
);
|
||||
}
|
||||
return elapsed;
|
||||
}
|
||||
|
||||
interface ScenarioResult {
|
||||
name: string;
|
||||
description: string;
|
||||
stats: Stats;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
async function runScenario(
|
||||
sandbox: SandboxInstance,
|
||||
scenario: Scenario,
|
||||
mode: DaemonMode,
|
||||
iterations: number,
|
||||
warmup: number,
|
||||
): Promise<ScenarioResult> {
|
||||
try {
|
||||
if (scenario.setup) {
|
||||
for (const cmd of scenario.setup) {
|
||||
await agentBrowser(sandbox, cmd, mode);
|
||||
}
|
||||
}
|
||||
|
||||
for (let w = 0; w < warmup; w++) {
|
||||
for (const cmd of scenario.commands) {
|
||||
await agentBrowser(sandbox, cmd, mode);
|
||||
}
|
||||
}
|
||||
|
||||
const samples: number[] = [];
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
let totalMs = 0;
|
||||
for (const cmd of scenario.commands) {
|
||||
totalMs += await timedAgentBrowser(sandbox, cmd, mode);
|
||||
}
|
||||
samples.push(totalMs);
|
||||
}
|
||||
|
||||
if (scenario.teardown) {
|
||||
for (const cmd of scenario.teardown) {
|
||||
await agentBrowser(sandbox, cmd, mode);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: scenario.name,
|
||||
description: scenario.description,
|
||||
stats: computeStats(samples),
|
||||
};
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
return {
|
||||
name: scenario.name,
|
||||
description: scenario.description,
|
||||
stats: { avgMs: -1, stddevMs: -1, minMs: -1, maxMs: -1, p50Ms: -1, samples: [] },
|
||||
error: message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Benchmark phases
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface DaemonResults {
|
||||
mode: DaemonMode;
|
||||
label: string;
|
||||
scenarios: ScenarioResult[];
|
||||
metrics: DaemonMetrics;
|
||||
}
|
||||
|
||||
async function benchmarkDaemon(
|
||||
sandbox: SandboxInstance,
|
||||
mode: DaemonMode,
|
||||
label: string,
|
||||
): Promise<DaemonResults> {
|
||||
console.log(`\n--- ${label} ---`);
|
||||
|
||||
// Measure sizes before launch
|
||||
const binarySizeBytes = await getBinarySize(sandbox);
|
||||
const distributionSizeBytes = await getDistributionSize(sandbox, mode);
|
||||
|
||||
// Cold start: time the first launch (daemon spawn + browser launch)
|
||||
const coldStartBegin = Date.now();
|
||||
await agentBrowser(sandbox, ["open", "about:blank"], mode);
|
||||
const coldStartMs = Date.now() - coldStartBegin;
|
||||
console.log(` Cold start: ${coldStartMs}ms`);
|
||||
console.log(` Binary size: ${formatBytes(binarySizeBytes)}`);
|
||||
console.log(` Distribution size: ${formatBytes(distributionSizeBytes)}`);
|
||||
|
||||
// Run all scenarios
|
||||
const results: ScenarioResult[] = [];
|
||||
for (const scenario of scenarios) {
|
||||
process.stdout.write(` ${scenario.name} `);
|
||||
const result = await runScenario(
|
||||
sandbox,
|
||||
scenario,
|
||||
mode,
|
||||
config.iterations,
|
||||
config.warmup,
|
||||
);
|
||||
if (result.error) {
|
||||
console.log(`FAILED: ${result.error.slice(0, 120)}`);
|
||||
} else {
|
||||
const dots = ".".repeat(Math.max(1, 30 - scenario.name.length));
|
||||
const s = result.stats;
|
||||
console.log(
|
||||
`${dots} ${s.avgMs}ms avg +/-${s.stddevMs}ms (p50: ${s.p50Ms}ms, min: ${s.minMs}ms, max: ${s.maxMs}ms)`,
|
||||
);
|
||||
}
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
// Collect system metrics after scenarios (daemon is still running)
|
||||
const session = `bench-${mode}`;
|
||||
const metrics = await collectDaemonMetrics(
|
||||
sandbox,
|
||||
session,
|
||||
coldStartMs,
|
||||
binarySizeBytes,
|
||||
distributionSizeBytes,
|
||||
);
|
||||
|
||||
// Also grab a full process snapshot for context
|
||||
const psOutput = await shellSafe(
|
||||
sandbox,
|
||||
`ps aux --sort=-rss | head -20`,
|
||||
);
|
||||
console.log(`\n Process snapshot (top by RSS):`);
|
||||
for (const line of psOutput.split("\n").slice(0, 10)) {
|
||||
console.log(` ${line}`);
|
||||
}
|
||||
|
||||
console.log(`\n Daemon processes (${metrics.daemonProcesses.length}):`);
|
||||
console.log(` RSS: ${formatKb(metrics.daemonRssKb)} (peak: ${formatKb(metrics.daemonPeakRssKb)})`);
|
||||
console.log(` CPU time: ${metrics.daemonCpuTimeSec.toFixed(1)}s`);
|
||||
for (const p of metrics.daemonProcesses) {
|
||||
console.log(` PID ${p.pid}: ${p.command} (RSS: ${formatKb(p.rssKb)}, CPU: ${p.cpuPercent}%)`);
|
||||
}
|
||||
console.log(` Browser processes (${metrics.browserProcesses.length}):`);
|
||||
console.log(` RSS: ${formatKb(metrics.browserRssKb)}`);
|
||||
for (const p of metrics.browserProcesses) {
|
||||
console.log(` PID ${p.pid}: ${p.command} (RSS: ${formatKb(p.rssKb)}, CPU: ${p.cpuPercent}%)`);
|
||||
}
|
||||
|
||||
await agentBrowser(sandbox, ["close"], mode);
|
||||
console.log(` Browser closed.`);
|
||||
|
||||
return { mode, label, scenarios: results, metrics };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Install helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function installChromiumDeps(sandbox: SandboxInstance) {
|
||||
console.log("Installing Chromium system dependencies...");
|
||||
await shell(
|
||||
sandbox,
|
||||
`sudo dnf clean all 2>&1 && sudo dnf install -y --skip-broken ${CHROMIUM_SYSTEM_DEPS.join(" ")} 2>&1 && sudo ldconfig 2>&1`,
|
||||
);
|
||||
}
|
||||
|
||||
async function installNodeDaemon(sandbox: SandboxInstance) {
|
||||
console.log("Installing agent-browser from npm (Node.js daemon)...");
|
||||
await run(sandbox, "npm", ["install", "-g", "agent-browser"]);
|
||||
await run(sandbox, "npx", ["agent-browser", "install"]);
|
||||
const version = await shell(sandbox, "agent-browser --version 2>&1 || true");
|
||||
console.log(` version: ${version.trim()}`);
|
||||
}
|
||||
|
||||
async function installNativeDaemon(sandbox: SandboxInstance, branch: string) {
|
||||
console.log(`\nBuilding native daemon from ${branch}...`);
|
||||
|
||||
console.log(" Installing build tools and Rust toolchain...");
|
||||
const rustStart = Date.now();
|
||||
await shell(
|
||||
sandbox,
|
||||
"sudo dnf install -y gcc gcc-c++ make perl-core openssl-devel 2>&1",
|
||||
);
|
||||
await shell(
|
||||
sandbox,
|
||||
"curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y 2>&1",
|
||||
);
|
||||
console.log(` Rust + build tools installed (${Math.round((Date.now() - rustStart) / 1000)}s)`);
|
||||
|
||||
console.log(` Cloning repo (branch: ${branch})...`);
|
||||
const cloneStart = Date.now();
|
||||
await shell(
|
||||
sandbox,
|
||||
`git clone --depth 1 --branch ${branch} ${REPO_URL} /tmp/agent-browser 2>&1`,
|
||||
);
|
||||
console.log(` Cloned (${Math.round((Date.now() - cloneStart) / 1000)}s)`);
|
||||
|
||||
console.log(" Building release binary (cargo build --release)...");
|
||||
const buildStart = Date.now();
|
||||
await shell(
|
||||
sandbox,
|
||||
"source $HOME/.cargo/env && cd /tmp/agent-browser/cli && cargo build --release 2>&1",
|
||||
);
|
||||
console.log(` Built (${Math.round((Date.now() - buildStart) / 1000)}s)`);
|
||||
|
||||
const npmBinPath = (await shell(sandbox, "which agent-browser")).trim();
|
||||
console.log(` Replacing ${npmBinPath} with native build...`);
|
||||
await shell(
|
||||
sandbox,
|
||||
`sudo cp /tmp/agent-browser/cli/target/release/agent-browser ${npmBinPath}`,
|
||||
);
|
||||
|
||||
const version = await shell(sandbox, "agent-browser --version 2>&1 || true");
|
||||
console.log(` version: ${version.trim()}`);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Output
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function printResults(node: DaemonResults, native: DaemonResults) {
|
||||
console.log("\n\n========== COMMAND LATENCY ==========\n");
|
||||
|
||||
const header =
|
||||
"Scenario".padEnd(20) + "| Node avg +/-sd | Rust avg +/-sd | Speedup";
|
||||
const sep = "-".repeat(20) + "|-----------------|-----------------|--------";
|
||||
console.log(header);
|
||||
console.log(sep);
|
||||
|
||||
for (let i = 0; i < node.scenarios.length; i++) {
|
||||
const n = node.scenarios[i];
|
||||
const r = native.scenarios[i];
|
||||
const name = n.name.padEnd(20);
|
||||
|
||||
if (n.error || r.error) {
|
||||
const nodeVal = n.error ? "FAILED".padEnd(15) : `${n.stats.avgMs}ms`.padEnd(15);
|
||||
const rustVal = r.error ? "FAILED".padEnd(15) : `${r.stats.avgMs}ms`.padEnd(15);
|
||||
console.log(`${name}| ${nodeVal} | ${rustVal} | --`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const nodeVal = `${n.stats.avgMs} +/-${n.stats.stddevMs}ms`.padEnd(15);
|
||||
const rustVal = `${r.stats.avgMs} +/-${r.stats.stddevMs}ms`.padEnd(15);
|
||||
const speedup =
|
||||
r.stats.avgMs > 0
|
||||
? (n.stats.avgMs / r.stats.avgMs).toFixed(2) + "x"
|
||||
: "--";
|
||||
console.log(`${name}| ${nodeVal} | ${rustVal} | ${speedup.padStart(6)}`);
|
||||
}
|
||||
|
||||
console.log("\n\n========== SYSTEM METRICS ==========\n");
|
||||
|
||||
const nm = node.metrics;
|
||||
const rm = native.metrics;
|
||||
|
||||
function ratio(a: number, b: number): string {
|
||||
if (b <= 0) return "--";
|
||||
return (a / b).toFixed(2) + "x";
|
||||
}
|
||||
|
||||
const metricRows: [string, string, string, string][] = [
|
||||
[
|
||||
"Cold start",
|
||||
`${nm.coldStartMs}ms`,
|
||||
`${rm.coldStartMs}ms`,
|
||||
ratio(nm.coldStartMs, rm.coldStartMs),
|
||||
],
|
||||
[
|
||||
"Binary size",
|
||||
formatBytes(nm.binarySizeBytes),
|
||||
formatBytes(rm.binarySizeBytes),
|
||||
ratio(nm.binarySizeBytes, rm.binarySizeBytes),
|
||||
],
|
||||
[
|
||||
"Distribution size",
|
||||
formatBytes(nm.distributionSizeBytes),
|
||||
formatBytes(rm.distributionSizeBytes),
|
||||
ratio(nm.distributionSizeBytes, rm.distributionSizeBytes),
|
||||
],
|
||||
[
|
||||
"Daemon RSS",
|
||||
formatKb(nm.daemonRssKb),
|
||||
formatKb(rm.daemonRssKb),
|
||||
ratio(nm.daemonRssKb, rm.daemonRssKb),
|
||||
],
|
||||
[
|
||||
"Daemon peak RSS",
|
||||
formatKb(nm.daemonPeakRssKb),
|
||||
formatKb(rm.daemonPeakRssKb),
|
||||
ratio(nm.daemonPeakRssKb, rm.daemonPeakRssKb),
|
||||
],
|
||||
[
|
||||
"Browser RSS",
|
||||
formatKb(nm.browserRssKb),
|
||||
formatKb(rm.browserRssKb),
|
||||
ratio(nm.browserRssKb, rm.browserRssKb),
|
||||
],
|
||||
[
|
||||
"Daemon CPU time",
|
||||
`${nm.daemonCpuTimeSec.toFixed(1)}s`,
|
||||
`${rm.daemonCpuTimeSec.toFixed(1)}s`,
|
||||
ratio(nm.daemonCpuTimeSec, rm.daemonCpuTimeSec),
|
||||
],
|
||||
[
|
||||
"Daemon processes",
|
||||
String(nm.daemonProcesses.length),
|
||||
String(rm.daemonProcesses.length),
|
||||
"--",
|
||||
],
|
||||
[
|
||||
"Browser processes",
|
||||
String(nm.browserProcesses.length),
|
||||
String(rm.browserProcesses.length),
|
||||
"--",
|
||||
],
|
||||
];
|
||||
|
||||
const mHeader =
|
||||
"Metric".padEnd(20) + "| Node".padEnd(14) + "| Rust".padEnd(14) + "| Ratio";
|
||||
const mSep = "-".repeat(20) + "|" + "-".repeat(13) + "|" + "-".repeat(13) + "|--------";
|
||||
console.log(mHeader);
|
||||
console.log(mSep);
|
||||
for (const [metric, nodeVal, rustVal, ratio] of metricRows) {
|
||||
console.log(
|
||||
`${metric.padEnd(20)}| ${nodeVal.padEnd(12)}| ${rustVal.padEnd(12)}| ${ratio}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function main() {
|
||||
console.log("agent-browser Daemon Benchmark (Node.js vs Rust Native)");
|
||||
console.log(`Branch: ${config.branch}`);
|
||||
console.log(`Iterations: ${config.iterations} (+ ${config.warmup} warmup)`);
|
||||
console.log(`vCPUs: ${config.vcpus}\n`);
|
||||
|
||||
console.log("Creating sandbox...");
|
||||
const sandbox = await Sandbox.create({
|
||||
...credentials,
|
||||
timeout: TIMEOUT_MS,
|
||||
runtime: "node22",
|
||||
networkPolicy: "allow-all" as const,
|
||||
resources: { vcpus: config.vcpus },
|
||||
});
|
||||
console.log(`Sandbox: ${sandbox.sandboxId}`);
|
||||
|
||||
try {
|
||||
await installChromiumDeps(sandbox);
|
||||
|
||||
// Phase 1: Node.js daemon (from published npm package)
|
||||
await installNodeDaemon(sandbox);
|
||||
const nodeResults = await benchmarkDaemon(
|
||||
sandbox,
|
||||
"node",
|
||||
"Node.js Daemon (npm)",
|
||||
);
|
||||
|
||||
// Phase 2: Rust native daemon (built from branch)
|
||||
await installNativeDaemon(sandbox, config.branch);
|
||||
const nativeResults = await benchmarkDaemon(
|
||||
sandbox,
|
||||
"native",
|
||||
`Rust Native Daemon (${config.branch})`,
|
||||
);
|
||||
|
||||
printResults(nodeResults, nativeResults);
|
||||
|
||||
if (config.json) {
|
||||
const output = {
|
||||
timestamp: new Date().toISOString(),
|
||||
branch: config.branch,
|
||||
vcpus: config.vcpus,
|
||||
iterations: config.iterations,
|
||||
warmup: config.warmup,
|
||||
node: {
|
||||
scenarios: nodeResults.scenarios.map((s) => ({
|
||||
name: s.name,
|
||||
description: s.description,
|
||||
...s.stats,
|
||||
error: s.error,
|
||||
})),
|
||||
metrics: nodeResults.metrics,
|
||||
},
|
||||
native: {
|
||||
scenarios: nativeResults.scenarios.map((s) => ({
|
||||
name: s.name,
|
||||
description: s.description,
|
||||
...s.stats,
|
||||
error: s.error,
|
||||
})),
|
||||
metrics: nativeResults.metrics,
|
||||
},
|
||||
};
|
||||
writeFileSync("results.json", JSON.stringify(output, null, 2));
|
||||
console.log("\nResults written to results.json");
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
console.error(`\nFatal error: ${message}`);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
try {
|
||||
await sandbox.stop();
|
||||
console.log("\nSandbox stopped.");
|
||||
} catch {
|
||||
console.warn("Warning: failed to stop sandbox.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"name": "agent-browser-benchmarks",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"bench": "tsx bench.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vercel/sandbox": "^1.8.0",
|
||||
"tsx": "^4.19.0"
|
||||
}
|
||||
}
|
||||
Generated
-472
@@ -1,472 +0,0 @@
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@vercel/sandbox':
|
||||
specifier: ^1.8.0
|
||||
version: 1.8.1
|
||||
tsx:
|
||||
specifier: ^4.19.0
|
||||
version: 4.21.0
|
||||
|
||||
packages:
|
||||
|
||||
'@esbuild/aix-ppc64@0.27.4':
|
||||
resolution: {integrity: sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [aix]
|
||||
|
||||
'@esbuild/android-arm64@0.27.4':
|
||||
resolution: {integrity: sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-arm@0.27.4':
|
||||
resolution: {integrity: sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-x64@0.27.4':
|
||||
resolution: {integrity: sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/darwin-arm64@0.27.4':
|
||||
resolution: {integrity: sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/darwin-x64@0.27.4':
|
||||
resolution: {integrity: sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/freebsd-arm64@0.27.4':
|
||||
resolution: {integrity: sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/freebsd-x64@0.27.4':
|
||||
resolution: {integrity: sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/linux-arm64@0.27.4':
|
||||
resolution: {integrity: sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-arm@0.27.4':
|
||||
resolution: {integrity: sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ia32@0.27.4':
|
||||
resolution: {integrity: sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-loong64@0.27.4':
|
||||
resolution: {integrity: sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-mips64el@0.27.4':
|
||||
resolution: {integrity: sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ppc64@0.27.4':
|
||||
resolution: {integrity: sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-riscv64@0.27.4':
|
||||
resolution: {integrity: sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-s390x@0.27.4':
|
||||
resolution: {integrity: sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-x64@0.27.4':
|
||||
resolution: {integrity: sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/netbsd-arm64@0.27.4':
|
||||
resolution: {integrity: sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/netbsd-x64@0.27.4':
|
||||
resolution: {integrity: sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/openbsd-arm64@0.27.4':
|
||||
resolution: {integrity: sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openbsd-x64@0.27.4':
|
||||
resolution: {integrity: sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openharmony-arm64@0.27.4':
|
||||
resolution: {integrity: sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
|
||||
'@esbuild/sunos-x64@0.27.4':
|
||||
resolution: {integrity: sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
|
||||
'@esbuild/win32-arm64@0.27.4':
|
||||
resolution: {integrity: sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-ia32@0.27.4':
|
||||
resolution: {integrity: sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-x64@0.27.4':
|
||||
resolution: {integrity: sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@vercel/oidc@3.2.0':
|
||||
resolution: {integrity: sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==}
|
||||
engines: {node: '>= 20'}
|
||||
|
||||
'@vercel/sandbox@1.8.1':
|
||||
resolution: {integrity: sha512-txohjI20aMxZiAzBL/KJi5EqTYsesBdOyIOtpTIyebPLTqYtDYfNhQ4OeYiUcPMUo0XBt8gSet/rIdLQEjj3/A==}
|
||||
|
||||
async-retry@1.3.3:
|
||||
resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==}
|
||||
|
||||
b4a@1.8.0:
|
||||
resolution: {integrity: sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==}
|
||||
peerDependencies:
|
||||
react-native-b4a: '*'
|
||||
peerDependenciesMeta:
|
||||
react-native-b4a:
|
||||
optional: true
|
||||
|
||||
bare-events@2.8.2:
|
||||
resolution: {integrity: sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==}
|
||||
peerDependencies:
|
||||
bare-abort-controller: '*'
|
||||
peerDependenciesMeta:
|
||||
bare-abort-controller:
|
||||
optional: true
|
||||
|
||||
esbuild@0.27.4:
|
||||
resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
events-universal@1.0.1:
|
||||
resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==}
|
||||
|
||||
fast-fifo@1.3.2:
|
||||
resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==}
|
||||
|
||||
fsevents@2.3.3:
|
||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
os: [darwin]
|
||||
|
||||
get-tsconfig@4.13.6:
|
||||
resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==}
|
||||
|
||||
jsonlines@0.1.1:
|
||||
resolution: {integrity: sha512-ekDrAGso79Cvf+dtm+mL8OBI2bmAOt3gssYs833De/C9NmIpWDWyUO4zPgB5x2/OhY366dkhgfPMYfwZF7yOZA==}
|
||||
|
||||
ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
||||
os-paths@4.4.0:
|
||||
resolution: {integrity: sha512-wrAwOeXp1RRMFfQY8Sy7VaGVmPocaLwSFOYCGKSyo8qmJ+/yaafCl5BCA1IQZWqFSRBrKDYFeR9d/VyQzfH/jg==}
|
||||
engines: {node: '>= 6.0'}
|
||||
|
||||
picocolors@1.1.1:
|
||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||
|
||||
resolve-pkg-maps@1.0.0:
|
||||
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
|
||||
|
||||
retry@0.13.1:
|
||||
resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==}
|
||||
engines: {node: '>= 4'}
|
||||
|
||||
streamx@2.23.0:
|
||||
resolution: {integrity: sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==}
|
||||
|
||||
tar-stream@3.1.7:
|
||||
resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==}
|
||||
|
||||
text-decoder@1.2.7:
|
||||
resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==}
|
||||
|
||||
tsx@4.21.0:
|
||||
resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
hasBin: true
|
||||
|
||||
undici@7.24.1:
|
||||
resolution: {integrity: sha512-5xoBibbmnjlcR3jdqtY2Lnx7WbrD/tHlT01TmvqZUFVc9Q1w4+j5hbnapTqbcXITMH1ovjq/W7BkqBilHiVAaA==}
|
||||
engines: {node: '>=20.18.1'}
|
||||
|
||||
xdg-app-paths@5.1.0:
|
||||
resolution: {integrity: sha512-RAQ3WkPf4KTU1A8RtFx3gWywzVKe00tfOPFfl2NDGqbIFENQO4kqAJp7mhQjNj/33W5x5hiWWUdyfPq/5SU3QA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
xdg-portable@7.3.0:
|
||||
resolution: {integrity: sha512-sqMMuL1rc0FmMBOzCpd0yuy9trqF2yTTVe+E9ogwCSWQCdDEtQUwrZPT6AxqtsFGRNxycgncbP/xmOOSPw5ZUw==}
|
||||
engines: {node: '>= 6.0'}
|
||||
|
||||
zod@3.24.4:
|
||||
resolution: {integrity: sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@esbuild/aix-ppc64@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm64@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-x64@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-arm64@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-x64@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-arm64@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-x64@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm64@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ia32@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-loong64@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-mips64el@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ppc64@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-riscv64@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-s390x@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-x64@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-arm64@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-x64@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-arm64@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-x64@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openharmony-arm64@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/sunos-x64@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-arm64@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-ia32@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-x64@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@vercel/oidc@3.2.0': {}
|
||||
|
||||
'@vercel/sandbox@1.8.1':
|
||||
dependencies:
|
||||
'@vercel/oidc': 3.2.0
|
||||
async-retry: 1.3.3
|
||||
jsonlines: 0.1.1
|
||||
ms: 2.1.3
|
||||
picocolors: 1.1.1
|
||||
tar-stream: 3.1.7
|
||||
undici: 7.24.1
|
||||
xdg-app-paths: 5.1.0
|
||||
zod: 3.24.4
|
||||
transitivePeerDependencies:
|
||||
- bare-abort-controller
|
||||
- react-native-b4a
|
||||
|
||||
async-retry@1.3.3:
|
||||
dependencies:
|
||||
retry: 0.13.1
|
||||
|
||||
b4a@1.8.0: {}
|
||||
|
||||
bare-events@2.8.2: {}
|
||||
|
||||
esbuild@0.27.4:
|
||||
optionalDependencies:
|
||||
'@esbuild/aix-ppc64': 0.27.4
|
||||
'@esbuild/android-arm': 0.27.4
|
||||
'@esbuild/android-arm64': 0.27.4
|
||||
'@esbuild/android-x64': 0.27.4
|
||||
'@esbuild/darwin-arm64': 0.27.4
|
||||
'@esbuild/darwin-x64': 0.27.4
|
||||
'@esbuild/freebsd-arm64': 0.27.4
|
||||
'@esbuild/freebsd-x64': 0.27.4
|
||||
'@esbuild/linux-arm': 0.27.4
|
||||
'@esbuild/linux-arm64': 0.27.4
|
||||
'@esbuild/linux-ia32': 0.27.4
|
||||
'@esbuild/linux-loong64': 0.27.4
|
||||
'@esbuild/linux-mips64el': 0.27.4
|
||||
'@esbuild/linux-ppc64': 0.27.4
|
||||
'@esbuild/linux-riscv64': 0.27.4
|
||||
'@esbuild/linux-s390x': 0.27.4
|
||||
'@esbuild/linux-x64': 0.27.4
|
||||
'@esbuild/netbsd-arm64': 0.27.4
|
||||
'@esbuild/netbsd-x64': 0.27.4
|
||||
'@esbuild/openbsd-arm64': 0.27.4
|
||||
'@esbuild/openbsd-x64': 0.27.4
|
||||
'@esbuild/openharmony-arm64': 0.27.4
|
||||
'@esbuild/sunos-x64': 0.27.4
|
||||
'@esbuild/win32-arm64': 0.27.4
|
||||
'@esbuild/win32-ia32': 0.27.4
|
||||
'@esbuild/win32-x64': 0.27.4
|
||||
|
||||
events-universal@1.0.1:
|
||||
dependencies:
|
||||
bare-events: 2.8.2
|
||||
transitivePeerDependencies:
|
||||
- bare-abort-controller
|
||||
|
||||
fast-fifo@1.3.2: {}
|
||||
|
||||
fsevents@2.3.3:
|
||||
optional: true
|
||||
|
||||
get-tsconfig@4.13.6:
|
||||
dependencies:
|
||||
resolve-pkg-maps: 1.0.0
|
||||
|
||||
jsonlines@0.1.1: {}
|
||||
|
||||
ms@2.1.3: {}
|
||||
|
||||
os-paths@4.4.0: {}
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
|
||||
resolve-pkg-maps@1.0.0: {}
|
||||
|
||||
retry@0.13.1: {}
|
||||
|
||||
streamx@2.23.0:
|
||||
dependencies:
|
||||
events-universal: 1.0.1
|
||||
fast-fifo: 1.3.2
|
||||
text-decoder: 1.2.7
|
||||
transitivePeerDependencies:
|
||||
- bare-abort-controller
|
||||
- react-native-b4a
|
||||
|
||||
tar-stream@3.1.7:
|
||||
dependencies:
|
||||
b4a: 1.8.0
|
||||
fast-fifo: 1.3.2
|
||||
streamx: 2.23.0
|
||||
transitivePeerDependencies:
|
||||
- bare-abort-controller
|
||||
- react-native-b4a
|
||||
|
||||
text-decoder@1.2.7:
|
||||
dependencies:
|
||||
b4a: 1.8.0
|
||||
transitivePeerDependencies:
|
||||
- react-native-b4a
|
||||
|
||||
tsx@4.21.0:
|
||||
dependencies:
|
||||
esbuild: 0.27.4
|
||||
get-tsconfig: 4.13.6
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
|
||||
undici@7.24.1: {}
|
||||
|
||||
xdg-app-paths@5.1.0:
|
||||
dependencies:
|
||||
xdg-portable: 7.3.0
|
||||
|
||||
xdg-portable@7.3.0:
|
||||
dependencies:
|
||||
os-paths: 4.4.0
|
||||
|
||||
zod@3.24.4: {}
|
||||
@@ -1,105 +0,0 @@
|
||||
/**
|
||||
* Benchmark scenarios for comparing Node.js daemon vs Rust native daemon.
|
||||
*
|
||||
* Each scenario defines CLI commands run via `sandbox.runCommand("agent-browser", args)`.
|
||||
* Setup/teardown commands run once and are not timed.
|
||||
* The `commands` array is timed over N iterations.
|
||||
*/
|
||||
|
||||
export interface Scenario {
|
||||
name: string;
|
||||
description: string;
|
||||
setup?: string[][];
|
||||
commands: string[][];
|
||||
teardown?: string[][];
|
||||
}
|
||||
|
||||
const FORM_HTML = [
|
||||
"<html><head><title>Bench</title></head><body>",
|
||||
"<h1>Benchmark Page</h1>",
|
||||
"<input id='name' type='text' placeholder='Name'>",
|
||||
"<input id='email' type='email' placeholder='Email'>",
|
||||
"<select id='color'><option value='red'>Red</option><option value='blue'>Blue</option></select>",
|
||||
"<input id='agree' type='checkbox'>",
|
||||
"<textarea id='bio' placeholder='Bio'></textarea>",
|
||||
"<button id='submit'>Submit</button>",
|
||||
"<p id='status'>Ready</p>",
|
||||
"<a id='link' href='javascript:void(0)' onclick=\"document.getElementById('status').textContent='Clicked'\">Click me</a>",
|
||||
"<ul>",
|
||||
...Array.from({ length: 20 }, (_, i) => `<li class='item'>Item ${i + 1}</li>`),
|
||||
"</ul>",
|
||||
"</body></html>",
|
||||
].join("");
|
||||
|
||||
const INJECT_FORM_SCRIPT = `document.open(); document.write(${JSON.stringify(FORM_HTML)}); document.close(); 'ok'`;
|
||||
|
||||
const SETUP_PAGE: string[][] = [
|
||||
["open", "about:blank"],
|
||||
["eval", INJECT_FORM_SCRIPT],
|
||||
];
|
||||
|
||||
export const scenarios: Scenario[] = [
|
||||
{
|
||||
name: "navigate",
|
||||
description: "Page navigation (about:blank round-trip)",
|
||||
commands: [["open", "about:blank"]],
|
||||
},
|
||||
{
|
||||
name: "snapshot",
|
||||
description: "DOM snapshot (accessibility tree)",
|
||||
setup: SETUP_PAGE,
|
||||
commands: [["snapshot"]],
|
||||
},
|
||||
{
|
||||
name: "screenshot",
|
||||
description: "Screenshot capture",
|
||||
setup: SETUP_PAGE,
|
||||
commands: [["screenshot"]],
|
||||
},
|
||||
{
|
||||
name: "evaluate",
|
||||
description: "JavaScript evaluation",
|
||||
setup: SETUP_PAGE,
|
||||
commands: [
|
||||
[
|
||||
"eval",
|
||||
"document.title + ' ' + document.querySelectorAll('li').length",
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "click",
|
||||
description: "Element click interaction",
|
||||
setup: SETUP_PAGE,
|
||||
commands: [["click", "#link"]],
|
||||
},
|
||||
{
|
||||
name: "fill",
|
||||
description: "Form field fill",
|
||||
setup: SETUP_PAGE,
|
||||
commands: [["fill", "#name", "Benchmark User"]],
|
||||
},
|
||||
{
|
||||
name: "agent-loop",
|
||||
description: "AI agent loop: snapshot -> click -> snapshot (typical agent cycle)",
|
||||
setup: SETUP_PAGE,
|
||||
commands: [["snapshot"], ["click", "#link"], ["snapshot"]],
|
||||
},
|
||||
{
|
||||
name: "full-workflow",
|
||||
description:
|
||||
"Realistic workflow: navigate, inject form, snapshot, click, fill, evaluate, screenshot",
|
||||
commands: [
|
||||
["open", "about:blank"],
|
||||
["eval", INJECT_FORM_SCRIPT],
|
||||
["snapshot"],
|
||||
["click", "#link"],
|
||||
["fill", "#name", "Agent User"],
|
||||
[
|
||||
"eval",
|
||||
"document.getElementById('name').value",
|
||||
],
|
||||
["screenshot"],
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "dist",
|
||||
"declaration": true
|
||||
},
|
||||
"include": ["*.ts"]
|
||||
}
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1 @@
|
||||
/Users/leo/github.com/agent-browser/cli/target/release/agent-browser: /Users/leo/github.com/agent-browser/cli/build.rs /Users/leo/github.com/agent-browser/cli/cdp-protocol/browser_protocol.json /Users/leo/github.com/agent-browser/cli/cdp-protocol/js_protocol.json /Users/leo/github.com/agent-browser/cli/src/color.rs /Users/leo/github.com/agent-browser/cli/src/commands.rs /Users/leo/github.com/agent-browser/cli/src/connection.rs /Users/leo/github.com/agent-browser/cli/src/flags.rs /Users/leo/github.com/agent-browser/cli/src/install.rs /Users/leo/github.com/agent-browser/cli/src/main.rs /Users/leo/github.com/agent-browser/cli/src/output.rs /Users/leo/github.com/agent-browser/cli/src/validation.rs
|
||||
Generated
+2
-2
@@ -44,8 +44,8 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "agent-browser"
|
||||
version = "0.27.0"
|
||||
name = "agent-browser-stealth"
|
||||
version = "0.24.0-fork.1"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"async-trait",
|
||||
|
||||
+8
-4
@@ -1,15 +1,19 @@
|
||||
[package]
|
||||
name = "agent-browser"
|
||||
version = "0.27.0"
|
||||
name = "agent-browser-stealth"
|
||||
version = "0.24.0-fork.1"
|
||||
edition = "2021"
|
||||
description = "Fast browser automation CLI for AI agents"
|
||||
license = "Apache-2.0"
|
||||
repository = "https://github.com/vercel-labs/agent-browser"
|
||||
homepage = "https://agent-browser.dev"
|
||||
repository = "https://github.com/leeguooooo/agent-browser-stealth"
|
||||
homepage = "https://github.com/leeguooooo/agent-browser-stealth"
|
||||
readme = "../README.md"
|
||||
keywords = ["browser", "automation", "ai", "cdp", "chrome"]
|
||||
categories = ["command-line-utilities", "web-programming"]
|
||||
|
||||
[[bin]]
|
||||
name = "agent-browser"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/app/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"iconLibrary": "lucide",
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"registries": {}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||
import nextTs from "eslint-config-next/typescript";
|
||||
|
||||
const eslintConfig = defineConfig([
|
||||
...nextVitals,
|
||||
...nextTs,
|
||||
// Override default ignores of eslint-config-next.
|
||||
globalIgnores([
|
||||
// Default ignores of eslint-config-next:
|
||||
".next/**",
|
||||
"out/**",
|
||||
"build/**",
|
||||
"next-env.d.ts",
|
||||
]),
|
||||
]);
|
||||
|
||||
export default eslintConfig;
|
||||
@@ -1,102 +0,0 @@
|
||||
import type { MDXComponents } from "mdx/types";
|
||||
import Link from "next/link";
|
||||
import { CodeBlock } from "@/components/code-block";
|
||||
|
||||
function slugify(text: string): string {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.replace(/[^\w\s-]/g, "")
|
||||
.replace(/\s+/g, "-")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function extractText(children: React.ReactNode): string {
|
||||
if (typeof children === "string") return children;
|
||||
if (typeof children === "number") return String(children);
|
||||
if (Array.isArray(children)) return children.map(extractText).join("");
|
||||
if (children && typeof children === "object") {
|
||||
const obj = children as unknown as Record<string, unknown>;
|
||||
if ("props" in obj) {
|
||||
const props = obj.props as { children?: React.ReactNode } | undefined;
|
||||
return extractText(props?.children);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
export function useMDXComponents(components: MDXComponents): MDXComponents {
|
||||
return {
|
||||
...components,
|
||||
h1: ({ children }: { children?: React.ReactNode }) => {
|
||||
const id = slugify(extractText(children));
|
||||
return (
|
||||
<h1 id={id} className="heading-anchor">
|
||||
{children}
|
||||
<a href={`#${id}`} aria-label="Link to this section">#</a>
|
||||
</h1>
|
||||
);
|
||||
},
|
||||
h2: ({ children }: { children?: React.ReactNode }) => {
|
||||
const id = slugify(extractText(children));
|
||||
return (
|
||||
<h2 id={id} className="heading-anchor">
|
||||
{children}
|
||||
<a href={`#${id}`} aria-label="Link to this section">#</a>
|
||||
</h2>
|
||||
);
|
||||
},
|
||||
h3: ({ children }: { children?: React.ReactNode }) => {
|
||||
const id = slugify(extractText(children));
|
||||
return (
|
||||
<h3 id={id} className="heading-anchor">
|
||||
{children}
|
||||
<a href={`#${id}`} aria-label="Link to this section">#</a>
|
||||
</h3>
|
||||
);
|
||||
},
|
||||
a: ({
|
||||
href,
|
||||
children,
|
||||
}: {
|
||||
href?: string;
|
||||
children?: React.ReactNode;
|
||||
}) => {
|
||||
if (href?.startsWith("/")) {
|
||||
return <Link href={href}>{children}</Link>;
|
||||
}
|
||||
return (
|
||||
<a href={href} target="_blank" rel="noopener noreferrer">
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
code: ({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
}) => {
|
||||
if (className) {
|
||||
return <code className={className}>{children}</code>;
|
||||
}
|
||||
return <code>{children}</code>;
|
||||
},
|
||||
pre: async ({ children }: { children?: React.ReactNode }) => {
|
||||
const codeElement = children as React.ReactElement<{
|
||||
className?: string;
|
||||
children?: string;
|
||||
}>;
|
||||
const className = codeElement?.props?.className || "";
|
||||
const lang = className.replace("language-", "") || "bash";
|
||||
const code = codeElement?.props?.children || "";
|
||||
|
||||
return (
|
||||
<CodeBlock
|
||||
code={typeof code === "string" ? code : String(code)}
|
||||
lang={lang}
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import createMDX from "@next/mdx";
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
pageExtensions: ["js", "jsx", "ts", "tsx", "md", "mdx"],
|
||||
serverExternalPackages: ["just-bash", "bash-tool"],
|
||||
};
|
||||
|
||||
const withMDX = createMDX({});
|
||||
|
||||
export default withMDX(nextConfig);
|
||||
@@ -1,48 +0,0 @@
|
||||
{
|
||||
"name": "docs",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "portless agent-browser next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/react": "^3.0.80",
|
||||
"@mdx-js/loader": "^3.1.1",
|
||||
"@mdx-js/mdx": "^3.1.1",
|
||||
"@mdx-js/react": "^3.1.1",
|
||||
"@next/mdx": "^16.1.6",
|
||||
"@streamdown/code": "^1.0.2",
|
||||
"@upstash/ratelimit": "^2.0.8",
|
||||
"@upstash/redis": "^1.36.2",
|
||||
"@vercel/analytics": "^1.6.1",
|
||||
"@vercel/speed-insights": "^1.3.1",
|
||||
"ai": "^6.0.78",
|
||||
"bash-tool": "^1.3.14",
|
||||
"clsx": "^2.1.1",
|
||||
"geist": "^1.7.0",
|
||||
"just-bash": "^2.9.6",
|
||||
"next": "16.1.1",
|
||||
"next-themes": "^0.4.6",
|
||||
"radix-ui": "^1.4.3",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3",
|
||||
"shiki": "^3.21.0",
|
||||
"streamdown": "^2.1.0",
|
||||
"tailwind-merge": "^3.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.1.1",
|
||||
"tailwindcss": "^4",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
Generated
-8139
File diff suppressed because it is too large
Load Diff
@@ -1,7 +0,0 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,123 +0,0 @@
|
||||
import { readFile } from "fs/promises";
|
||||
import { join } from "path";
|
||||
import { convertToModelMessages, stepCountIs, streamText } from "ai";
|
||||
import type { ModelMessage, UIMessage } from "ai";
|
||||
import { createBashTool } from "bash-tool";
|
||||
import { headers } from "next/headers";
|
||||
import { allDocsPages } from "@/lib/docs-navigation";
|
||||
import { mdxToCleanMarkdown } from "@/lib/mdx-to-markdown";
|
||||
import { minuteRateLimit, dailyRateLimit } from "@/lib/rate-limit";
|
||||
|
||||
export const maxDuration = 60;
|
||||
|
||||
const DEFAULT_MODEL = "anthropic/claude-sonnet-4.6";
|
||||
|
||||
const SYSTEM_PROMPT = `You are a helpful documentation assistant for agent-browser, a browser automation CLI designed for AI agents.
|
||||
|
||||
GitHub repository: https://github.com/vercel-labs/agent-browser
|
||||
Documentation: https://agent-browser.dev
|
||||
npm package: agent-browser
|
||||
|
||||
You have access to the full agent-browser documentation via the bash and readFile tools. The docs are available as markdown files in the /workspace/ directory.
|
||||
|
||||
When answering questions:
|
||||
- Use the bash tool to list files (ls /workspace/) or search for content (grep -r "keyword" /workspace/)
|
||||
- Use the readFile tool to read specific documentation pages (e.g. readFile with path "/workspace/index.md")
|
||||
- Do NOT use bash to write, create, modify, or delete files (no tee, cat >, sed -i, echo >, cp, mv, rm, mkdir, touch, etc.) — you are read-only
|
||||
- Always base your answers on the actual documentation content
|
||||
- Be concise and accurate
|
||||
- If the docs don't cover a topic, say so honestly
|
||||
- Do NOT include source references or file paths in your response
|
||||
- Do NOT use emojis in your responses`;
|
||||
|
||||
async function loadDocsFiles(): Promise<Record<string, string>> {
|
||||
const files: Record<string, string> = {};
|
||||
|
||||
const results = await Promise.allSettled(
|
||||
allDocsPages.map(async (page) => {
|
||||
const slug = page.href === "/" ? "" : page.href.replace(/^\//, "");
|
||||
const filePath = slug
|
||||
? join(process.cwd(), "src", "app", slug, "page.mdx")
|
||||
: join(process.cwd(), "src", "app", "page.mdx");
|
||||
|
||||
const raw = await readFile(filePath, "utf-8");
|
||||
const md = mdxToCleanMarkdown(raw);
|
||||
const fileName = slug ? `/${slug}.md` : "/index.md";
|
||||
return { fileName, md };
|
||||
}),
|
||||
);
|
||||
|
||||
for (const result of results) {
|
||||
if (result.status === "fulfilled") {
|
||||
files[result.value.fileName] = result.value.md;
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
function addCacheControl(messages: ModelMessage[]): ModelMessage[] {
|
||||
if (messages.length === 0) return messages;
|
||||
return messages.map((message, index) => {
|
||||
if (index === messages.length - 1) {
|
||||
return {
|
||||
...message,
|
||||
providerOptions: {
|
||||
...message.providerOptions,
|
||||
anthropic: { cacheControl: { type: "ephemeral" } },
|
||||
},
|
||||
};
|
||||
}
|
||||
return message;
|
||||
});
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const headersList = await headers();
|
||||
const ip = headersList.get("x-forwarded-for")?.split(",")[0] ?? "anonymous";
|
||||
|
||||
const [minuteResult, dailyResult] = await Promise.all([
|
||||
minuteRateLimit.limit(ip),
|
||||
dailyRateLimit.limit(ip),
|
||||
]);
|
||||
|
||||
if (!minuteResult.success || !dailyResult.success) {
|
||||
const isMinuteLimit = !minuteResult.success;
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: "Rate limit exceeded",
|
||||
message: isMinuteLimit
|
||||
? "Too many requests. Please wait a moment before trying again."
|
||||
: "Daily limit reached. Please try again tomorrow.",
|
||||
}),
|
||||
{
|
||||
status: 429,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const { messages }: { messages: UIMessage[] } = await req.json();
|
||||
|
||||
const docsFiles = await loadDocsFiles();
|
||||
const {
|
||||
tools: { bash, readFile },
|
||||
} = await createBashTool({ files: docsFiles });
|
||||
|
||||
const result = streamText({
|
||||
headers: {
|
||||
"http-referer": "https://agent-browser.dev",
|
||||
"x-title": "agent-browser",
|
||||
},
|
||||
model: DEFAULT_MODEL,
|
||||
system: SYSTEM_PROMPT,
|
||||
messages: await convertToModelMessages(messages),
|
||||
stopWhen: stepCountIs(5),
|
||||
tools: { bash, readFile },
|
||||
prepareStep: ({ messages: stepMessages }) => ({
|
||||
messages: addCacheControl(stepMessages),
|
||||
}),
|
||||
});
|
||||
|
||||
return result.toUIMessageStreamResponse();
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import { readFile } from "fs/promises";
|
||||
import { join } from "path";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { mdxToCleanMarkdown } from "@/lib/mdx-to-markdown";
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const docPath = searchParams.get("path");
|
||||
|
||||
if (!docPath) {
|
||||
return NextResponse.json(
|
||||
{ error: "Missing ?path= parameter" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const normalized = docPath
|
||||
.replace(/^\//, "")
|
||||
.replace(/\.\./g, "")
|
||||
.replace(/[^a-zA-Z0-9/_-]/g, "");
|
||||
|
||||
const slug = normalized;
|
||||
const filePath = slug
|
||||
? join(process.cwd(), "src", "app", ...slug.split("/"), "page.mdx")
|
||||
: join(process.cwd(), "src", "app", "page.mdx");
|
||||
|
||||
try {
|
||||
const raw = await readFile(filePath, "utf-8");
|
||||
const markdown = mdxToCleanMarkdown(raw);
|
||||
|
||||
return new NextResponse(markdown, {
|
||||
headers: {
|
||||
"Content-Type": "text/markdown; charset=utf-8",
|
||||
"Cache-Control": "public, max-age=3600",
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
return NextResponse.json({ error: "Page not found" }, { status: 404 });
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getSearchIndex } from "@/lib/search-index";
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const q = req.nextUrl.searchParams.get("q")?.trim().toLowerCase();
|
||||
|
||||
if (!q) {
|
||||
return NextResponse.json({ results: [] });
|
||||
}
|
||||
|
||||
const index = await getSearchIndex();
|
||||
const terms = q.split(/\s+/).filter(Boolean);
|
||||
|
||||
const results = index
|
||||
.map((entry) => {
|
||||
const titleLower = entry.title.toLowerCase();
|
||||
const contentLower = entry.content.toLowerCase();
|
||||
|
||||
const titleMatch = terms.every((t) => titleLower.includes(t));
|
||||
const contentMatch = terms.every((t) => contentLower.includes(t));
|
||||
|
||||
if (!titleMatch && !contentMatch) return null;
|
||||
|
||||
let snippet = "";
|
||||
if (contentMatch) {
|
||||
const firstTermIdx = Math.min(
|
||||
...terms.map((t) => {
|
||||
const idx = contentLower.indexOf(t);
|
||||
return idx === -1 ? Infinity : idx;
|
||||
}),
|
||||
);
|
||||
if (firstTermIdx !== Infinity) {
|
||||
const start = Math.max(0, firstTermIdx - 40);
|
||||
const end = Math.min(entry.content.length, firstTermIdx + 120);
|
||||
snippet =
|
||||
(start > 0 ? "..." : "") +
|
||||
entry.content.slice(start, end).replace(/\n/g, " ") +
|
||||
(end < entry.content.length ? "..." : "");
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
title: entry.title,
|
||||
href: entry.href,
|
||||
section: entry.section,
|
||||
snippet,
|
||||
score: titleMatch ? 2 : 1,
|
||||
};
|
||||
})
|
||||
.filter(
|
||||
(
|
||||
r,
|
||||
): r is {
|
||||
title: string;
|
||||
href: string;
|
||||
section: string;
|
||||
snippet: string;
|
||||
score: number;
|
||||
} => r !== null,
|
||||
)
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.slice(0, 20)
|
||||
.map(({ score: _, ...rest }) => rest);
|
||||
|
||||
return NextResponse.json(
|
||||
{ results },
|
||||
{ headers: { "Cache-Control": "public, max-age=60" } },
|
||||
);
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata";
|
||||
|
||||
export const metadata = pageMetadata("cdp-mode");
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return children;
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
# CDP Mode
|
||||
|
||||
Connect to an existing browser via Chrome DevTools Protocol:
|
||||
|
||||
```bash
|
||||
# Start Chrome with: google-chrome --remote-debugging-port=9222
|
||||
|
||||
# Connect once, then run commands without --cdp
|
||||
agent-browser connect 9222
|
||||
agent-browser snapshot
|
||||
agent-browser tab
|
||||
agent-browser close
|
||||
|
||||
# Or pass --cdp on each command
|
||||
agent-browser --cdp 9222 snapshot
|
||||
```
|
||||
|
||||
## Remote WebSocket URLs
|
||||
|
||||
Connect to remote browser services via WebSocket URL:
|
||||
|
||||
```bash
|
||||
# Connect to remote browser service
|
||||
agent-browser --cdp "wss://browser-service.com/cdp?token=..." snapshot
|
||||
|
||||
# Works with any CDP-compatible service
|
||||
agent-browser --cdp "ws://localhost:9222/devtools/browser/abc123" open example.com
|
||||
```
|
||||
|
||||
The `--cdp` flag accepts either:
|
||||
|
||||
- A port number (e.g., `9222`) for local connections via `http://localhost:{port}`
|
||||
- A full WebSocket URL (e.g., `wss://...` or `ws://...`) for remote browser services
|
||||
|
||||
## Auto-Connect
|
||||
|
||||
Use `--auto-connect` to automatically discover and connect to a running Chrome instance without specifying a port:
|
||||
|
||||
```bash
|
||||
# Auto-discover running Chrome with remote debugging
|
||||
agent-browser --auto-connect open example.com
|
||||
agent-browser --auto-connect snapshot
|
||||
|
||||
# Or via environment variable
|
||||
AGENT_BROWSER_AUTO_CONNECT=1 agent-browser snapshot
|
||||
```
|
||||
|
||||
Auto-connect discovers Chrome by:
|
||||
|
||||
1. Reading Chrome's `DevToolsActivePort` file from the default user data directory
|
||||
2. Falling back to probing common debugging ports (9222, 9229)
|
||||
3. If HTTP-based discovery (`/json/version`, `/json/list`) fails, falling back to a direct WebSocket connection
|
||||
|
||||
This is useful when:
|
||||
|
||||
- Chrome 144+ has remote debugging enabled via `chrome://inspect/#remote-debugging` (which uses a dynamic port)
|
||||
- You want a zero-configuration connection to your existing browser
|
||||
- You don't want to track which port Chrome is using
|
||||
|
||||
## Color scheme
|
||||
|
||||
Use `--color-scheme` to set a persistent preference when connecting via CDP:
|
||||
|
||||
```bash
|
||||
agent-browser --cdp 9222 --color-scheme dark open https://example.com
|
||||
agent-browser --cdp 9222 snapshot # stays in dark mode
|
||||
```
|
||||
|
||||
Or set it globally via config or environment variable:
|
||||
|
||||
```bash
|
||||
AGENT_BROWSER_COLOR_SCHEME=dark agent-browser --cdp 9222 open https://example.com
|
||||
```
|
||||
|
||||
## Use cases
|
||||
|
||||
This enables control of:
|
||||
|
||||
- Electron apps
|
||||
- Chrome/Chromium with remote debugging
|
||||
- WebView2 applications
|
||||
- Remote browser services (via WebSocket URL)
|
||||
- Any browser exposing a CDP endpoint
|
||||
|
||||
## Global options
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Option</th><th>Description</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><code>--session <name></code></td><td>Use isolated session</td></tr>
|
||||
<tr><td><code>--profile <path></code></td><td>Persistent browser profile directory</td></tr>
|
||||
<tr><td><code>-p <provider></code></td><td>Cloud browser provider (<code>browserbase</code>, <code>browseruse</code>, <code>kernel</code>, <code>browserless</code>)</td></tr>
|
||||
<tr><td><code>--headers <json></code></td><td>HTTP headers scoped to origin</td></tr>
|
||||
<tr><td><code>--executable-path</code></td><td>Custom browser executable</td></tr>
|
||||
<tr><td><code>--args <args></code></td><td>Browser launch args (comma-separated)</td></tr>
|
||||
<tr><td><code>--user-agent <ua></code></td><td>Custom User-Agent string</td></tr>
|
||||
<tr><td><code>--proxy <url></code></td><td>Proxy server URL</td></tr>
|
||||
<tr><td><code>--proxy-bypass <hosts></code></td><td>Hosts to bypass proxy</td></tr>
|
||||
<tr><td><code>--json</code></td><td>JSON output for scripts</td></tr>
|
||||
<tr><td><code>--name, -n</code></td><td>Locator name filter</td></tr>
|
||||
<tr><td><code>--exact</code></td><td>Exact text match</td></tr>
|
||||
<tr><td><code>--headed</code></td><td>Show browser window</td></tr>
|
||||
<tr><td><code>{"--cdp <port|url>"}</code></td><td>CDP connection (port or WebSocket URL)</td></tr>
|
||||
<tr><td><code>--auto-connect</code></td><td>Auto-discover and connect to running Chrome</td></tr>
|
||||
<tr><td><code>--color-scheme <scheme></code></td><td>Persistent color scheme (<code>dark</code>, <code>light</code>, <code>no-preference</code>)</td></tr>
|
||||
<tr><td><code>--debug</code></td><td>Debug output</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## Cloud providers
|
||||
|
||||
Use the `-p` flag to connect to a cloud browser provider instead of launching a local browser:
|
||||
|
||||
```bash
|
||||
agent-browser -p browserbase open https://example.com
|
||||
```
|
||||
|
||||
See the [Providers](/providers/browser-use) section for setup and configuration of each supported provider: [Browser Use](/providers/browser-use), [Browserbase](/providers/browserbase), [Browserless](/providers/browserless), and [Kernel](/providers/kernel).
|
||||
@@ -1,7 +0,0 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata";
|
||||
|
||||
export const metadata = pageMetadata("changelog");
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return children;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +0,0 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata";
|
||||
|
||||
export const metadata = pageMetadata("commands");
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return children;
|
||||
}
|
||||
@@ -1,569 +0,0 @@
|
||||
# Commands
|
||||
|
||||
## Core
|
||||
|
||||
```bash
|
||||
agent-browser open # Launch browser (no nav); stays on about:blank
|
||||
agent-browser open <url> # Launch + navigate (aliases: goto, navigate)
|
||||
agent-browser click <sel> # Click element (--new-tab to open in new tab)
|
||||
agent-browser dblclick <sel> # Double-click
|
||||
agent-browser fill <sel> <text> # Clear and fill
|
||||
agent-browser type <sel> <text> # Type into element
|
||||
agent-browser press <key> # Press key (Enter, Tab, Control+a) (alias: key)
|
||||
agent-browser keyboard type <text> # Type at current focus (no selector needed)
|
||||
agent-browser keyboard inserttext <text> # Insert text without key events
|
||||
agent-browser keydown <key> # Hold key down
|
||||
agent-browser keyup <key> # Release key
|
||||
agent-browser hover <sel> # Hover element
|
||||
agent-browser focus <sel> # Focus element
|
||||
agent-browser select <sel> <val> # Select dropdown option
|
||||
agent-browser check <sel> # Check checkbox
|
||||
agent-browser uncheck <sel> # Uncheck checkbox
|
||||
agent-browser scroll <dir> [px] # Scroll (up/down/left/right, --selector <sel>)
|
||||
agent-browser scrollintoview <sel> # Scroll element into view
|
||||
agent-browser drag <src> <dst> # Drag and drop
|
||||
agent-browser upload <sel> <files> # Upload files
|
||||
agent-browser screenshot [path] # Screenshot (--full for full page)
|
||||
agent-browser screenshot --annotate # Annotated screenshot with numbered element labels
|
||||
agent-browser screenshot --screenshot-dir ./shots # Save to custom directory
|
||||
agent-browser screenshot --screenshot-format jpeg --screenshot-quality 80
|
||||
agent-browser pdf <path> # Save page as PDF
|
||||
agent-browser snapshot # Accessibility tree with refs
|
||||
agent-browser eval <js> # Run JavaScript
|
||||
agent-browser connect <port|url> # Connect to browser via CDP
|
||||
agent-browser stream enable [--port <port>] # Start runtime WebSocket streaming
|
||||
agent-browser stream status # Show runtime streaming state and bound port
|
||||
agent-browser stream disable # Stop runtime WebSocket streaming
|
||||
agent-browser close # Close browser (aliases: quit, exit)
|
||||
agent-browser close --all # Close all active sessions
|
||||
```
|
||||
|
||||
## Get info
|
||||
|
||||
```bash
|
||||
agent-browser get text <sel> # Get text content
|
||||
agent-browser get html <sel> # Get innerHTML
|
||||
agent-browser get value <sel> # Get input value
|
||||
agent-browser get attr <sel> <attr> # Get attribute
|
||||
agent-browser get title # Get page title
|
||||
agent-browser get url # Get current URL
|
||||
agent-browser get cdp-url # Get CDP WebSocket URL
|
||||
agent-browser get count <sel> # Count matching elements
|
||||
agent-browser get box <sel> # Get bounding box
|
||||
agent-browser get styles <sel> # Get computed styles
|
||||
```
|
||||
|
||||
## Check state
|
||||
|
||||
```bash
|
||||
agent-browser is visible <sel> # Check if visible
|
||||
agent-browser is enabled <sel> # Check if enabled
|
||||
agent-browser is checked <sel> # Check if checked
|
||||
```
|
||||
|
||||
## Find elements
|
||||
|
||||
Semantic locators with actions (`click`, `fill`, `type`, `hover`, `focus`, `check`, `uncheck`, `text`):
|
||||
|
||||
```bash
|
||||
agent-browser find role <role> <action> [value]
|
||||
agent-browser find text <text> <action>
|
||||
agent-browser find label <label> <action> [value]
|
||||
agent-browser find placeholder <ph> <action> [value]
|
||||
agent-browser find alt <text> <action>
|
||||
agent-browser find title <text> <action>
|
||||
agent-browser find testid <id> <action> [value]
|
||||
agent-browser find first <sel> <action> [value]
|
||||
agent-browser find last <sel> <action> [value]
|
||||
agent-browser find nth <n> <sel> <action> [value]
|
||||
```
|
||||
|
||||
Options:
|
||||
|
||||
- `--name <name>` -- filter role by accessible name
|
||||
- `--exact` -- require exact text match
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
agent-browser find role button click --name "Submit"
|
||||
agent-browser find label "Email" fill "test@test.com"
|
||||
agent-browser find alt "Logo" click
|
||||
agent-browser find first ".item" click
|
||||
agent-browser find last ".item" text
|
||||
agent-browser find nth 2 ".card" hover
|
||||
```
|
||||
|
||||
## Wait
|
||||
|
||||
```bash
|
||||
agent-browser wait <selector> # Wait for element
|
||||
agent-browser wait <ms> # Wait for time
|
||||
agent-browser wait --text "Welcome" # Wait for text (substring match)
|
||||
agent-browser wait --url "**/dash" # Wait for URL pattern
|
||||
agent-browser wait --load networkidle # Wait for load state
|
||||
agent-browser wait --fn "condition" # Wait for JS condition
|
||||
agent-browser wait --download [path] # Wait for download
|
||||
agent-browser wait --fn "!document.body.innerText.includes('Loading...')" # Wait for text to disappear
|
||||
agent-browser wait "#spinner" --state hidden # Wait for element to disappear
|
||||
```
|
||||
|
||||
## Downloads
|
||||
|
||||
```bash
|
||||
agent-browser download <sel> <path> # Click element to trigger download
|
||||
agent-browser wait --download [path] # Wait for any download to complete
|
||||
```
|
||||
|
||||
Use `--download-path <dir>` (or `AGENT_BROWSER_DOWNLOAD_PATH` env) to set a default download directory. Without it, downloads go to a temporary directory that is deleted when the browser closes.
|
||||
|
||||
## Mouse
|
||||
|
||||
```bash
|
||||
agent-browser mouse move <x> <y> # Move mouse
|
||||
agent-browser mouse down [button] # Press button
|
||||
agent-browser mouse up [button] # Release button
|
||||
agent-browser mouse wheel <dy> [dx] # Scroll wheel
|
||||
```
|
||||
|
||||
## Clipboard
|
||||
|
||||
```bash
|
||||
agent-browser clipboard read # Read text from clipboard
|
||||
agent-browser clipboard write "Hello, World!" # Write text to clipboard
|
||||
agent-browser clipboard copy # Copy current selection (Ctrl+C)
|
||||
agent-browser clipboard paste # Paste from clipboard (Ctrl+V)
|
||||
```
|
||||
|
||||
## Settings
|
||||
|
||||
```bash
|
||||
agent-browser set viewport <w> <h> [scale] # Set viewport size (scale for retina, e.g. 2)
|
||||
agent-browser set device <name> # Emulate device ("iPhone 14")
|
||||
agent-browser set geo <lat> <lng> # Set geolocation
|
||||
agent-browser set offline [on|off] # Toggle offline mode
|
||||
agent-browser set headers <json> # Extra HTTP headers
|
||||
agent-browser set credentials <u> <p> # HTTP basic auth
|
||||
agent-browser set media [dark|light] # Emulate color scheme (persists for session)
|
||||
```
|
||||
|
||||
Use `--color-scheme` for persistent dark/light mode across all commands:
|
||||
|
||||
```bash
|
||||
agent-browser --color-scheme dark open https://example.com
|
||||
```
|
||||
|
||||
## Cookies & storage
|
||||
|
||||
```bash
|
||||
agent-browser cookies # Get all cookies
|
||||
agent-browser cookies set <name> <val> # Set cookie
|
||||
agent-browser cookies clear # Clear cookies
|
||||
|
||||
agent-browser storage local # Get all localStorage
|
||||
agent-browser storage local <key> # Get specific key
|
||||
agent-browser storage local set <k> <v> # Set value
|
||||
agent-browser storage local clear # Clear all
|
||||
|
||||
agent-browser storage session # Same for sessionStorage
|
||||
```
|
||||
|
||||
## Network
|
||||
|
||||
```bash
|
||||
agent-browser network route <url> # Intercept requests
|
||||
agent-browser network route <url> --abort # Block requests
|
||||
agent-browser network route <url> --body <json> # Mock response
|
||||
agent-browser network route '*' --abort --resource-type script # Block scripts only
|
||||
agent-browser network unroute [url] # Remove routes
|
||||
agent-browser network requests # View tracked requests
|
||||
agent-browser network requests --clear # Clear request log
|
||||
agent-browser network requests --filter <pat> # Filter by URL pattern
|
||||
agent-browser network requests --type xhr,fetch # Filter by resource type
|
||||
agent-browser network requests --method POST # Filter by HTTP method
|
||||
agent-browser network requests --status 2xx # Filter by status (200, 2xx, 400-499)
|
||||
agent-browser network request <requestId> # View full request/response detail
|
||||
agent-browser network har start # Start HAR recording
|
||||
agent-browser network har stop [output.har] # Stop and save HAR (temp path if omitted)
|
||||
```
|
||||
|
||||
## Tabs & frames
|
||||
|
||||
```bash
|
||||
agent-browser tab # List tabs (each row shows tabId and label)
|
||||
agent-browser tab new [url] # New tab
|
||||
agent-browser tab new --label docs [url] # New tab with a user-assigned label
|
||||
agent-browser tab <t<N>|label> # Switch to a tab by id or label
|
||||
agent-browser tab close [t<N>|label] # Close a tab (defaults to active)
|
||||
agent-browser window new # Open new browser window
|
||||
agent-browser frame <sel> # Switch to iframe by CSS selector
|
||||
agent-browser frame @e3 # Switch to iframe by element ref
|
||||
agent-browser frame main # Back to main frame
|
||||
```
|
||||
|
||||
### Stable tab ids and labels
|
||||
|
||||
Tab ids are stable strings of the form `t1`, `t2`, `t3`. They're never reused
|
||||
within a session, so `t2` keeps pointing at the same tab even as other tabs
|
||||
are opened or closed. The `t` prefix mirrors the `@e1` element-ref convention
|
||||
and is not interchangeable with positional integers — `agent-browser tab 2`
|
||||
errors with a teaching message; use `t2`.
|
||||
|
||||
You can also assign a memorable label (`docs`, `app`, `admin`) at tab-creation
|
||||
time and use it anywhere an id is accepted:
|
||||
|
||||
```bash
|
||||
agent-browser tab new --label docs https://docs.example.com
|
||||
agent-browser tab docs # switch to the docs tab
|
||||
agent-browser snapshot # populate refs for docs
|
||||
agent-browser click @e3 # click uses docs's refs
|
||||
agent-browser tab close docs # close by label
|
||||
```
|
||||
|
||||
Labels are never auto-generated and never rewritten on navigation — an agent
|
||||
that names a tab `docs` keeps that name until the tab is closed. Labels are
|
||||
unique within a session; creating a second tab with an existing label
|
||||
errors.
|
||||
|
||||
Refs (`@e1`, etc.) are scoped to the tab that was active when the snapshot
|
||||
ran, so switch tabs first, then snapshot and interact:
|
||||
|
||||
```bash
|
||||
agent-browser tab docs # switch first
|
||||
agent-browser snapshot # refs for docs
|
||||
agent-browser click @e3 # uses docs's refs
|
||||
```
|
||||
|
||||
### Iframe support
|
||||
|
||||
Iframes are detected automatically during snapshots. `Iframe` nodes are resolved and their content is
|
||||
inlined beneath the iframe element in the snapshot output. Refs assigned to elements inside iframes carry
|
||||
frame context, so `click`, `fill`, and other interactions work without manually switching frames.
|
||||
|
||||
```bash
|
||||
agent-browser snapshot -i
|
||||
# @e3 [Iframe] "payment-frame"
|
||||
# @e4 [input] "Card number"
|
||||
# @e5 [button] "Pay"
|
||||
|
||||
# Interact directly using refs — no frame switch needed
|
||||
agent-browser fill @e4 "4111111111111111"
|
||||
agent-browser click @e5
|
||||
|
||||
# Or switch frame context for scoped snapshots
|
||||
agent-browser frame @e3
|
||||
agent-browser snapshot -i # Only elements inside that iframe
|
||||
agent-browser frame main # Return to main frame
|
||||
```
|
||||
|
||||
The `frame` command accepts element refs (`@e3`), CSS selectors (`"#my-iframe"`), or frame name/URL.
|
||||
|
||||
## Dialogs
|
||||
|
||||
```bash
|
||||
agent-browser dialog accept [text] # Accept dialog (with optional prompt text)
|
||||
agent-browser dialog dismiss # Dismiss dialog
|
||||
agent-browser dialog status # Check if a dialog is currently open
|
||||
```
|
||||
|
||||
By default, `alert` and `beforeunload` dialogs are automatically accepted so they never block the agent. `confirm` and `prompt` dialogs still require explicit handling. Use `--no-auto-dialog` (or `AGENT_BROWSER_NO_AUTO_DIALOG=1`) to disable automatic handling.
|
||||
|
||||
When a JavaScript dialog (`alert`, `confirm`, `prompt`) is pending, all command responses include a `warning` field with the dialog type and message.
|
||||
|
||||
## Streaming
|
||||
|
||||
```bash
|
||||
agent-browser stream enable # Start runtime WebSocket streaming on an auto-selected port
|
||||
agent-browser stream enable --port 9223 # Bind a specific localhost port
|
||||
agent-browser stream status # Show enabled state, port, browser connection, screencasting
|
||||
agent-browser stream disable # Stop runtime streaming and remove the .stream metadata file
|
||||
```
|
||||
|
||||
Streaming is enabled automatically for all sessions. Use these commands to check status, re-enable on a specific port, or disable streaming.
|
||||
|
||||
## Debug
|
||||
|
||||
```bash
|
||||
agent-browser trace start [path] # Start trace
|
||||
agent-browser trace stop [path] # Stop and save trace
|
||||
agent-browser profiler start # Start Chrome DevTools profiling
|
||||
agent-browser profiler stop [path] # Stop and save profile (.json)
|
||||
agent-browser record start <path> # Start video recording (WebM)
|
||||
agent-browser record stop # Stop and save video
|
||||
agent-browser record restart <path> # Stop current and start new recording
|
||||
agent-browser console # View console messages
|
||||
agent-browser console --json # JSON output with raw CDP args
|
||||
agent-browser console --clear # Clear console log
|
||||
agent-browser errors # View page errors
|
||||
agent-browser errors --clear # Clear error log
|
||||
agent-browser highlight <sel> # Highlight element
|
||||
agent-browser inspect # Open Chrome DevTools for the active page
|
||||
```
|
||||
|
||||
## Auth vault
|
||||
|
||||
```bash
|
||||
agent-browser auth save <name> [opts] # Save auth profile
|
||||
agent-browser auth login <name> # Login using saved credentials
|
||||
agent-browser auth list # List saved profiles (names and URLs only)
|
||||
agent-browser auth show <name> # Show profile metadata (no passwords)
|
||||
agent-browser auth delete <name> # Delete a saved profile
|
||||
```
|
||||
|
||||
Save options:
|
||||
|
||||
- `--url <url>` -- login page URL (required)
|
||||
- `--username <user>` -- username (required)
|
||||
- `--password <pass>` -- password (required unless `--password-stdin`)
|
||||
- `--password-stdin` -- read password from stdin (recommended to avoid shell history exposure)
|
||||
- `--username-selector <sel>` -- custom CSS selector for username field
|
||||
- `--password-selector <sel>` -- custom CSS selector for password field
|
||||
- `--submit-selector <sel>` -- custom CSS selector for submit button
|
||||
|
||||
`auth login` navigates with `load` and then waits for the username/password/submit selectors to appear before interacting. This improves reliability on SPA login pages where fields render after initial page load.
|
||||
|
||||
```bash
|
||||
echo "pass" | agent-browser auth save github --url https://github.com/login --username user --password-stdin
|
||||
agent-browser auth login github
|
||||
agent-browser auth list
|
||||
```
|
||||
|
||||
## Confirmation
|
||||
|
||||
When `--confirm-actions` is set, certain action categories return a `confirmation_required` response instead of executing immediately. Use `confirm` or `deny` to approve or reject the action.
|
||||
|
||||
```bash
|
||||
agent-browser confirm <confirmation-id> # Approve a pending action
|
||||
agent-browser deny <confirmation-id> # Deny a pending action
|
||||
```
|
||||
|
||||
Pending confirmations auto-deny after 60 seconds.
|
||||
|
||||
```bash
|
||||
agent-browser --confirm-actions eval,download eval "document.title"
|
||||
# Returns confirmation_required with ID
|
||||
agent-browser confirm c_8f3a1234
|
||||
```
|
||||
|
||||
## State management
|
||||
|
||||
```bash
|
||||
agent-browser state save <path> # Save auth state to file
|
||||
agent-browser state load <path> # Load auth state from file
|
||||
agent-browser state list # List saved state files
|
||||
agent-browser state show <file> # Show state summary
|
||||
agent-browser state rename <old> <new> # Rename state file
|
||||
agent-browser state clear [name] # Clear states for session name
|
||||
agent-browser state clear --all # Clear all saved states
|
||||
agent-browser state clean --older-than <days> # Delete old states
|
||||
```
|
||||
|
||||
## Sessions
|
||||
|
||||
```bash
|
||||
agent-browser session # Show current session name
|
||||
agent-browser session list # List active sessions
|
||||
```
|
||||
|
||||
## Chrome profiles
|
||||
|
||||
```bash
|
||||
agent-browser profiles # List available Chrome profiles
|
||||
agent-browser profiles --json # List profiles as JSON
|
||||
agent-browser --profile Default open https://gmail.com # Reuse a profile's login state
|
||||
```
|
||||
|
||||
## Dashboard
|
||||
|
||||
```bash
|
||||
agent-browser dashboard [start] # Start the dashboard server (default port: 4848)
|
||||
agent-browser dashboard start --port <n> # Start on a specific port
|
||||
agent-browser dashboard stop # Stop the dashboard server
|
||||
```
|
||||
|
||||
Open the dashboard through `http://localhost:4848` or a proxied/forwarded dashboard URL such as `https://dashboard.agent-browser.localhost`. The browser stays on the dashboard origin; per-session tabs, status, and stream traffic are proxied internally, so session ports do not need to be exposed.
|
||||
|
||||
## Doctor
|
||||
|
||||
Diagnose your install, auto-clean stale daemon files, and optionally repair common problems.
|
||||
|
||||
```bash
|
||||
agent-browser doctor # Full diagnosis (env, Chrome, daemons, config, providers, network, launch test)
|
||||
agent-browser doctor --offline --quick # Local-only, fastest
|
||||
agent-browser doctor --fix # Also run destructive repairs (reinstall Chrome, purge old state, ...)
|
||||
agent-browser doctor --json # Structured JSON output for agents
|
||||
```
|
||||
|
||||
Exit code is `0` if all checks pass (warnings are fine), `1` if any fail. See the [Installation page](/installation#doctor) for the full check catalog.
|
||||
|
||||
## Chat
|
||||
|
||||
Use natural language to control the browser via AI. The `chat` command translates instructions into agent-browser commands, executes them, and streams the AI response. Requires `AI_GATEWAY_API_KEY` to be set.
|
||||
|
||||
```bash
|
||||
agent-browser chat "open google.com and search for cats" # Single-shot instruction
|
||||
agent-browser chat # Interactive REPL (type quit to exit)
|
||||
echo "summarize this page" | agent-browser chat # Piped input
|
||||
agent-browser -q chat "summarize this page" # Quiet: text only, no tool calls shown
|
||||
agent-browser -v chat "fill in the login form" # Verbose: show commands and their output
|
||||
agent-browser --model openai/gpt-4o chat "take a screenshot" # Override the default AI model
|
||||
agent-browser --json chat "open example.com" # Structured JSON output
|
||||
```
|
||||
|
||||
Chat-specific options:
|
||||
|
||||
```bash
|
||||
--model <name> # AI model (or AI_GATEWAY_MODEL env, default: anthropic/claude-sonnet-4.6)
|
||||
-v, --verbose # Show tool commands and their raw output
|
||||
-q, --quiet # Show only the AI text response (hide tool calls)
|
||||
```
|
||||
|
||||
## Navigation
|
||||
|
||||
```bash
|
||||
agent-browser back # Go back
|
||||
agent-browser forward # Go forward
|
||||
agent-browser reload # Reload page
|
||||
agent-browser pushstate <url> # SPA client-side nav; auto-detects window.next.router.push,
|
||||
# falls back to history.pushState + popstate
|
||||
```
|
||||
|
||||
## Pre-navigation setup
|
||||
|
||||
Some flows need routes, cookies, or init scripts configured *before* the
|
||||
first navigation (SSR debug, auth on protected origins, etc.). `open`
|
||||
without a URL launches the browser but stays on `about:blank`, leaving
|
||||
room to stage state. `batch` makes it one CLI invocation:
|
||||
|
||||
```bash
|
||||
agent-browser batch \
|
||||
'["open"]' \
|
||||
'["network","route","*","--abort","--resource-type","script"]' \
|
||||
'["cookies","set","--curl","cookies.curl","--domain","localhost"]' \
|
||||
'["navigate","http://localhost:3000/target"]'
|
||||
```
|
||||
|
||||
## React / Web Vitals
|
||||
|
||||
React commands require `--enable react-devtools` at launch (installs the
|
||||
React DevTools hook before any page JS runs). `vitals` and `pushstate`
|
||||
work on any site.
|
||||
|
||||
```bash
|
||||
agent-browser open --enable react-devtools <url> # Launch with React hook installed
|
||||
agent-browser react tree # Full component tree
|
||||
agent-browser react inspect <fiberId> # Inspect one component
|
||||
agent-browser react renders start # Begin fiber render recording
|
||||
agent-browser react renders stop [--json] # Stop + print profile
|
||||
agent-browser react suspense [--only-dynamic] [--json] # Suspense boundaries + classifier
|
||||
# --only-dynamic hides the "static" list
|
||||
agent-browser vitals [url] [--json] # LCP/CLS/TTFB/FCP/INP + hydration
|
||||
```
|
||||
|
||||
Works on any React app (Next.js, Remix, Vite+React, CRA, TanStack Start,
|
||||
React Native Web, etc.). `vitals` and `pushstate` are framework-agnostic.
|
||||
|
||||
## Init scripts
|
||||
|
||||
```bash
|
||||
agent-browser open --init-script <path> # Register before first navigation (repeatable)
|
||||
agent-browser addinitscript <js> # Register at runtime (returns identifier)
|
||||
agent-browser removeinitscript <identifier> # Remove a previously registered init script
|
||||
```
|
||||
|
||||
## Global options
|
||||
|
||||
```bash
|
||||
--session <name> # Isolated browser session
|
||||
--session-name <name> # Auto-save/restore session state (cookies, localStorage)
|
||||
--profile <path> # Persistent browser profile directory
|
||||
--state <path> # Load storage state from JSON file
|
||||
--headers <json> # HTTP headers scoped to URL's origin
|
||||
--executable-path <path> # Custom browser executable
|
||||
--extension <path> # Load browser extension (repeatable)
|
||||
--init-script <path> # Register a page init script before first navigation (repeatable)
|
||||
--enable <feature> # Built-in init scripts: react-devtools (repeatable or comma-list)
|
||||
--args <args> # Browser launch args (comma separated)
|
||||
--user-agent <ua> # Custom User-Agent string
|
||||
--proxy <url> # Proxy server URL
|
||||
--proxy-bypass <hosts> # Hosts to bypass proxy
|
||||
--ignore-https-errors # Ignore HTTPS certificate errors
|
||||
--allow-file-access # Allow file:// URLs to access local files (Chromium only)
|
||||
-p, --provider <name> # Browser provider (ios, browserbase, kernel, browseruse, browserless)
|
||||
--device <name> # iOS device name (e.g., "iPhone 15 Pro")
|
||||
--json # JSON output (for scripts)
|
||||
--annotate # Annotated screenshot with numbered element labels
|
||||
--screenshot-dir <path> # Default screenshot output directory (or AGENT_BROWSER_SCREENSHOT_DIR)
|
||||
--screenshot-quality <n> # JPEG quality 0-100 (or AGENT_BROWSER_SCREENSHOT_QUALITY)
|
||||
--screenshot-format <fmt> # Format: png (default), jpeg (or AGENT_BROWSER_SCREENSHOT_FORMAT)
|
||||
--headed # Show browser window (not headless)
|
||||
--cdp <port|url> # Connect via Chrome DevTools Protocol (port or WebSocket URL)
|
||||
--auto-connect # Auto-discover and connect to running Chrome
|
||||
--color-scheme <scheme> # Color scheme: dark, light, no-preference
|
||||
--download-path <path> # Default download directory
|
||||
--content-boundaries # Wrap page output in boundary markers for LLM safety
|
||||
--max-output <chars> # Truncate page output to N characters
|
||||
--allowed-domains <list> # Comma-separated allowed domain patterns
|
||||
--action-policy <path> # Path to action policy JSON file
|
||||
--confirm-actions <list> # Action categories requiring confirmation
|
||||
--confirm-interactive # Interactive confirmation prompts (auto-denies if stdin is not a TTY)
|
||||
--model <name> # AI model for chat (or AI_GATEWAY_MODEL env)
|
||||
-v, --verbose # Show tool commands and their raw output (chat)
|
||||
-q, --quiet # Show only AI text responses (chat)
|
||||
--config <path> # Use a custom config file
|
||||
--debug # Debug output
|
||||
```
|
||||
|
||||
## Batch execution
|
||||
|
||||
Execute multiple commands in a single invocation. Commands can be passed as quoted arguments or piped as JSON via stdin.
|
||||
|
||||
```bash
|
||||
# Argument mode: each quoted argument is a full command
|
||||
agent-browser batch "open https://example.com" "snapshot -i" "screenshot"
|
||||
|
||||
# With --bail to stop on first error
|
||||
agent-browser batch --bail "open https://example.com" "click @e1" "screenshot"
|
||||
|
||||
# Stdin mode: pipe commands as JSON
|
||||
echo '[
|
||||
["open", "https://example.com"],
|
||||
["snapshot", "-i"],
|
||||
["click", "@e1"],
|
||||
["screenshot", "result.png"]
|
||||
]' | agent-browser batch --json
|
||||
```
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Option</th><th>Description</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><code>--bail</code></td><td>Stop on first error (default: continue all commands)</td></tr>
|
||||
<tr><td><code>--json</code></td><td>Output results as a JSON array</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## Command chaining
|
||||
|
||||
Chain commands with `&&` in a single shell invocation. The browser persists via a background daemon, so chaining works naturally and is more efficient than separate calls:
|
||||
|
||||
```bash
|
||||
agent-browser open example.com && agent-browser wait --load networkidle && agent-browser snapshot -i
|
||||
agent-browser fill @e1 "user@example.com" && agent-browser fill @e2 "pass" && agent-browser click @e3
|
||||
agent-browser open example.com && agent-browser wait --load networkidle && agent-browser screenshot page.png
|
||||
```
|
||||
|
||||
Use `&&` when you don't need to read intermediate output. Run commands separately when you need to parse output first (e.g., snapshot to discover refs, then interact with those refs).
|
||||
|
||||
## Local files
|
||||
|
||||
Open local files (PDFs, HTML) using `file://` URLs:
|
||||
|
||||
```bash
|
||||
agent-browser --allow-file-access open file:///path/to/document.pdf
|
||||
agent-browser --allow-file-access open file:///path/to/page.html
|
||||
agent-browser screenshot output.png
|
||||
```
|
||||
|
||||
The `--allow-file-access` flag enables JavaScript to access other local files. Chromium only.
|
||||
@@ -1,7 +0,0 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata";
|
||||
|
||||
export const metadata = pageMetadata("configuration");
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return children;
|
||||
}
|
||||
@@ -1,212 +0,0 @@
|
||||
# Configuration
|
||||
|
||||
Create an `agent-browser.json` file to set persistent defaults instead of repeating flags on every command.
|
||||
|
||||
## Config File Locations
|
||||
|
||||
agent-browser checks two locations, merged in priority order:
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Priority</th><th>Location</th><th>Scope</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>1 (lowest)</td><td><code>~/.agent-browser/config.json</code></td><td>User-level defaults</td></tr>
|
||||
<tr><td>2</td><td><code>./agent-browser.json</code></td><td>Project-level overrides</td></tr>
|
||||
<tr><td>3</td><td><code>AGENT_BROWSER_*</code> env vars</td><td>Override config values</td></tr>
|
||||
<tr><td>4 (highest)</td><td>CLI flags</td><td>Override everything</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
Project-level values override user-level values. Environment variables override both. CLI flags always win.
|
||||
|
||||
Use `--config <path>` or the `AGENT_BROWSER_CONFIG` environment variable to load a specific config file instead of the default locations:
|
||||
|
||||
```bash
|
||||
agent-browser --config ./ci-config.json open example.com
|
||||
AGENT_BROWSER_CONFIG=./ci-config.json agent-browser open example.com
|
||||
```
|
||||
|
||||
## Example Config
|
||||
|
||||
```json
|
||||
{
|
||||
"headed": true,
|
||||
"proxy": "http://localhost:8080",
|
||||
"profile": "./browser-data",
|
||||
"userAgent": "my-agent/1.0",
|
||||
"ignoreHttpsErrors": true
|
||||
}
|
||||
```
|
||||
|
||||
A [JSON Schema](https://agent-browser.dev/schema.json) is available for IDE autocomplete and validation. Add a `$schema` key to your config file to enable it:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://agent-browser.dev/schema.json",
|
||||
"headed": true
|
||||
}
|
||||
```
|
||||
|
||||
## All Options
|
||||
|
||||
Every CLI flag can be set in the config file using its camelCase equivalent:
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Config Key</th><th>CLI Flag</th><th>Type</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><code>headed</code></td><td><code>--headed</code></td><td>boolean</td></tr>
|
||||
<tr><td><code>json</code></td><td><code>--json</code></td><td>boolean</td></tr>
|
||||
<tr><td><code>full</code></td><td><code>--full, -f</code></td><td>boolean</td></tr>
|
||||
<tr><td><code>debug</code></td><td><code>--debug</code></td><td>boolean</td></tr>
|
||||
<tr><td><code>session</code></td><td><code>--session</code></td><td>string</td></tr>
|
||||
<tr><td><code>sessionName</code></td><td><code>--session-name</code></td><td>string</td></tr>
|
||||
<tr><td><code>executablePath</code></td><td><code>--executable-path</code></td><td>string</td></tr>
|
||||
<tr><td><code>extensions</code></td><td><code>--extension</code></td><td>string[]</td></tr>
|
||||
<tr><td><code>profile</code></td><td><code>--profile</code></td><td>string</td></tr>
|
||||
<tr><td><code>state</code></td><td><code>--state</code></td><td>string</td></tr>
|
||||
<tr><td><code>proxy</code></td><td><code>--proxy</code></td><td>string</td></tr>
|
||||
<tr><td><code>proxyBypass</code></td><td><code>--proxy-bypass</code></td><td>string</td></tr>
|
||||
<tr><td><code>args</code></td><td><code>--args</code></td><td>string</td></tr>
|
||||
<tr><td><code>userAgent</code></td><td><code>--user-agent</code></td><td>string</td></tr>
|
||||
<tr><td><code>provider</code></td><td><code>-p, --provider</code></td><td>string</td></tr>
|
||||
<tr><td><code>device</code></td><td><code>--device</code></td><td>string</td></tr>
|
||||
<tr><td><code>ignoreHttpsErrors</code></td><td><code>--ignore-https-errors</code></td><td>boolean</td></tr>
|
||||
<tr><td><code>allowFileAccess</code></td><td><code>--allow-file-access</code></td><td>boolean</td></tr>
|
||||
<tr><td><code>cdp</code></td><td><code>--cdp</code></td><td>string</td></tr>
|
||||
<tr><td><code>autoConnect</code></td><td><code>--auto-connect</code></td><td>boolean</td></tr>
|
||||
<tr><td><code>colorScheme</code></td><td><code>--color-scheme</code></td><td>string (<code>dark</code>, <code>light</code>, <code>no-preference</code>)</td></tr>
|
||||
<tr><td><code>downloadPath</code></td><td><code>--download-path</code></td><td>string</td></tr>
|
||||
<tr><td><code>contentBoundaries</code></td><td><code>--content-boundaries</code></td><td>boolean</td></tr>
|
||||
<tr><td><code>maxOutput</code></td><td><code>--max-output</code></td><td>number</td></tr>
|
||||
<tr><td><code>allowedDomains</code></td><td><code>--allowed-domains</code></td><td>string[]</td></tr>
|
||||
<tr><td><code>actionPolicy</code></td><td><code>--action-policy</code></td><td>string</td></tr>
|
||||
<tr><td><code>confirmActions</code></td><td><code>--confirm-actions</code></td><td>string</td></tr>
|
||||
<tr><td><code>confirmInteractive</code></td><td><code>--confirm-interactive</code></td><td>boolean</td></tr>
|
||||
<tr><td><code>engine</code></td><td><code>--engine</code></td><td>string (<code>chrome</code>, <code>lightpanda</code>)</td></tr>
|
||||
<tr><td><code>noAutoDialog</code></td><td><code>--no-auto-dialog</code></td><td>boolean</td></tr>
|
||||
<tr><td><code>headers</code></td><td><code>--headers</code></td><td>string (JSON)</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## Common Configurations
|
||||
|
||||
### Local Development
|
||||
|
||||
```json
|
||||
{
|
||||
"headed": true,
|
||||
"profile": "./browser-data"
|
||||
}
|
||||
```
|
||||
|
||||
### Behind a Proxy
|
||||
|
||||
```json
|
||||
{
|
||||
"proxy": "http://proxy.corp.example.com:8080",
|
||||
"proxyBypass": "localhost,*.internal.com",
|
||||
"ignoreHttpsErrors": true
|
||||
}
|
||||
```
|
||||
|
||||
### CI / Devcontainer
|
||||
|
||||
```json
|
||||
{
|
||||
"args": "--no-sandbox,--disable-gpu",
|
||||
"ignoreHttpsErrors": true
|
||||
}
|
||||
```
|
||||
|
||||
### iOS Testing
|
||||
|
||||
```json
|
||||
{
|
||||
"provider": "ios",
|
||||
"device": "iPhone 16 Pro"
|
||||
}
|
||||
```
|
||||
|
||||
### AI Agent Security
|
||||
|
||||
```json
|
||||
{
|
||||
"contentBoundaries": true,
|
||||
"maxOutput": 50000,
|
||||
"allowedDomains": ["your-app.com", "*.your-app.com"],
|
||||
"actionPolicy": "./policy.json"
|
||||
}
|
||||
```
|
||||
|
||||
## Overriding Boolean Options
|
||||
|
||||
Boolean flags accept an optional `true`/`false` value to override config settings:
|
||||
|
||||
```bash
|
||||
agent-browser --headed false open example.com
|
||||
```
|
||||
|
||||
A bare flag is equivalent to passing `true`:
|
||||
|
||||
```bash
|
||||
agent-browser --headed open example.com # same as --headed true
|
||||
agent-browser --headed true open example.com # explicit
|
||||
```
|
||||
|
||||
This applies to all boolean flags: `--headed`, `--debug`, `--json`, `--ignore-https-errors`, `--allow-file-access`, `--auto-connect`, `--content-boundaries`, `--confirm-interactive`.
|
||||
|
||||
## Extensions Merging
|
||||
|
||||
Extensions from user-level and project-level configs are **concatenated**, not replaced. For example, if `~/.agent-browser/config.json` specifies `["/ext1"]` and `./agent-browser.json` specifies `["/ext2"]`, the result is `["/ext1", "/ext2"]`.
|
||||
|
||||
The `AGENT_BROWSER_EXTENSIONS` environment variable and CLI `--extension` flags follow the standard priority rules (env replaces config, CLI appends).
|
||||
|
||||
## Environment Variables
|
||||
|
||||
These environment variables configure additional daemon and runtime behavior:
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Variable</th><th>Description</th><th>Default</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><code>AGENT_BROWSER_AUTO_CONNECT</code></td><td>Auto-discover and connect to a running Chrome instance.</td><td>(disabled)</td></tr>
|
||||
<tr><td><code>AGENT_BROWSER_ALLOW_FILE_ACCESS</code></td><td>Allow <code>file://</code> URLs to access local files.</td><td>(disabled)</td></tr>
|
||||
<tr><td><code>AGENT_BROWSER_COLOR_SCHEME</code></td><td>Color scheme preference (<code>dark</code>, <code>light</code>, <code>no-preference</code>).</td><td>(none)</td></tr>
|
||||
<tr><td><code>AGENT_BROWSER_DOWNLOAD_PATH</code></td><td>Default directory for browser downloads.</td><td>(temp directory)</td></tr>
|
||||
<tr><td><code>AGENT_BROWSER_DEFAULT_TIMEOUT</code></td><td>Default timeout in ms. Keep below 30000 to avoid IPC timeouts.</td><td><code>25000</code></td></tr>
|
||||
<tr><td><code>AGENT_BROWSER_SESSION_NAME</code></td><td>Auto-save/load state persistence name.</td><td>(none)</td></tr>
|
||||
<tr><td><code>AGENT_BROWSER_STATE_EXPIRE_DAYS</code></td><td>Auto-delete saved session states older than N days.</td><td><code>30</code></td></tr>
|
||||
<tr><td><code>AGENT_BROWSER_ENCRYPTION_KEY</code></td><td>64-char hex key for AES-256-GCM session encryption.</td><td>(none)</td></tr>
|
||||
<tr><td><code>AGENT_BROWSER_EXTENSIONS</code></td><td>Comma-separated browser extension paths. Extensions work in both headed and headless mode.</td><td>(none)</td></tr>
|
||||
<tr><td><code>AGENT_BROWSER_HEADED</code></td><td>Show browser window instead of running headless (<code>1</code> to enable).</td><td>(disabled)</td></tr>
|
||||
<tr><td><code>AGENT_BROWSER_STREAM_PORT</code></td><td>Override the WebSocket streaming port. By default, an OS-assigned port is used. Set this to bind to a specific port (e.g., <code>9223</code>).</td><td>OS-assigned</td></tr>
|
||||
<tr><td><code>AGENT_BROWSER_IDLE_TIMEOUT_MS</code></td><td>Auto-shutdown the daemon after N ms of inactivity (no commands received). Useful for ephemeral environments.</td><td>(disabled)</td></tr>
|
||||
<tr><td><code>AGENT_BROWSER_IOS_DEVICE</code></td><td>Default iOS device name for the <code>ios</code> provider.</td><td>(none)</td></tr>
|
||||
<tr><td><code>AGENT_BROWSER_IOS_UDID</code></td><td>Default iOS device UDID for the <code>ios</code> provider.</td><td>(none)</td></tr>
|
||||
<tr><td><code>AGENT_BROWSER_DEBUG</code></td><td>Enable debug output (<code>1</code> to enable).</td><td>(disabled)</td></tr>
|
||||
<tr><td><code>AGENT_BROWSER_CONTENT_BOUNDARIES</code></td><td>Wrap page output in boundary markers for LLM safety.</td><td>(disabled)</td></tr>
|
||||
<tr><td><code>AGENT_BROWSER_MAX_OUTPUT</code></td><td>Max characters for page output (truncates beyond limit).</td><td>(unlimited)</td></tr>
|
||||
<tr><td><code>AGENT_BROWSER_ALLOWED_DOMAINS</code></td><td>Comma-separated allowed domain patterns (e.g., <code>example.com,*.example.com</code>).</td><td>(unrestricted)</td></tr>
|
||||
<tr><td><code>AGENT_BROWSER_ACTION_POLICY</code></td><td>Path to action policy JSON file.</td><td>(none)</td></tr>
|
||||
<tr><td><code>AGENT_BROWSER_CONFIRM_ACTIONS</code></td><td>Comma-separated action categories requiring confirmation.</td><td>(none)</td></tr>
|
||||
<tr><td><code>AGENT_BROWSER_CONFIRM_INTERACTIVE</code></td><td>Enable interactive confirmation prompts (auto-denies if stdin is not a TTY).</td><td>(disabled)</td></tr>
|
||||
<tr><td><code>AGENT_BROWSER_ENGINE</code></td><td>Browser engine to use: <code>chrome</code> (default), <code>lightpanda</code>.</td><td><code>chrome</code></td></tr>
|
||||
<tr><td><code>AGENT_BROWSER_NO_AUTO_DIALOG</code></td><td>Disable automatic dismissal of <code>alert</code>/<code>beforeunload</code> dialogs.</td><td>(disabled)</td></tr>
|
||||
<tr><td><code>AI_GATEWAY_URL</code></td><td>Vercel AI Gateway base URL.</td><td><code>https://ai-gateway.vercel.sh</code></td></tr>
|
||||
<tr><td><code>AI_GATEWAY_API_KEY</code></td><td>API key for the Vercel AI Gateway. Required to enable AI chat.</td><td>(none)</td></tr>
|
||||
<tr><td><code>AI_GATEWAY_MODEL</code></td><td>Default AI model for dashboard chat.</td><td><code>anthropic/claude-sonnet-4.6</code></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## Error Handling
|
||||
|
||||
- **Auto-discovered config files** (`~/.agent-browser/config.json`, `./agent-browser.json`) that are missing are silently ignored.
|
||||
- **`--config <path>`** with a missing or malformed file exits with an error.
|
||||
- **Malformed JSON** in auto-discovered files prints a warning to stderr and continues without that file.
|
||||
- **Unknown keys** are silently ignored for forward compatibility.
|
||||
|
||||
> **Tip:** If your project-level `agent-browser.json` contains environment-specific values (paths, proxies), consider adding it to `.gitignore`.
|
||||
@@ -1,7 +0,0 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata";
|
||||
|
||||
export const metadata = pageMetadata("dashboard");
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return children;
|
||||
}
|
||||
@@ -1,160 +0,0 @@
|
||||
# Observability Dashboard
|
||||
|
||||
Monitor agent-browser sessions in real time with a local web dashboard showing a live browser viewport and command activity feed.
|
||||
|
||||
## Usage
|
||||
|
||||
The dashboard is bundled into the binary and requires no separate install. Start the server and open any session:
|
||||
|
||||
```bash
|
||||
agent-browser dashboard start
|
||||
agent-browser open example.com
|
||||
```
|
||||
|
||||
Then open `http://localhost:4848` or a proxied/forwarded dashboard URL such as `https://dashboard.agent-browser.localhost` in your browser to see the live dashboard.
|
||||
|
||||
All sessions automatically stream to the dashboard. No extra flags are needed. The browser stays on the dashboard origin while the server proxies per-session tabs, status, and stream traffic internally, so session ports do not need to be exposed.
|
||||
|
||||
### Custom stream port
|
||||
|
||||
By default each session binds its WebSocket stream server to an OS-assigned port. To use a specific port, set the `AGENT_BROWSER_STREAM_PORT` environment variable:
|
||||
|
||||
```bash
|
||||
AGENT_BROWSER_STREAM_PORT=9223 agent-browser open example.com
|
||||
```
|
||||
|
||||
You can also use the runtime commands to control streaming on a running session:
|
||||
|
||||
```bash
|
||||
agent-browser stream enable --port 9223
|
||||
agent-browser stream status
|
||||
agent-browser stream disable
|
||||
```
|
||||
|
||||
## Dashboard features
|
||||
|
||||
The dashboard is a single-page web app with three areas:
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Area</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>Live viewport</strong></td>
|
||||
<td>Real-time JPEG frames from the browser, rendered to a canvas element</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Activity feed</strong></td>
|
||||
<td>Chronological stream of commands, results, and console messages with expandable details</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Session creation</strong></td>
|
||||
<td>Create new sessions from the dashboard with local engines (Chrome, Lightpanda) or cloud providers (AgentCore, Browserbase, Browserless, Browser Use, Kernel)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Status bar</strong></td>
|
||||
<td>Connection status, viewport dimensions, and WebSocket endpoint</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## WebSocket protocol
|
||||
|
||||
The dashboard connects to the same WebSocket endpoint used by [Streaming](/streaming), with additional message types for observability:
|
||||
|
||||
### Command events
|
||||
|
||||
Sent when a command begins executing:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "command",
|
||||
"action": "click",
|
||||
"id": "r123",
|
||||
"params": { "selector": "@e5" },
|
||||
"timestamp": 1711367000000
|
||||
}
|
||||
```
|
||||
|
||||
### Result events
|
||||
|
||||
Sent when a command finishes:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "result",
|
||||
"id": "r123",
|
||||
"action": "click",
|
||||
"success": true,
|
||||
"data": {},
|
||||
"duration_ms": 45,
|
||||
"timestamp": 1711367000045
|
||||
}
|
||||
```
|
||||
|
||||
### Console events
|
||||
|
||||
Sent when the browser logs to the console:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "console",
|
||||
"level": "log",
|
||||
"text": "Page loaded",
|
||||
"args": [{"type": "string", "value": "Page loaded"}],
|
||||
"timestamp": 1711367000100
|
||||
}
|
||||
```
|
||||
|
||||
The `args` array contains the raw CDP `Runtime.consoleAPICalled` arguments for programmatic access. Object arguments include preview data (e.g. `{userId: "abc", count: 42}` instead of `"Object"`).
|
||||
|
||||
These are in addition to the existing `frame`, `status`, and `error` message types documented on the [Streaming](/streaming) page.
|
||||
|
||||
## Architecture
|
||||
|
||||
The dashboard is a Next.js static export (`output: 'export'`) that produces plain HTML, CSS, and JS. It lives at `packages/dashboard/` in the monorepo and is built with:
|
||||
|
||||
```bash
|
||||
pnpm build:dashboard
|
||||
```
|
||||
|
||||
The dashboard is embedded into the CLI binary at compile time using `rust-embed`. Plain HTTP requests serve the embedded dashboard assets and same-origin API routes. Session-specific tabs, status, and stream WebSocket traffic are proxied through the dashboard server to loopback-only session ports.
|
||||
|
||||
## AI Chat
|
||||
|
||||
The dashboard includes an optional AI chat panel powered by the [Vercel AI Gateway](https://vercel.com/docs/ai-gateway). When enabled, a **Chat** tab appears in the right pane alongside Activity, Console, Network, Storage, and Extensions.
|
||||
|
||||
### Setup
|
||||
|
||||
The Chat tab is always visible. Set the API key to enable responses:
|
||||
|
||||
```bash
|
||||
export AI_GATEWAY_API_KEY=gw_your_key_here
|
||||
agent-browser dashboard start
|
||||
```
|
||||
|
||||
Optionally override the gateway URL or model:
|
||||
|
||||
```bash
|
||||
export AI_GATEWAY_URL=https://ai-gateway.vercel.sh # this is the default
|
||||
export AI_GATEWAY_MODEL=openai/gpt-4o-mini # default: anthropic/claude-sonnet-4.6
|
||||
```
|
||||
|
||||
### How it works
|
||||
|
||||
The Rust server proxies chat requests from the dashboard to the Vercel AI Gateway and streams responses back using the Vercel AI SDK's UI Message Stream protocol. The dashboard frontend uses `useChat` from `@ai-sdk/react` with `DefaultChatTransport`.
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Variable</th><th>Description</th><th>Default</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><code>AI_GATEWAY_URL</code></td><td>Vercel AI Gateway base URL.</td><td><code>https://ai-gateway.vercel.sh</code></td></tr>
|
||||
<tr><td><code>AI_GATEWAY_API_KEY</code></td><td>API key for the AI Gateway. Required to enable AI chat responses.</td><td>(none)</td></tr>
|
||||
<tr><td><code>AI_GATEWAY_MODEL</code></td><td>Default AI model for chat requests.</td><td><code>anthropic/claude-sonnet-4.6</code></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -1,7 +0,0 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata";
|
||||
|
||||
export const metadata = pageMetadata("diffing");
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return children;
|
||||
}
|
||||
@@ -1,175 +0,0 @@
|
||||
import { DiffDemo } from "@/components/diff-demo"
|
||||
|
||||
# Diffing
|
||||
|
||||
Compare page states to detect changes -- structurally via accessibility tree snapshots, visually via pixel comparison, or across two different URLs.
|
||||
|
||||
<DiffDemo />
|
||||
|
||||
## Commands
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Command</th><th>Description</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><code>diff snapshot</code></td><td>Compare current snapshot to last snapshot in session</td></tr>
|
||||
<tr><td><code>diff snapshot --baseline <file></code></td><td>Compare current snapshot to a saved file</td></tr>
|
||||
<tr><td><code>diff screenshot --baseline <file></code></td><td>Visual pixel diff against a baseline image</td></tr>
|
||||
<tr><td><code>diff url <url1> <url2></code></td><td>Compare two pages (snapshot + optional screenshot)</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## Snapshot diff
|
||||
|
||||
Compares the accessibility tree between two points in time using a line-level text diff.
|
||||
|
||||
```bash
|
||||
# Compare against the last snapshot taken in this session
|
||||
agent-browser diff snapshot
|
||||
|
||||
# Compare against a saved baseline file
|
||||
agent-browser diff snapshot --baseline before.txt
|
||||
|
||||
# Scope to a specific part of the page
|
||||
agent-browser diff snapshot --selector "#main" --compact
|
||||
```
|
||||
|
||||
Without `--baseline`, the command automatically compares against the most recent snapshot taken in the current session. This is the primary use case for agents verifying that an action had the intended effect.
|
||||
|
||||
### Options
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Flag</th><th>Description</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><code>-b, --baseline <file></code></td><td>Path to a saved snapshot file to compare against</td></tr>
|
||||
<tr><td><code>-s, --selector <sel></code></td><td>Scope the current snapshot to a CSS selector or @ref</td></tr>
|
||||
<tr><td><code>-c, --compact</code></td><td>Use compact snapshot format</td></tr>
|
||||
<tr><td><code>-d, --depth <n></code></td><td>Limit snapshot tree depth</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
### Output
|
||||
|
||||
The diff uses `+` for added lines and `-` for removed lines, similar to unified diff format. A summary line shows the count of additions, removals, and unchanged lines.
|
||||
|
||||
```
|
||||
- button "Submit" [ref=e2]
|
||||
+ button "Submit" [ref=e2] [disabled]
|
||||
3 additions, 2 removals, 41 unchanged
|
||||
```
|
||||
|
||||
## Screenshot diff
|
||||
|
||||
Compares the current page screenshot against a baseline image at the pixel level. Produces a diff image with changed pixels highlighted in red.
|
||||
|
||||
```bash
|
||||
# Basic visual diff
|
||||
agent-browser diff screenshot --baseline before.png
|
||||
|
||||
# Save diff image to a specific path
|
||||
agent-browser diff screenshot --baseline before.png --output diff.png
|
||||
|
||||
# Adjust threshold and scope to element
|
||||
agent-browser diff screenshot --baseline before.png --threshold 0.2 --selector "#hero"
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Flag</th><th>Description</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><code>-b, --baseline <file></code></td><td>Baseline PNG/JPEG image to compare against (required)</td></tr>
|
||||
<tr><td><code>-o, --output <file></code></td><td>Path for the generated diff image (default: temp dir)</td></tr>
|
||||
<tr><td><code>-t, --threshold <0-1></code></td><td>Color distance threshold (default: 0.1). Higher = more tolerant</td></tr>
|
||||
<tr><td><code>-s, --selector <sel></code></td><td>Scope the current screenshot to an element</td></tr>
|
||||
<tr><td><code>--full</code></td><td>Take a full-page screenshot</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
### Output
|
||||
|
||||
Reports the diff image path, number of different pixels, and mismatch percentage. The diff image shows unchanged pixels dimmed with changed pixels in red.
|
||||
|
||||
If the baseline and current images have different dimensions, the command reports a dimension mismatch instead of attempting pixel comparison.
|
||||
|
||||
## URL diff
|
||||
|
||||
Compares two pages by navigating to each in sequence and diffing the results.
|
||||
|
||||
```bash
|
||||
# Compare two URLs (snapshot diff)
|
||||
agent-browser diff url https://staging.example.com https://prod.example.com
|
||||
|
||||
# Include visual comparison
|
||||
agent-browser diff url https://v1.example.com https://v2.example.com --screenshot
|
||||
|
||||
# Full-page screenshot comparison
|
||||
agent-browser diff url https://v1.example.com https://v2.example.com --screenshot --full
|
||||
```
|
||||
|
||||
The command navigates to the first URL, captures state, then navigates to the second URL and captures again. Snapshot diff is always included. Screenshot diff requires the `--screenshot` flag.
|
||||
|
||||
After completion, the browser remains on the second URL.
|
||||
|
||||
### Options
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Flag</th><th>Description</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><code>--screenshot</code></td><td>Also perform visual screenshot comparison</td></tr>
|
||||
<tr><td><code>--full</code></td><td>Use full-page screenshots</td></tr>
|
||||
<tr><td><code>--wait-until <strategy></code></td><td>Navigation wait strategy: <code>load</code>, <code>domcontentloaded</code>, <code>networkidle</code> (default: <code>load</code>)</td></tr>
|
||||
<tr><td><code>-s, --selector <sel></code></td><td>Scope snapshots to a CSS selector or @ref</td></tr>
|
||||
<tr><td><code>-c, --compact</code></td><td>Use compact snapshot format</td></tr>
|
||||
<tr><td><code>-d, --depth <n></code></td><td>Limit snapshot tree depth</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## Use cases
|
||||
|
||||
### Verifying agent actions
|
||||
|
||||
The most common use case: confirm that an action (click, fill, submit) changed the page as expected.
|
||||
|
||||
```bash
|
||||
agent-browser snapshot -i # Take interactive-only snapshot (baseline)
|
||||
agent-browser fill @e3 "test@example.com"
|
||||
agent-browser diff snapshot # Compare current snapshot to the baseline
|
||||
```
|
||||
|
||||
### Monitoring for changes
|
||||
|
||||
Periodically compare a page against a saved baseline to detect updates.
|
||||
|
||||
```bash
|
||||
# Save baseline
|
||||
agent-browser open https://example.com && agent-browser snapshot > baseline.txt
|
||||
|
||||
# Later, check for changes
|
||||
agent-browser open https://example.com && agent-browser diff snapshot --baseline baseline.txt
|
||||
```
|
||||
|
||||
### Visual regression testing
|
||||
|
||||
Compare screenshots before and after a deploy to catch unintended visual changes.
|
||||
|
||||
```bash
|
||||
agent-browser open https://staging.example.com && agent-browser screenshot baseline.png
|
||||
# ... deploy happens ...
|
||||
agent-browser open https://staging.example.com && agent-browser diff screenshot --baseline baseline.png
|
||||
```
|
||||
|
||||
### Comparing environments
|
||||
|
||||
Diff staging against production to verify parity.
|
||||
|
||||
```bash
|
||||
agent-browser diff url https://staging.example.com https://prod.example.com --screenshot
|
||||
```
|
||||
@@ -1,7 +0,0 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata";
|
||||
|
||||
export const metadata = pageMetadata("engines/chrome");
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return children;
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
# Chrome
|
||||
|
||||
Chrome (and Chromium) is the default browser engine. agent-browser discovers, launches, and manages the Chrome process automatically via the Chrome DevTools Protocol (CDP).
|
||||
|
||||
## Binary Discovery
|
||||
|
||||
When no `--executable-path` is provided, agent-browser searches for Chrome in this order:
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Platform</th><th>Locations checked</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>macOS</td>
|
||||
<td>
|
||||
<code>/Applications/Google Chrome.app</code>,
|
||||
<code>/Applications/Google Chrome Canary.app</code>,
|
||||
<code>/Applications/Chromium.app</code>,
|
||||
<code>/Applications/Brave Browser.app</code>,
|
||||
Puppeteer cache (<code>~/.cache/puppeteer/chrome/</code> or <code>PUPPETEER_CACHE_DIR</code>),
|
||||
Chrome for Testing cache
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Linux</td>
|
||||
<td>
|
||||
<code>google-chrome</code>,
|
||||
<code>google-chrome-stable</code>,
|
||||
<code>chromium-browser</code>,
|
||||
<code>chromium</code> in PATH,
|
||||
Puppeteer cache (<code>~/.cache/puppeteer/chrome/</code> or <code>PUPPETEER_CACHE_DIR</code>),
|
||||
Chrome for Testing cache
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Windows</td>
|
||||
<td>
|
||||
<code>%LOCALAPPDATA%\Google\Chrome\Application\chrome.exe</code>,
|
||||
<code>C:\Program Files\Google\Chrome\Application\chrome.exe</code>,
|
||||
<code>C:\Program Files (x86)\...\chrome.exe</code>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
If Chrome is not found, run `agent-browser install` to download Chrome from Chrome for Testing.
|
||||
|
||||
## Usage
|
||||
|
||||
Chrome is the default engine -- no `--engine` flag is needed:
|
||||
|
||||
```bash
|
||||
agent-browser open example.com
|
||||
```
|
||||
|
||||
To be explicit:
|
||||
|
||||
```bash
|
||||
agent-browser --engine chrome open example.com
|
||||
```
|
||||
|
||||
## Custom Binary
|
||||
|
||||
Point to any Chromium-based browser with `--executable-path`:
|
||||
|
||||
```bash
|
||||
agent-browser --executable-path /path/to/chromium open example.com
|
||||
```
|
||||
|
||||
Or via environment variable:
|
||||
|
||||
```bash
|
||||
export AGENT_BROWSER_EXECUTABLE_PATH=/path/to/chromium
|
||||
agent-browser open example.com
|
||||
```
|
||||
|
||||
## Chrome-Specific Features
|
||||
|
||||
These features are available only with Chrome:
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Feature</th><th>Flag</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>Browser extensions</td><td><code>--extension <path></code></td></tr>
|
||||
<tr><td>Persistent profiles</td><td><code>--profile <path></code> (sets Chrome's <code>--user-data-dir</code>)</td></tr>
|
||||
<tr><td>Storage state</td><td><code>--state <path></code></td></tr>
|
||||
<tr><td>File URL access</td><td><code>--allow-file-access</code></td></tr>
|
||||
<tr><td>Headed mode</td><td><code>--headed</code></td></tr>
|
||||
<tr><td>Custom launch args</td><td><code>--args <args></code></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## Containers and CI
|
||||
|
||||
In Docker, CI runners, or other sandboxed environments, Chrome's user namespace sandbox may need to be disabled:
|
||||
|
||||
```bash
|
||||
agent-browser --args "--no-sandbox" open example.com
|
||||
```
|
||||
|
||||
agent-browser automatically adds `--no-sandbox` when it detects a container environment (Docker, Podman, running as root).
|
||||
@@ -1,7 +0,0 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata";
|
||||
|
||||
export const metadata = pageMetadata("engines/lightpanda");
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return children;
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
# Lightpanda
|
||||
|
||||
[Lightpanda](https://lightpanda.io/) is a headless browser engine built from scratch in Zig for machines. It starts instantly, uses 10x less memory than Chrome, and executes 10x faster.
|
||||
|
||||
agent-browser manages Lightpanda the same way it manages Chrome -- spawning the process, connecting via CDP, and shutting it down. All downstream commands (snapshot, click, fill, screenshot, etc.) work through the same CDP protocol path.
|
||||
|
||||
## Installation
|
||||
|
||||
Install the Lightpanda binary before using it with agent-browser:
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Platform</th><th>Command</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>macOS (Apple Silicon)</td>
|
||||
<td><code>curl -L -o lightpanda https://github.com/lightpanda-io/browser/releases/download/nightly/lightpanda-aarch64-macos && chmod a+x ./lightpanda</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Linux (x86_64)</td>
|
||||
<td><code>curl -L -o lightpanda https://github.com/lightpanda-io/browser/releases/download/nightly/lightpanda-x86_64-linux && chmod a+x ./lightpanda</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
Move the binary somewhere in your `PATH` (e.g. `/usr/local/bin/lightpanda` or `~/.local/bin/lightpanda`).
|
||||
|
||||
See the [Lightpanda installation docs](https://lightpanda.io/docs/open-source/installation) for more options.
|
||||
|
||||
## Usage
|
||||
|
||||
Use the `--engine` flag to select Lightpanda:
|
||||
|
||||
```bash
|
||||
agent-browser --engine lightpanda open example.com
|
||||
agent-browser --engine lightpanda snapshot
|
||||
agent-browser --engine lightpanda screenshot
|
||||
```
|
||||
|
||||
Or set it as the default via environment variable:
|
||||
|
||||
```bash
|
||||
export AGENT_BROWSER_ENGINE=lightpanda
|
||||
agent-browser open example.com
|
||||
```
|
||||
|
||||
Or in your `agent-browser.json` config:
|
||||
|
||||
```json
|
||||
{
|
||||
"engine": "lightpanda"
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Binary Path
|
||||
|
||||
If the `lightpanda` binary is not in your `PATH`, use `--executable-path`:
|
||||
|
||||
```bash
|
||||
agent-browser --engine lightpanda --executable-path /path/to/lightpanda open example.com
|
||||
```
|
||||
|
||||
## Differences from Chrome
|
||||
|
||||
Lightpanda is a purpose-built headless engine. Some Chrome-specific features are not available:
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Feature</th><th>Status</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>Extensions (<code>--extension</code>)</td><td>Not supported</td></tr>
|
||||
<tr><td>Persistent profiles (<code>--profile</code>)</td><td>Not supported</td></tr>
|
||||
<tr><td>Storage state (<code>--state</code>)</td><td>Not supported</td></tr>
|
||||
<tr><td>File access (<code>--allow-file-access</code>)</td><td>Not supported</td></tr>
|
||||
<tr><td>Headed mode (<code>--headed</code>)</td><td>Not applicable (headless only)</td></tr>
|
||||
<tr><td>Screenshots</td><td>Depends on Lightpanda CDP support</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
agent-browser returns a clear error if you combine `--engine lightpanda` with unsupported flags.
|
||||
|
||||
## When to Use Lightpanda
|
||||
|
||||
Lightpanda is a good fit for:
|
||||
|
||||
- Fast web scraping and data extraction
|
||||
- AI agent workflows where speed and low memory matter
|
||||
- CI/CD environments with constrained resources
|
||||
- High-volume parallel automation
|
||||
|
||||
Use Chrome when you need full browser fidelity, extensions, or persistent profiles.
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB |
@@ -1,374 +0,0 @@
|
||||
@import "tailwindcss";
|
||||
@plugin "tailwindcss-animate";
|
||||
|
||||
@source "../../node_modules/streamdown/dist/index.js";
|
||||
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
@theme {
|
||||
--font-sans: "Inter", ui-sans-serif, system-ui, -apple-system, sans-serif;
|
||||
--font-mono: var(--font-geist-mono), ui-monospace, "SF Mono", "Cascadia Mono", "Segoe UI Mono", Menlo, Consolas, monospace;
|
||||
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-border: var(--border);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-sidebar: var(--sidebar);
|
||||
}
|
||||
|
||||
:root {
|
||||
--background: #fff;
|
||||
--foreground: #171717;
|
||||
--border: #e5e5e5;
|
||||
--muted: #f5f5f5;
|
||||
--muted-foreground: #737373;
|
||||
--primary: #171717;
|
||||
--primary-foreground: #fff;
|
||||
--sidebar: #f5f5f5;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #f5f5f5;
|
||||
--border: #262626;
|
||||
--muted: #262626;
|
||||
--muted-foreground: #a3a3a3;
|
||||
--primary: #f5f5f5;
|
||||
--primary-foreground: #0a0a0a;
|
||||
--sidebar: #171717;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-padding-top: 4rem;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background-color: #000;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
::selection {
|
||||
background-color: #fff;
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
|
||||
/* Article tables */
|
||||
article table {
|
||||
width: 100%;
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: 1rem;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
article th {
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
padding: 0.5rem 0.75rem;
|
||||
text-align: left;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: #737373;
|
||||
}
|
||||
|
||||
article td {
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
padding: 0.5rem 0.75rem;
|
||||
color: #525252;
|
||||
}
|
||||
|
||||
:is(.dark) article th {
|
||||
border-bottom-color: #262626;
|
||||
color: #a3a3a3;
|
||||
}
|
||||
|
||||
:is(.dark) article td {
|
||||
border-bottom-color: rgba(38, 38, 38, 0.5);
|
||||
color: #a3a3a3;
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Code blocks */
|
||||
pre {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
padding: 0.875rem;
|
||||
overflow-x: auto;
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
pre:not(.shiki) {
|
||||
background: var(--muted);
|
||||
}
|
||||
|
||||
.code-block pre {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.code-block {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
pre {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
:not(pre) > code {
|
||||
background: var(--muted);
|
||||
padding: 0.125rem 0.375rem;
|
||||
border-radius: 3px;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
/* Diff line highlighting */
|
||||
.diff-add {
|
||||
color: #00952d;
|
||||
}
|
||||
|
||||
.diff-remove {
|
||||
color: #f32e40;
|
||||
}
|
||||
|
||||
.dark .diff-add {
|
||||
color: #00ca50;
|
||||
}
|
||||
|
||||
.dark .diff-remove {
|
||||
color: #f32e40;
|
||||
}
|
||||
|
||||
/* Shiki dual theme support */
|
||||
.shiki,
|
||||
.shiki span {
|
||||
color: var(--shiki-light) !important;
|
||||
background-color: var(--shiki-light-bg) !important;
|
||||
}
|
||||
|
||||
.dark .shiki,
|
||||
.dark .shiki span {
|
||||
color: var(--shiki-dark) !important;
|
||||
background-color: var(--shiki-dark-bg) !important;
|
||||
}
|
||||
|
||||
/* Prose */
|
||||
.prose {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.prose h1 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.02em;
|
||||
margin-bottom: 1.5rem;
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.prose h1 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.prose h2 {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
margin-top: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.prose h2:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.prose h3 {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 0.75rem;
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.prose .heading-anchor {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.prose .heading-anchor > a {
|
||||
opacity: 0;
|
||||
margin-left: 0.375rem;
|
||||
color: var(--muted-foreground);
|
||||
text-decoration: none;
|
||||
font-weight: 400;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.prose .heading-anchor:hover > a {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.prose .heading-anchor > a:hover {
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.prose p {
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.65;
|
||||
color: #525252;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
:is(.dark) .prose p {
|
||||
color: #a3a3a3;
|
||||
}
|
||||
|
||||
.prose ul, .prose ol {
|
||||
margin-bottom: 1rem;
|
||||
padding-left: 1.25rem;
|
||||
}
|
||||
|
||||
.prose ul {
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
.prose ol {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
|
||||
.prose li {
|
||||
margin-bottom: 0.25rem;
|
||||
color: #525252;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
:is(.dark) .prose li {
|
||||
color: #a3a3a3;
|
||||
}
|
||||
|
||||
.prose li strong {
|
||||
color: var(--foreground);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.prose a {
|
||||
color: var(--foreground);
|
||||
text-decoration: underline;
|
||||
text-decoration-color: #d4d4d4;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
|
||||
.prose a:hover {
|
||||
text-decoration-color: var(--foreground);
|
||||
}
|
||||
|
||||
:is(.dark) .prose a {
|
||||
text-decoration-color: #525252;
|
||||
}
|
||||
|
||||
:is(.dark) .prose a:hover {
|
||||
text-decoration-color: var(--foreground);
|
||||
}
|
||||
|
||||
.prose strong {
|
||||
font-weight: 500;
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.prose blockquote {
|
||||
margin-bottom: 1rem;
|
||||
border-left: 2px solid #e5e5e5;
|
||||
padding-left: 1rem;
|
||||
font-size: 0.875rem;
|
||||
color: #737373;
|
||||
}
|
||||
|
||||
:is(.dark) .prose blockquote {
|
||||
border-left-color: #525252;
|
||||
color: #a3a3a3;
|
||||
}
|
||||
|
||||
.prose table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 1.5rem 0;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.prose th, .prose td {
|
||||
text-align: left;
|
||||
padding: 0.625rem 0.875rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.prose th {
|
||||
font-weight: 500;
|
||||
color: var(--muted-foreground);
|
||||
text-transform: uppercase;
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.025em;
|
||||
}
|
||||
|
||||
.prose td {
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
.prose td code {
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
/* Tool call shimmer animation */
|
||||
@keyframes tool-shimmer {
|
||||
0% { opacity: 0.5; }
|
||||
50% { opacity: 1; }
|
||||
100% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
.animate-tool-shimmer {
|
||||
animation: tool-shimmer 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Override prose text color in chat so agent responses use primary foreground */
|
||||
.docs-chat-content p,
|
||||
.docs-chat-content li,
|
||||
.docs-chat-content td,
|
||||
.docs-chat-content th,
|
||||
.docs-chat-content strong,
|
||||
.docs-chat-content code {
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
/* Reset global pre styles inside chat so Streamdown's own styling takes effect */
|
||||
.docs-chat-content pre {
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
padding: revert-layer;
|
||||
}
|
||||
|
||||
/* Fix list rendering in chat content */
|
||||
.docs-chat-content ul,
|
||||
.docs-chat-content ol {
|
||||
list-style-position: outside;
|
||||
padding-left: 1.25em;
|
||||
}
|
||||
|
||||
.docs-chat-content li > p {
|
||||
display: inline;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.docs-chat-content li {
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata";
|
||||
|
||||
export const metadata = pageMetadata("installation");
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return children;
|
||||
}
|
||||
@@ -1,173 +0,0 @@
|
||||
# Installation
|
||||
|
||||
## Global installation (recommended)
|
||||
|
||||
Installs the native Rust binary for maximum performance:
|
||||
|
||||
```bash
|
||||
npm install -g agent-browser
|
||||
agent-browser install # Download Chrome from Chrome for Testing (first time)
|
||||
```
|
||||
|
||||
This is the fastest option -- commands run through the native Rust CLI directly with sub-millisecond parsing overhead.
|
||||
|
||||
## Quick start (no install)
|
||||
|
||||
```bash
|
||||
npx agent-browser install # Download Chrome (first time only)
|
||||
npx agent-browser open example.com
|
||||
```
|
||||
|
||||
## Project installation (local dependency)
|
||||
|
||||
For projects that want to pin the version in `package.json`:
|
||||
|
||||
```bash
|
||||
npm install agent-browser
|
||||
npx agent-browser install # Download Chrome (first time)
|
||||
```
|
||||
|
||||
Then use via `npx` or `package.json` scripts.
|
||||
|
||||
## Homebrew (macOS)
|
||||
|
||||
```bash
|
||||
brew install agent-browser
|
||||
agent-browser install # Download Chrome (first time)
|
||||
```
|
||||
|
||||
## Cargo (Rust)
|
||||
|
||||
```bash
|
||||
cargo install agent-browser
|
||||
agent-browser install # Download Chrome (first time)
|
||||
```
|
||||
|
||||
Compiles from source (~2-3 min). Requires the Rust toolchain ([rustup.rs](https://rustup.rs)).
|
||||
|
||||
## From source
|
||||
|
||||
```bash
|
||||
git clone https://github.com/vercel-labs/agent-browser
|
||||
cd agent-browser
|
||||
pnpm install
|
||||
pnpm build
|
||||
pnpm build:native
|
||||
./bin/agent-browser install
|
||||
pnpm link --global
|
||||
```
|
||||
|
||||
## Linux dependencies
|
||||
|
||||
On Linux, install system dependencies:
|
||||
|
||||
```bash
|
||||
agent-browser install --with-deps
|
||||
```
|
||||
|
||||
## Updating
|
||||
|
||||
Upgrade to the latest version:
|
||||
|
||||
```bash
|
||||
agent-browser upgrade
|
||||
```
|
||||
|
||||
Detects your installation method (npm, Homebrew, or Cargo) and runs the appropriate update command automatically. Displays the version change on success, or informs you if you are already on the latest version.
|
||||
|
||||
## Doctor
|
||||
|
||||
`doctor` diagnoses your install and auto-cleans stale daemon files. Run it whenever something stops working unexpectedly, or after upgrades:
|
||||
|
||||
```bash
|
||||
agent-browser doctor # Full diagnosis
|
||||
agent-browser doctor --offline --quick # Local-only, fastest (~<1s)
|
||||
agent-browser doctor --fix # Also run destructive repairs
|
||||
agent-browser doctor --json # Structured output
|
||||
```
|
||||
|
||||
It checks:
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Category</th><th>What it checks</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>Environment</td><td>CLI version, platform, home directory, state and socket dirs, free disk space</td></tr>
|
||||
<tr><td>Chrome</td><td>Chrome install path and version, cache dir, Puppeteer fallback, user-data dir and profile count, optional <code>lightpanda</code> engine</td></tr>
|
||||
<tr><td>Daemons</td><td>Running daemons per session, stale <code>.sock</code> / <code>.pid</code> / <code>.version</code> / <code>.stream</code> files (auto-cleaned), version mismatch with the CLI, dashboard process liveness</td></tr>
|
||||
<tr><td>Config</td><td><code>~/.agent-browser/config.json</code>, <code>./agent-browser.json</code>, and any file at <code>AGENT_BROWSER_CONFIG</code> parse as valid JSON</td></tr>
|
||||
<tr><td>Security</td><td>Encryption key env var or <code>~/.agent-browser/.encryption-key</code> (with 0600 permissions on unix), state file count and age vs <code>AGENT_BROWSER_STATE_EXPIRE_DAYS</code>, action policy file</td></tr>
|
||||
<tr><td>Providers</td><td>Env vars for Browserless, Browserbase, Browser Use, Kernel, AgentCore (AWS creds), Appium (for <code>--provider ios</code>), and <code>AI_GATEWAY_API_KEY</code> for chat</td></tr>
|
||||
<tr><td>Network</td><td>Reachability of the Chrome for Testing CDN, AI Gateway (if configured), and any currently selected provider endpoint (skipped under <code>--offline</code>)</td></tr>
|
||||
<tr><td>Launch test</td><td>Spawns a scratch session, launches headless Chrome, navigates to <code>about:blank</code>, then closes. Measures wall time (skipped under <code>--quick</code>)</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
Stale sidecar files are always cleaned. Destructive actions are opt-in via `--fix`:
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Check</th><th>What <code>--fix</code> does</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>Chrome missing</td><td>Runs <code>agent-browser install</code></td></tr>
|
||||
<tr><td>Version-mismatched daemons</td><td>Sends <code>close</code> to each and cleans files</td></tr>
|
||||
<tr><td>Old state files</td><td>Deletes state files older than <code>AGENT_BROWSER_STATE_EXPIRE_DAYS</code> (default 30)</td></tr>
|
||||
<tr><td>Missing encryption key</td><td>Generates a new key at <code>~/.agent-browser/.encryption-key</code> (0600, unix); never overwrites an existing key</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
Exit code is `0` if all checks pass (warnings are fine), `1` if any fail.
|
||||
|
||||
## Custom browser
|
||||
|
||||
Use a custom browser executable instead of bundled Chromium:
|
||||
|
||||
- **Serverless** - Use `@sparticuz/chromium` (~50MB vs ~684MB)
|
||||
- **System browser** - Use existing Chrome installation
|
||||
- **Custom builds** - Use modified browser builds
|
||||
|
||||
```bash
|
||||
# Via flag
|
||||
agent-browser --executable-path /path/to/chromium open example.com
|
||||
|
||||
# Via environment variable
|
||||
AGENT_BROWSER_EXECUTABLE_PATH=/path/to/chromium agent-browser open example.com
|
||||
```
|
||||
|
||||
### Serverless example
|
||||
|
||||
Use `@sparticuz/chromium` or similar to obtain a Chromium executable path, then pass it via `--executable-path` or `AGENT_BROWSER_EXECUTABLE_PATH`.
|
||||
|
||||
## AI agent setup
|
||||
|
||||
agent-browser works with any AI agent out of the box. For richer context:
|
||||
|
||||
### AI coding assistants (recommended)
|
||||
|
||||
Install the skill for your AI coding assistant:
|
||||
|
||||
```bash
|
||||
npx skills add vercel-labs/agent-browser
|
||||
```
|
||||
|
||||
This works with Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, Goose, OpenCode, and Windsurf. The skill is fetched from the repository and stays up to date automatically.
|
||||
|
||||
> **Do not** copy `SKILL.md` from `node_modules` -- it will become stale as new features are added. Always use `npx skills add` or reference the repository version.
|
||||
|
||||
### AGENTS.md / CLAUDE.md
|
||||
|
||||
Add to your instructions file:
|
||||
|
||||
```markdown
|
||||
## Browser Automation
|
||||
|
||||
Use `agent-browser` for web automation. Run `agent-browser --help` for all commands.
|
||||
|
||||
Core workflow:
|
||||
1. `agent-browser open <url>` - Navigate to page
|
||||
2. `agent-browser snapshot -i` - Get interactive elements with refs (@e1, @e2)
|
||||
3. `agent-browser click @e1` / `fill @e2 "text"` - Interact using refs
|
||||
4. Re-snapshot after page changes
|
||||
```
|
||||
@@ -1,7 +0,0 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata";
|
||||
|
||||
export const metadata = pageMetadata("ios");
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return children;
|
||||
}
|
||||
@@ -1,207 +0,0 @@
|
||||
# iOS Simulator
|
||||
|
||||
Control real Mobile Safari in the iOS Simulator for authentic mobile
|
||||
web testing. Uses Appium with XCUITest for native automation.
|
||||
|
||||
## Requirements
|
||||
|
||||
- macOS with Xcode installed
|
||||
- iOS Simulator runtimes (download via Xcode)
|
||||
- Appium with XCUITest driver
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
# Install Appium globally
|
||||
npm install -g appium
|
||||
|
||||
# Install the XCUITest driver for iOS
|
||||
appium driver install xcuitest
|
||||
```
|
||||
|
||||
## List available devices
|
||||
|
||||
See all iOS simulators available on your system:
|
||||
|
||||
```bash
|
||||
agent-browser device list
|
||||
|
||||
# Output:
|
||||
# Available iOS Simulators:
|
||||
#
|
||||
# ○ iPhone 16 Pro (iOS 18.0)
|
||||
# F21EEC0D-7618-419F-811B-33AF27A8B2FD
|
||||
# ○ iPhone 16 Pro Max (iOS 18.0)
|
||||
# 50402807-C9B8-4D37-9F13-2E00E782C744
|
||||
# ○ iPad Pro 13-inch (M4) (iOS 18.0)
|
||||
# 3A6C6436-B909-4593-866D-91D1062BB070
|
||||
# ...
|
||||
```
|
||||
|
||||
## Basic usage
|
||||
|
||||
Use the `-p ios` flag to enable iOS mode. The workflow is
|
||||
identical to desktop:
|
||||
|
||||
```bash
|
||||
# Launch Safari on iPhone 16 Pro
|
||||
agent-browser -p ios --device "iPhone 16 Pro" open https://example.com
|
||||
|
||||
# Get snapshot with refs (same as desktop)
|
||||
agent-browser -p ios snapshot -i
|
||||
|
||||
# Interact using refs
|
||||
agent-browser -p ios tap @e1
|
||||
agent-browser -p ios fill @e2 "text"
|
||||
|
||||
# Take screenshot
|
||||
agent-browser -p ios screenshot mobile.png
|
||||
|
||||
# Close session (shuts down simulator)
|
||||
agent-browser -p ios close
|
||||
```
|
||||
|
||||
## Mobile-specific commands
|
||||
|
||||
```bash
|
||||
# Swipe gestures
|
||||
agent-browser -p ios swipe up
|
||||
agent-browser -p ios swipe down
|
||||
agent-browser -p ios swipe left
|
||||
agent-browser -p ios swipe right
|
||||
|
||||
# Swipe with distance (pixels)
|
||||
agent-browser -p ios swipe up 500
|
||||
|
||||
# Tap (alias for click, semantically clearer for touch)
|
||||
agent-browser -p ios tap @e1
|
||||
```
|
||||
|
||||
## Environment variables
|
||||
|
||||
Configure iOS mode via environment variables:
|
||||
|
||||
```bash
|
||||
export AGENT_BROWSER_PROVIDER=ios
|
||||
export AGENT_BROWSER_IOS_DEVICE="iPhone 16 Pro"
|
||||
|
||||
# Now all commands use iOS
|
||||
agent-browser open https://example.com
|
||||
agent-browser snapshot -i
|
||||
agent-browser tap @e1
|
||||
```
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Variable</th><th>Description</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><code>AGENT_BROWSER_PROVIDER</code></td><td>Set to <code>ios</code> to enable iOS mode</td></tr>
|
||||
<tr><td><code>AGENT_BROWSER_IOS_DEVICE</code></td><td>Device name (e.g., "iPhone 16 Pro")</td></tr>
|
||||
<tr><td><code>AGENT_BROWSER_IOS_UDID</code></td><td>Device UDID (alternative to device name)</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## Supported devices
|
||||
|
||||
All iOS Simulators available in Xcode are supported, including:
|
||||
|
||||
- All iPhone models (iPhone 15, 16, 17, SE, etc.)
|
||||
- All iPad models (iPad Pro, iPad Air, iPad mini, etc.)
|
||||
- Multiple iOS versions (17.x, 18.x, etc.)
|
||||
|
||||
**Real devices** are also supported via USB connection (see below).
|
||||
|
||||
## Real device support
|
||||
|
||||
Appium can control Safari on real iOS devices connected via USB. This
|
||||
requires additional one-time setup.
|
||||
|
||||
### 1. Get your device UDID
|
||||
|
||||
```bash
|
||||
# List connected devices
|
||||
xcrun xctrace list devices
|
||||
|
||||
# Or via system profiler
|
||||
system_profiler SPUSBDataType | grep -A 5 "iPhone\|iPad"
|
||||
```
|
||||
|
||||
### 2. Sign WebDriverAgent (one-time)
|
||||
|
||||
WebDriverAgent needs to be signed with your Apple Developer
|
||||
certificate to run on real devices.
|
||||
|
||||
```bash
|
||||
# Open the WebDriverAgent Xcode project
|
||||
cd ~/.appium/node_modules/appium-xcuitest-driver/node_modules/appium-webdriveragent
|
||||
open WebDriverAgent.xcodeproj
|
||||
```
|
||||
|
||||
In Xcode:
|
||||
|
||||
1. Select the `WebDriverAgentRunner` target
|
||||
2. Go to Signing & Capabilities
|
||||
3. Select your Team (requires Apple Developer account, free tier works)
|
||||
4. Let Xcode manage signing automatically
|
||||
|
||||
### 3. Use with agent-browser
|
||||
|
||||
```bash
|
||||
# Connect device via USB, then use the UDID
|
||||
agent-browser -p ios --device "<DEVICE_UDID>" open https://example.com
|
||||
|
||||
# Or use the device name if unique
|
||||
agent-browser -p ios --device "John's iPhone" open https://example.com
|
||||
```
|
||||
|
||||
### Real device notes
|
||||
|
||||
- First run installs WebDriverAgent to the device (may require Trust prompt on device)
|
||||
- Device must be unlocked and connected via USB
|
||||
- Slightly slower initial connection than simulator
|
||||
- Tests against real Safari performance and behavior
|
||||
- On first install, go to Settings → General → VPN & Device Management to trust the developer certificate
|
||||
|
||||
## Performance notes
|
||||
|
||||
- **First launch:** Takes 30-60 seconds to boot the simulator and start Appium
|
||||
- **Subsequent commands:** Fast (simulator stays running)
|
||||
- **Close command:** Shuts down simulator and Appium server
|
||||
|
||||
## Differences from desktop
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Feature</th><th>Desktop</th><th>iOS</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>Browser</td><td>Chrome, Lightpanda</td><td>Safari only</td></tr>
|
||||
<tr><td>Tabs</td><td>Supported</td><td>Single tab only</td></tr>
|
||||
<tr><td>PDF export</td><td>Supported</td><td>Not supported</td></tr>
|
||||
<tr><td>Screencast</td><td>Supported</td><td>Not supported</td></tr>
|
||||
<tr><td>Swipe gestures</td><td>Not native</td><td>Native support</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Appium not found
|
||||
|
||||
```bash
|
||||
# Make sure Appium is installed globally
|
||||
npm install -g appium
|
||||
appium driver install xcuitest
|
||||
|
||||
# Verify installation
|
||||
appium --version
|
||||
```
|
||||
|
||||
### No simulators available
|
||||
|
||||
Open Xcode and download iOS Simulator runtimes from **Settings → Platforms**.
|
||||
|
||||
### Simulator won't boot
|
||||
|
||||
Try booting the simulator manually from Xcode or the Simulator app to
|
||||
ensure it works, then retry with agent-browser.
|
||||
@@ -1,93 +0,0 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Inter, Geist_Mono } from "next/font/google";
|
||||
import { GeistPixelSquare } from "geist/font/pixel";
|
||||
import "./globals.css";
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
import { Header } from "@/components/header";
|
||||
import { DocsSidebar } from "@/components/docs-sidebar";
|
||||
import { DocsMobileNav } from "@/components/docs-mobile-nav";
|
||||
import { CopyPageButton } from "@/components/copy-page-button";
|
||||
import { DocsChat } from "@/components/docs-chat";
|
||||
import { cookies } from "next/headers";
|
||||
import { SpeedInsights } from "@vercel/speed-insights/next";
|
||||
import { Analytics } from "@vercel/analytics/next";
|
||||
|
||||
const inter = Inter({
|
||||
variable: "--font-inter",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL("https://agent-browser.dev"),
|
||||
title: {
|
||||
default: "agent-browser | Browser Automation for AI",
|
||||
template: "%s | agent-browser",
|
||||
},
|
||||
description: "Browser automation CLI for AI agents",
|
||||
openGraph: {
|
||||
type: "website",
|
||||
locale: "en_US",
|
||||
url: "https://agent-browser.dev",
|
||||
siteName: "agent-browser",
|
||||
title: "agent-browser | Browser Automation for AI",
|
||||
description: "Browser automation CLI for AI agents",
|
||||
images: [{ url: "/og", width: 1200, height: 630, alt: "agent-browser" }],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: "agent-browser | Browser Automation for AI",
|
||||
description: "Browser automation CLI for AI agents",
|
||||
images: ["/og"],
|
||||
},
|
||||
};
|
||||
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const cookieStore = await cookies();
|
||||
const chatOpen = cookieStore.get("docs-chat-open")?.value === "true";
|
||||
const chatWidth = Number(cookieStore.get("docs-chat-width")?.value) || 400;
|
||||
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<head>
|
||||
{chatOpen && (
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `@media(min-width:640px){body{padding-right:${chatWidth}px}}`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</head>
|
||||
<body
|
||||
className={`${inter.variable} ${geistMono.variable} ${GeistPixelSquare.variable} bg-white text-neutral-900 antialiased dark:bg-neutral-950 dark:text-neutral-100`}
|
||||
>
|
||||
<ThemeProvider>
|
||||
<Header />
|
||||
<DocsMobileNav />
|
||||
<div className="max-w-5xl mx-auto px-6 py-8 lg:py-12 flex gap-16">
|
||||
<aside className="w-48 shrink-0 hidden lg:block sticky top-28 h-[calc(100vh-7rem)] overflow-y-auto">
|
||||
<DocsSidebar />
|
||||
</aside>
|
||||
<div className="flex-1 min-w-0 max-w-2xl pb-20">
|
||||
<div className="flex justify-end mb-4">
|
||||
<CopyPageButton />
|
||||
</div>
|
||||
<article className="prose">{children}</article>
|
||||
</div>
|
||||
</div>
|
||||
<DocsChat defaultOpen={chatOpen} defaultWidth={chatWidth} />
|
||||
</ThemeProvider>
|
||||
<SpeedInsights />
|
||||
<Analytics />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata";
|
||||
|
||||
export const metadata = pageMetadata("native-mode");
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return children;
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
# Native Mode
|
||||
|
||||
agent-browser is now 100% native Rust by default. The Node.js/Playwright daemon has been removed.
|
||||
|
||||
This page is no longer relevant. See the main [documentation](/) for current architecture and usage.
|
||||
@@ -1,7 +0,0 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata";
|
||||
|
||||
export const metadata = pageMetadata("next");
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return children;
|
||||
}
|
||||
@@ -1,207 +0,0 @@
|
||||
# Next.js + Vercel
|
||||
|
||||
Run agent-browser from a Next.js app on Vercel using Vercel Sandbox.
|
||||
A Linux microVM spins up on demand, runs agent-browser + Chrome, and
|
||||
shuts down. No binary size limits, no Chromium bundling complexity.
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
pnpm add @vercel/sandbox
|
||||
```
|
||||
|
||||
## Server action
|
||||
|
||||
The Vercel Sandbox runs Amazon Linux. Chromium requires system libraries
|
||||
that are not installed by default, so fresh sandboxes need a `dnf install`
|
||||
step before agent-browser can launch Chrome. Use a sandbox snapshot
|
||||
(below) to skip this entirely in production.
|
||||
|
||||
```ts
|
||||
"use server";
|
||||
import { Sandbox } from "@vercel/sandbox";
|
||||
|
||||
const snapshotId = process.env.AGENT_BROWSER_SNAPSHOT_ID;
|
||||
|
||||
const CHROMIUM_SYSTEM_DEPS = [
|
||||
"nss", "nspr", "libxkbcommon", "atk", "at-spi2-atk", "at-spi2-core",
|
||||
"libXcomposite", "libXdamage", "libXrandr", "libXfixes", "libXcursor",
|
||||
"libXi", "libXtst", "libXScrnSaver", "libXext", "mesa-libgbm", "libdrm",
|
||||
"mesa-libGL", "mesa-libEGL", "cups-libs", "alsa-lib", "pango", "cairo",
|
||||
"gtk3", "dbus-libs",
|
||||
];
|
||||
|
||||
function getSandboxCredentials() {
|
||||
if (
|
||||
process.env.VERCEL_TOKEN &&
|
||||
process.env.VERCEL_TEAM_ID &&
|
||||
process.env.VERCEL_PROJECT_ID
|
||||
) {
|
||||
return {
|
||||
token: process.env.VERCEL_TOKEN,
|
||||
teamId: process.env.VERCEL_TEAM_ID,
|
||||
projectId: process.env.VERCEL_PROJECT_ID,
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
async function withBrowser<T>(
|
||||
fn: (sandbox: InstanceType<typeof Sandbox>) => Promise<T>,
|
||||
): Promise<T> {
|
||||
const credentials = getSandboxCredentials();
|
||||
|
||||
const sandbox = snapshotId
|
||||
? await Sandbox.create({
|
||||
...credentials,
|
||||
source: { type: "snapshot", snapshotId },
|
||||
timeout: 120_000,
|
||||
})
|
||||
: await Sandbox.create({ ...credentials, runtime: "node24", timeout: 120_000 });
|
||||
|
||||
if (!snapshotId) {
|
||||
await sandbox.runCommand("sh", [
|
||||
"-c",
|
||||
`sudo dnf clean all 2>&1 && sudo dnf install -y --skip-broken ${CHROMIUM_SYSTEM_DEPS.join(" ")} 2>&1 && sudo ldconfig 2>&1`,
|
||||
]);
|
||||
await sandbox.runCommand("npm", ["install", "-g", "agent-browser"]);
|
||||
await sandbox.runCommand("npx", ["agent-browser", "install"]);
|
||||
}
|
||||
|
||||
try {
|
||||
return await fn(sandbox);
|
||||
} finally {
|
||||
await sandbox.stop();
|
||||
}
|
||||
}
|
||||
|
||||
export async function screenshotUrl(url: string) {
|
||||
return withBrowser(async (sandbox) => {
|
||||
await sandbox.runCommand("agent-browser", ["open", url]);
|
||||
|
||||
const ssResult = await sandbox.runCommand("agent-browser", [
|
||||
"screenshot", "--json",
|
||||
]);
|
||||
const ssPath = JSON.parse(await ssResult.stdout())?.data?.path;
|
||||
const b64Result = await sandbox.runCommand("base64", ["-w", "0", ssPath]);
|
||||
const screenshot = (await b64Result.stdout()).trim();
|
||||
|
||||
await sandbox.runCommand("agent-browser", ["close"]);
|
||||
return { ok: true, screenshot };
|
||||
});
|
||||
}
|
||||
|
||||
export async function snapshotUrl(url: string) {
|
||||
return withBrowser(async (sandbox) => {
|
||||
await sandbox.runCommand("agent-browser", ["open", url]);
|
||||
|
||||
const result = await sandbox.runCommand("agent-browser", [
|
||||
"snapshot", "-i", "-c",
|
||||
]);
|
||||
const snapshot = await result.stdout();
|
||||
|
||||
await sandbox.runCommand("agent-browser", ["close"]);
|
||||
return { ok: true, snapshot };
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Sandbox snapshots
|
||||
|
||||
Without optimization, each Sandbox run installs system dependencies +
|
||||
agent-browser + Chromium from scratch (~30 seconds). A **sandbox snapshot**
|
||||
is a saved VM image with everything pre-installed -- like a Docker image
|
||||
for Vercel Sandbox. When `AGENT_BROWSER_SNAPSHOT_ID` is set, the sandbox
|
||||
boots from that image instead of installing, bringing startup down to
|
||||
sub-second.
|
||||
|
||||
This is different from an agent-browser *accessibility snapshot* (which
|
||||
dumps a page's accessibility tree). A sandbox snapshot is a Vercel
|
||||
infrastructure concept.
|
||||
|
||||
Create a sandbox snapshot by running the helper script once:
|
||||
|
||||
```bash
|
||||
npx tsx scripts/create-snapshot.ts
|
||||
```
|
||||
|
||||
The script spins up a fresh sandbox, installs system dependencies +
|
||||
agent-browser + Chromium, saves the VM state, and prints the snapshot ID:
|
||||
|
||||
```
|
||||
AGENT_BROWSER_SNAPSHOT_ID=snap_xxxxxxxxxxxx
|
||||
```
|
||||
|
||||
Add this to your Vercel project environment variables (or `.env.local`
|
||||
for local development). Recommended for any production deployment.
|
||||
|
||||
## Authentication
|
||||
|
||||
On Vercel deployments, the Sandbox SDK authenticates automatically via
|
||||
OIDC. For local development, provide explicit credentials:
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Variable</th><th>Description</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><code>VERCEL_TOKEN</code></td><td>Vercel personal access token</td></tr>
|
||||
<tr><td><code>VERCEL_TEAM_ID</code></td><td>Vercel team ID</td></tr>
|
||||
<tr><td><code>VERCEL_PROJECT_ID</code></td><td>Vercel project ID</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
When all three are set, they are passed to `Sandbox.create()`. When
|
||||
absent, the SDK falls back to `VERCEL_OIDC_TOKEN` (automatic on Vercel).
|
||||
|
||||
## Scheduled workflows (cron)
|
||||
|
||||
For recurring tasks like daily monitoring, use Vercel Cron Jobs:
|
||||
|
||||
```ts
|
||||
// app/api/cron/monitor/route.ts
|
||||
export async function GET() {
|
||||
const result = await withBrowser(async (sandbox) => {
|
||||
await sandbox.runCommand("agent-browser", [
|
||||
"open", "https://example.com/pricing",
|
||||
]);
|
||||
const snap = await sandbox.runCommand("agent-browser", [
|
||||
"snapshot", "-i", "-c",
|
||||
]);
|
||||
await sandbox.runCommand("agent-browser", ["close"]);
|
||||
return await snap.stdout();
|
||||
});
|
||||
|
||||
// Process results, send alerts, store data...
|
||||
return Response.json({ ok: true, snapshot: result });
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
// vercel.json
|
||||
{
|
||||
"crons": [
|
||||
{ "path": "/api/cron/monitor", "schedule": "0 9 * * *" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Environment variables
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Variable</th><th>Description</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><code>AGENT_BROWSER_SNAPSHOT_ID</code></td><td>Sandbox snapshot ID for sub-second startup (see above)</td></tr>
|
||||
<tr><td><code>VERCEL_TOKEN</code></td><td>Vercel personal access token (for local dev; OIDC is automatic on Vercel)</td></tr>
|
||||
<tr><td><code>VERCEL_TEAM_ID</code></td><td>Vercel team ID (for local dev)</td></tr>
|
||||
<tr><td><code>VERCEL_PROJECT_ID</code></td><td>Vercel project ID (for local dev)</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## Demo app
|
||||
|
||||
A working demo with streaming progress UI, rate limiting, and a
|
||||
deploy-to-Vercel button is at
|
||||
[`examples/environments/`](https://github.com/vercel-labs/agent-browser/tree/main/examples/environments).
|
||||
@@ -1,16 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getPageTitle, renderOgImage } from "../og-image";
|
||||
|
||||
export async function GET(
|
||||
_request: Request,
|
||||
{ params }: { params: Promise<{ slug: string[] }> },
|
||||
) {
|
||||
const { slug } = await params;
|
||||
const title = getPageTitle(slug.join("/"));
|
||||
|
||||
if (!title) {
|
||||
return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
return renderOgImage(title);
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
import { ImageResponse } from "next/og";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
export { getPageTitle } from "@/lib/page-titles";
|
||||
|
||||
let fontCache: { geistRegular: Buffer; geistPixelSquare: Buffer } | null =
|
||||
null;
|
||||
|
||||
async function loadFonts() {
|
||||
if (fontCache) return fontCache;
|
||||
const [geistRegular, geistPixelSquare] = await Promise.all([
|
||||
readFile(join(process.cwd(), "public/Geist-Regular.ttf")),
|
||||
readFile(join(process.cwd(), "public/GeistPixel-Square.ttf")),
|
||||
]);
|
||||
fontCache = { geistRegular, geistPixelSquare };
|
||||
return fontCache;
|
||||
}
|
||||
|
||||
export async function renderOgImage(title: string) {
|
||||
const { geistRegular, geistPixelSquare } = await loadFonts();
|
||||
|
||||
return new ImageResponse(
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
backgroundColor: "black",
|
||||
padding: "60px 80px",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "16px",
|
||||
}}
|
||||
>
|
||||
<svg width="36" height="36" viewBox="0 0 16 16" fill="white">
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M8 1L16 15H0L8 1Z" />
|
||||
</svg>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 36,
|
||||
color: "#666",
|
||||
fontFamily: "Geist",
|
||||
fontWeight: 400,
|
||||
}}
|
||||
>
|
||||
/
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 36,
|
||||
fontFamily: "GeistPixelSquare",
|
||||
fontWeight: 400,
|
||||
color: "white",
|
||||
}}
|
||||
>
|
||||
agent-browser
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flex: 1,
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
{title.split("\n").map((line, i) => (
|
||||
<span
|
||||
key={i}
|
||||
style={{
|
||||
fontSize: 72,
|
||||
fontFamily: "Geist",
|
||||
fontWeight: 400,
|
||||
color: "white",
|
||||
letterSpacing: "-0.02em",
|
||||
textAlign: "center",
|
||||
lineHeight: 1.2,
|
||||
}}
|
||||
>
|
||||
{line}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>,
|
||||
{
|
||||
width: 1200,
|
||||
height: 630,
|
||||
fonts: [
|
||||
{
|
||||
name: "Geist",
|
||||
data: geistRegular.buffer as ArrayBuffer,
|
||||
style: "normal",
|
||||
weight: 400,
|
||||
},
|
||||
{
|
||||
name: "GeistPixelSquare",
|
||||
data: geistPixelSquare.buffer as ArrayBuffer,
|
||||
style: "normal",
|
||||
weight: 400,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { getPageTitle, renderOgImage } from "./og-image";
|
||||
|
||||
export async function GET() {
|
||||
const title = getPageTitle("")!;
|
||||
return renderOgImage(title);
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
# agent-browser
|
||||
|
||||
Browser automation CLI designed for AI agents. Compact text output minimizes context usage. 100% native Rust.
|
||||
|
||||
```bash
|
||||
npm install -g agent-browser # all platforms
|
||||
brew install agent-browser # macOS
|
||||
agent-browser install # Download Chrome (first time)
|
||||
|
||||
# or try without installing
|
||||
npx agent-browser open example.com
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- **Agent-first** - Compact text output uses fewer tokens than JSON, designed for AI context efficiency
|
||||
- **Ref-based** - Snapshot returns accessibility tree with refs for deterministic element selection
|
||||
- **Fast** - Native Rust CLI for instant command parsing
|
||||
- **Complete** - 50+ commands for navigation, forms, screenshots, network, storage
|
||||
- **Sessions** - Multiple isolated browser instances with separate auth
|
||||
- **Cross-platform** - macOS, Linux, Windows with native binaries
|
||||
|
||||
## Works with
|
||||
|
||||
Claude Code, Cursor, GitHub Copilot, OpenAI Codex, Google Gemini, opencode, and any agent that can run shell commands.
|
||||
|
||||
## Example
|
||||
|
||||
```bash
|
||||
# Navigate and get snapshot
|
||||
agent-browser open example.com
|
||||
agent-browser snapshot -i
|
||||
|
||||
# Output:
|
||||
# - heading "Example Domain" [ref=e1]
|
||||
# - link "More information..." [ref=e2]
|
||||
|
||||
# Interact using refs
|
||||
agent-browser click @e2
|
||||
agent-browser screenshot page.png
|
||||
agent-browser close
|
||||
```
|
||||
|
||||
## Why refs?
|
||||
|
||||
The `snapshot` command returns a compact accessibility tree where each element
|
||||
has a unique ref like `@e1`, `@e2`. This provides:
|
||||
|
||||
- **Context-efficient** - Text output uses ~200-400 tokens vs ~3000-5000 for full DOM
|
||||
- **Deterministic** - Ref points to exact element from snapshot
|
||||
- **Fast** - No DOM re-query needed
|
||||
- **AI-friendly** - LLMs parse text output naturally
|
||||
|
||||
## Architecture
|
||||
|
||||
Client-daemon architecture for optimal performance:
|
||||
|
||||
1. **Rust CLI** - Parses commands, communicates with daemon
|
||||
2. **Native Daemon** - Pure Rust daemon using direct CDP, manages Chrome via Chrome DevTools Protocol
|
||||
|
||||
Daemon starts automatically and persists between commands.
|
||||
|
||||
## Platforms
|
||||
|
||||
Native Rust binaries for macOS (ARM64, x64), Linux (ARM64, x64), and Windows (x64).
|
||||
@@ -1,7 +0,0 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata";
|
||||
|
||||
export const metadata = pageMetadata("profiler");
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return children;
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
# Profiler
|
||||
|
||||
Capture Chrome DevTools performance profiles during browser automation.
|
||||
Use profiles to diagnose slow page loads, expensive JavaScript, layout thrashing,
|
||||
and other performance bottlenecks in agentic workflows.
|
||||
|
||||
## Basic usage
|
||||
|
||||
```bash
|
||||
# Start profiling
|
||||
agent-browser profiler start
|
||||
|
||||
# Perform actions
|
||||
agent-browser navigate https://example.com
|
||||
agent-browser click "#button"
|
||||
|
||||
# Stop and save profile
|
||||
agent-browser profiler stop ./trace.json
|
||||
```
|
||||
|
||||
The output JSON file can be loaded into Chrome DevTools, Perfetto UI, or any
|
||||
tool that accepts Chrome Trace Event format.
|
||||
|
||||
## Commands
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Command</th><th>Description</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><code>profiler start</code></td><td>Start recording a performance profile</td></tr>
|
||||
<tr><td><code>profiler start --categories <list></code></td><td>Start with custom trace categories</td></tr>
|
||||
<tr><td><code>profiler stop [path]</code></td><td>Stop profiling and save to file</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## Trace categories
|
||||
|
||||
The `--categories` flag accepts a comma-separated list of Chrome trace categories.
|
||||
|
||||
```bash
|
||||
agent-browser profiler start --categories "devtools.timeline,v8.execute,blink.user_timing"
|
||||
```
|
||||
|
||||
Default categories include `devtools.timeline`, `v8.execute`, `blink`,
|
||||
`blink.user_timing`, `latencyInfo`, `renderer.scheduler`, `toplevel`, and
|
||||
several `disabled-by-default-*` categories for detailed CPU profiling and
|
||||
call stack analysis.
|
||||
|
||||
### Common categories
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Category</th><th>What it captures</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><code>devtools.timeline</code></td><td>Standard DevTools performance events</td></tr>
|
||||
<tr><td><code>v8.execute</code></td><td>Time spent running JavaScript</td></tr>
|
||||
<tr><td><code>blink</code></td><td>Renderer events (layout, paint, style)</td></tr>
|
||||
<tr><td><code>blink.user_timing</code></td><td><code>performance.mark()</code> and <code>performance.measure()</code> calls</td></tr>
|
||||
<tr><td><code>latencyInfo</code></td><td>Input-to-display latency</td></tr>
|
||||
<tr><td><code>disabled-by-default-v8.cpu_profiler</code></td><td>Sampling-based JS CPU profiling</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## Output format
|
||||
|
||||
The output is a JSON file in Chrome Trace Event format:
|
||||
|
||||
```json
|
||||
{
|
||||
"traceEvents": [
|
||||
{
|
||||
"cat": "devtools.timeline",
|
||||
"name": "RunTask",
|
||||
"ph": "X",
|
||||
"ts": 12345,
|
||||
"dur": 100,
|
||||
"pid": 1,
|
||||
"tid": 1
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"clock-domain": "LINUX_CLOCK_MONOTONIC"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `metadata.clock-domain` field reflects the host platform (Linux or macOS).
|
||||
On Windows it is omitted.
|
||||
|
||||
## Viewing profiles
|
||||
|
||||
- **Chrome DevTools** -- Performance panel > Load profile
|
||||
- **Perfetto** -- https://ui.perfetto.dev/ (drag and drop the JSON file)
|
||||
- **Trace Viewer** -- `chrome://tracing` in any Chromium browser
|
||||
|
||||
## Use cases
|
||||
|
||||
- **Page load analysis** -- Profile navigation to identify slow resources, long tasks, or layout shifts
|
||||
- **Interaction profiling** -- Measure the cost of clicks, form fills, and other user interactions
|
||||
- **CI regression checks** -- Capture profiles per build and compare trace data over time
|
||||
- **Agent workflow optimization** -- Find which steps in an agentic flow are most expensive
|
||||
|
||||
## Limitations
|
||||
|
||||
- Only works with Chromium-based browsers (Chrome, Edge). Not supported on Firefox or WebKit.
|
||||
- Trace data accumulates in memory while profiling is active (capped at 5 million events). Stop profiling promptly after the area of interest.
|
||||
- Data collection on stop has a 30-second timeout. If the browser is unresponsive, the stop command may fail.
|
||||
- When no output path is provided, the profile is saved to an auto-generated path under the agent-browser temp directory.
|
||||
@@ -1,7 +0,0 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata";
|
||||
|
||||
export const metadata = pageMetadata("providers/agentcore");
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return children;
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
# AgentCore
|
||||
|
||||
[AWS Bedrock AgentCore](https://aws.amazon.com/bedrock/agentcore/) provides cloud browser sessions with SigV4 authentication. Use it when running agent-browser in AWS environments or when you need managed cloud browsers backed by AWS infrastructure.
|
||||
|
||||
## Setup
|
||||
|
||||
Credentials are automatically resolved from:
|
||||
|
||||
1. Environment variables (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`)
|
||||
2. AWS CLI (`aws configure export-credentials`) which supports SSO, profiles, IAM roles, etc.
|
||||
|
||||
```bash
|
||||
agent-browser -p agentcore open https://example.com
|
||||
```
|
||||
|
||||
Or use environment variables for CI/scripts:
|
||||
|
||||
```bash
|
||||
export AGENT_BROWSER_PROVIDER=agentcore
|
||||
agent-browser open https://example.com
|
||||
```
|
||||
|
||||
The `-p` flag takes precedence over `AGENT_BROWSER_PROVIDER`.
|
||||
|
||||
## Configuration
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Variable</th><th>Description</th><th>Default</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><code>AGENTCORE_REGION</code></td><td>AWS region for the AgentCore endpoint</td><td><code>us-east-1</code></td></tr>
|
||||
<tr><td><code>AGENTCORE_BROWSER_ID</code></td><td>Browser identifier</td><td><code>aws.browser.v1</code></td></tr>
|
||||
<tr><td><code>AGENTCORE_PROFILE_ID</code></td><td>Browser profile for persistent state (cookies, localStorage)</td><td>(none)</td></tr>
|
||||
<tr><td><code>AGENTCORE_SESSION_TIMEOUT</code></td><td>Session timeout in seconds</td><td><code>3600</code></td></tr>
|
||||
<tr><td><code>AWS_PROFILE</code></td><td>AWS CLI profile for credential resolution</td><td><code>default</code></td></tr>
|
||||
<tr><td><code>AWS_ACCESS_KEY_ID</code></td><td>AWS access key (checked before AWS CLI fallback)</td><td>(none)</td></tr>
|
||||
<tr><td><code>AWS_SECRET_ACCESS_KEY</code></td><td>AWS secret key</td><td>(none)</td></tr>
|
||||
<tr><td><code>AWS_SESSION_TOKEN</code></td><td>Temporary session token (for STS/SSO credentials)</td><td>(none)</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## Browser Profiles
|
||||
|
||||
Use `AGENTCORE_PROFILE_ID` to persist browser state (cookies, localStorage) across sessions:
|
||||
|
||||
```bash
|
||||
AGENTCORE_PROFILE_ID=my-profile agent-browser -p agentcore open https://example.com
|
||||
```
|
||||
|
||||
When a profile is set, AgentCore stores and restores browser state automatically between sessions.
|
||||
|
||||
## Live View
|
||||
|
||||
When a session starts, AgentCore prints a Live View URL to stderr:
|
||||
|
||||
```
|
||||
Session: abc123-def456
|
||||
Live View: https://us-east-1.console.aws.amazon.com/bedrock-agentcore/browser/aws.browser.v1/session/abc123-def456#
|
||||
```
|
||||
|
||||
Open this URL in your browser to watch the agent session in real time from the AWS Console.
|
||||
|
||||
## Credential Resolution
|
||||
|
||||
AgentCore uses lightweight manual SigV4 signing (no AWS SDK dependency). Credentials are resolved in order:
|
||||
|
||||
1. **Environment variables** (`AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY`, optionally `AWS_SESSION_TOKEN`)
|
||||
2. **AWS CLI** (`aws configure export-credentials --format env`), which supports SSO, IAM roles, credential files, and profiles
|
||||
|
||||
If using SSO, run `aws sso login` before launching agent-browser. Set `AWS_PROFILE` to select a specific named profile.
|
||||
|
||||
## Example
|
||||
|
||||
```bash
|
||||
# Basic usage (credentials auto-resolved via AWS CLI)
|
||||
agent-browser -p agentcore open https://example.com
|
||||
|
||||
# With a browser profile for persistent login state
|
||||
AGENTCORE_PROFILE_ID=my-profile agent-browser -p agentcore open https://x.com/home
|
||||
|
||||
# With explicit region
|
||||
AGENTCORE_REGION=eu-west-1 agent-browser -p agentcore open https://example.com
|
||||
|
||||
# With SSO profile
|
||||
AWS_PROFILE=my-sso-profile agent-browser -p agentcore open https://example.com
|
||||
```
|
||||
|
||||
When enabled, agent-browser connects to an AgentCore cloud browser session instead of launching a local browser. All commands work identically.
|
||||
@@ -1,7 +0,0 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata";
|
||||
|
||||
export const metadata = pageMetadata("providers/browser-use");
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return children;
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
# Browser Use
|
||||
|
||||
[Browser Use](https://browser-use.com) provides cloud browser infrastructure for AI agents. Use it when running agent-browser in environments where a local browser isn't available (serverless, CI/CD, etc.).
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
export BROWSER_USE_API_KEY="your-api-key"
|
||||
agent-browser -p browseruse open https://example.com
|
||||
```
|
||||
|
||||
Or use environment variables for CI/scripts:
|
||||
|
||||
```bash
|
||||
export AGENT_BROWSER_PROVIDER=browseruse
|
||||
export BROWSER_USE_API_KEY="your-api-key"
|
||||
agent-browser open https://example.com
|
||||
```
|
||||
|
||||
The `-p` flag takes precedence over `AGENT_BROWSER_PROVIDER`.
|
||||
|
||||
When enabled, agent-browser connects to a Browser Use cloud session instead of launching a local browser. All commands work identically.
|
||||
|
||||
Get your API key from the [Browser Use Cloud Dashboard](https://cloud.browser-use.com/settings?tab=api-keys). Free credits are available to get started, with pay-as-you-go pricing after.
|
||||
@@ -1,7 +0,0 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata";
|
||||
|
||||
export const metadata = pageMetadata("providers/browserbase");
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return children;
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
# Browserbase
|
||||
|
||||
[Browserbase](https://browserbase.com) provides remote browser infrastructure to make deployment of agentic browsing agents easy. Use it when running agent-browser in environments where a local browser isn't feasible.
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
export BROWSERBASE_API_KEY="your-api-key"
|
||||
agent-browser -p browserbase open https://example.com
|
||||
```
|
||||
|
||||
Or use environment variables for CI/scripts:
|
||||
|
||||
```bash
|
||||
export AGENT_BROWSER_PROVIDER=browserbase
|
||||
export BROWSERBASE_API_KEY="your-api-key"
|
||||
agent-browser open https://example.com
|
||||
```
|
||||
|
||||
The `-p` flag takes precedence over `AGENT_BROWSER_PROVIDER`.
|
||||
|
||||
When enabled, agent-browser connects to a Browserbase session instead of launching a local browser. All commands work identically.
|
||||
|
||||
Get your API key from the [Browserbase Dashboard](https://browserbase.com/overview).
|
||||
@@ -1,7 +0,0 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata";
|
||||
|
||||
export const metadata = pageMetadata("providers/browserless");
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return children;
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
# Browserless
|
||||
|
||||
[Browserless](https://browserless.io) provides cloud browser infrastructure with a Sessions API. Use it when running agent-browser in environments where a local browser isn't available.
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
export BROWSERLESS_API_KEY="your-api-token"
|
||||
agent-browser -p browserless open https://example.com
|
||||
```
|
||||
|
||||
Or use environment variables for CI/scripts:
|
||||
|
||||
```bash
|
||||
export AGENT_BROWSER_PROVIDER=browserless
|
||||
export BROWSERLESS_API_KEY="your-api-token"
|
||||
agent-browser open https://example.com
|
||||
```
|
||||
|
||||
The `-p` flag takes precedence over `AGENT_BROWSER_PROVIDER`.
|
||||
|
||||
## Configuration
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Variable</th><th>Description</th><th>Default</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><code>BROWSERLESS_API_KEY</code></td><td>API token (required)</td><td></td></tr>
|
||||
<tr><td><code>BROWSERLESS_API_URL</code></td><td>Base API URL (for custom regions or self-hosted)</td><td><code>https://production-sfo.browserless.io</code></td></tr>
|
||||
<tr><td><code>BROWSERLESS_BROWSER_TYPE</code></td><td>Type of browser to use (<code>chromium</code> or <code>chrome</code>)</td><td><code>chromium</code></td></tr>
|
||||
<tr><td><code>BROWSERLESS_TTL</code></td><td>Session TTL in milliseconds</td><td><code>300000</code></td></tr>
|
||||
<tr><td><code>BROWSERLESS_STEALTH</code></td><td>Enable stealth mode</td><td><code>true</code></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
When enabled, agent-browser connects to a Browserless cloud session instead of launching a local browser. All commands work identically.
|
||||
|
||||
Get your API token from the [Browserless Dashboard](https://browserless.io).
|
||||
@@ -1,7 +0,0 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata";
|
||||
|
||||
export const metadata = pageMetadata("providers/kernel");
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return children;
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
# Kernel
|
||||
|
||||
[Kernel](https://www.kernel.sh) provides cloud browser infrastructure for AI agents with features like stealth mode and persistent profiles.
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
export KERNEL_API_KEY="your-api-key"
|
||||
agent-browser -p kernel open https://example.com
|
||||
```
|
||||
|
||||
Or use environment variables for CI/scripts:
|
||||
|
||||
```bash
|
||||
export AGENT_BROWSER_PROVIDER=kernel
|
||||
export KERNEL_API_KEY="your-api-key"
|
||||
agent-browser open https://example.com
|
||||
```
|
||||
|
||||
The `-p` flag takes precedence over `AGENT_BROWSER_PROVIDER`.
|
||||
|
||||
## Configuration
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Variable</th><th>Description</th><th>Default</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><code>KERNEL_API_KEY</code></td><td>API key (required)</td><td></td></tr>
|
||||
<tr><td><code>KERNEL_HEADLESS</code></td><td>Run browser in headless mode</td><td><code>true</code></td></tr>
|
||||
<tr><td><code>KERNEL_STEALTH</code></td><td>Enable stealth mode to avoid bot detection</td><td><code>false</code></td></tr>
|
||||
<tr><td><code>KERNEL_TIMEOUT_SECONDS</code></td><td>Session timeout in seconds</td><td><code>300</code></td></tr>
|
||||
<tr><td><code>KERNEL_PROFILE_NAME</code></td><td>Browser profile name for persistent cookies/logins</td><td>(none)</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
**Profile persistence:** When `KERNEL_PROFILE_NAME` is set, the profile will be created if it doesn't already exist. Cookies, logins, and session data are automatically saved back to the profile when the browser session ends, making them available for future sessions.
|
||||
|
||||
When enabled, agent-browser connects to a Kernel cloud session instead of launching a local browser. All commands work identically.
|
||||
|
||||
Get your API key from the [Kernel Dashboard](https://dashboard.onkernel.com).
|
||||
@@ -1,7 +0,0 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata";
|
||||
|
||||
export const metadata = pageMetadata("quick-start");
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return children;
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
# Quick Start
|
||||
|
||||
## Core workflow
|
||||
|
||||
Every browser automation follows this pattern:
|
||||
|
||||
```bash
|
||||
# 1. Navigate
|
||||
agent-browser open example.com
|
||||
|
||||
# 2. Snapshot to get element refs
|
||||
agent-browser snapshot -i
|
||||
# Output:
|
||||
# @e1 [heading] "Example Domain"
|
||||
# @e2 [link] "More information..."
|
||||
|
||||
# 3. Interact using refs
|
||||
agent-browser click @e2
|
||||
|
||||
# 4. Re-snapshot after page changes
|
||||
agent-browser snapshot -i
|
||||
```
|
||||
|
||||
## Common commands
|
||||
|
||||
```bash
|
||||
agent-browser open example.com
|
||||
agent-browser snapshot -i # Get interactive elements with refs
|
||||
agent-browser click @e2 # Click by ref
|
||||
agent-browser fill @e3 "test@example.com" # Fill input by ref
|
||||
agent-browser get text @e1 # Get text content
|
||||
agent-browser screenshot # Save to temp directory
|
||||
agent-browser screenshot page.png # Save to specific path
|
||||
agent-browser close
|
||||
```
|
||||
|
||||
## Traditional selectors
|
||||
|
||||
CSS selectors and semantic locators also supported:
|
||||
|
||||
```bash
|
||||
agent-browser click "#submit"
|
||||
agent-browser fill "#email" "test@example.com"
|
||||
agent-browser find role button click --name "Submit"
|
||||
```
|
||||
|
||||
## Headed mode
|
||||
|
||||
Show browser window for debugging:
|
||||
|
||||
```bash
|
||||
agent-browser open example.com --headed
|
||||
```
|
||||
|
||||
## Wait for content
|
||||
|
||||
```bash
|
||||
agent-browser wait @e1 # Wait for element
|
||||
agent-browser wait --load networkidle # Wait for network idle
|
||||
agent-browser wait --url "**/dashboard" # Wait for URL pattern
|
||||
agent-browser wait 2000 # Wait milliseconds
|
||||
```
|
||||
|
||||
## Command chaining
|
||||
|
||||
Chain commands with `&&` in a single shell call. The browser persists via a background daemon, so chaining is safe and efficient:
|
||||
|
||||
```bash
|
||||
# Open, wait, and snapshot in one call
|
||||
agent-browser open example.com && agent-browser wait --load networkidle && agent-browser snapshot -i
|
||||
|
||||
# Chain multiple interactions
|
||||
agent-browser fill @e1 "user@example.com" && agent-browser fill @e2 "pass" && agent-browser click @e3
|
||||
|
||||
# Navigate and capture
|
||||
agent-browser open example.com && agent-browser wait --load networkidle && agent-browser screenshot page.png
|
||||
```
|
||||
|
||||
Use `&&` when you don't need intermediate output. Run commands separately when you need to parse output first (e.g., snapshot to discover refs before interacting).
|
||||
|
||||
## JSON output
|
||||
|
||||
For programmatic parsing in scripts:
|
||||
|
||||
```bash
|
||||
agent-browser snapshot --json
|
||||
agent-browser get text @e1 --json
|
||||
```
|
||||
|
||||
Note: The default text output is more compact and preferred for AI agents.
|
||||
@@ -1,7 +0,0 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata";
|
||||
|
||||
export const metadata = pageMetadata("security");
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return children;
|
||||
}
|
||||
@@ -1,242 +0,0 @@
|
||||
# Security
|
||||
|
||||
agent-browser includes security features to protect against credential exposure, prompt injection via untrusted page content, and unauthorized browser actions.
|
||||
|
||||
All security features are opt-in. By default, agent-browser imposes no restrictions on navigation, actions, or output. Enable these features as needed for your deployment -- existing workflows are unaffected until you explicitly activate a feature.
|
||||
|
||||
## Threat Model
|
||||
|
||||
These features are designed to mitigate the following threats when an LLM-based agent drives a browser:
|
||||
|
||||
- **Credential exposure** -- Passwords stored in the auth vault are never included in LLM context. The CLI handles vault operations locally; credentials do not pass through the daemon's IPC channel.
|
||||
- **Prompt injection via page content** -- Malicious pages can embed text that looks like tool output or system instructions. Content boundary markers (`--content-boundaries`) let the orchestrator distinguish trusted tool output from untrusted page content.
|
||||
- **Unauthorized navigation / data exfiltration** -- A compromised or manipulated agent could navigate to attacker-controlled domains to exfiltrate data. The domain allowlist (`--allowed-domains`) blocks navigations, sub-resource requests, WebSocket connections, EventSource streams, and `sendBeacon` calls to non-allowed domains.
|
||||
- **Unauthorized destructive actions** -- Action policy (`--action-policy`) and confirmation gating (`--confirm-actions`) prevent the agent from performing dangerous operations (eval, downloads, uploads) without explicit approval.
|
||||
- **Context flooding** -- Large page outputs can overwhelm an LLM's context window. Output truncation (`--max-output`) caps the size of page-sourced content.
|
||||
|
||||
### Known limitations
|
||||
|
||||
- **WebSocket/EventSource blocking is best-effort.** It works by overriding browser constructors via an init script. If the `eval` action category is allowed, page scripts could theoretically restore the original constructors. Deny `eval` via `--action-policy` for maximum protection.
|
||||
- **Domain filter timing on remote connections.** When connecting to a pre-existing browser via CDP or a cloud provider, pages may have already loaded content before the domain filter is installed. agent-browser navigates disallowed pages to `about:blank` after the filter is active, but resources loaded before that point are not retroactively blocked.
|
||||
- **Content boundaries are defense-in-depth.** They rely on the LLM and orchestrator respecting the structural markers. A sufficiently capable adversarial page could attempt to mimic the boundary format, though the per-process CSPRNG nonce makes this impractical to predict.
|
||||
- **Confirmation timeout.** Pending confirmations auto-deny after 60 seconds. Orchestrators must respond within that window.
|
||||
- **Non-TTY auto-deny.** When `--confirm-interactive` is set but stdin is not a terminal (e.g., piped input), actions are automatically denied to prevent accidental approval in non-interactive contexts.
|
||||
|
||||
## Authentication Vault
|
||||
|
||||
Store credentials locally and reference them by name. The LLM never sees passwords.
|
||||
|
||||
```bash
|
||||
# Save credentials (encrypted if AGENT_BROWSER_ENCRYPTION_KEY is set)
|
||||
# Recommended: pipe password via stdin to avoid shell history / process listing exposure
|
||||
echo "pass" | agent-browser auth save github --url https://github.com/login --username user --password-stdin
|
||||
|
||||
# Or pass directly (a warning will be shown)
|
||||
agent-browser auth save github --url https://github.com/login --username user --password pass
|
||||
|
||||
# Login using saved credentials
|
||||
agent-browser auth login github
|
||||
|
||||
# List saved profiles (names and URLs only, no secrets)
|
||||
agent-browser auth list
|
||||
|
||||
# Show profile metadata
|
||||
agent-browser auth show github
|
||||
|
||||
# Delete a profile
|
||||
agent-browser auth delete github
|
||||
```
|
||||
|
||||
`auth login` navigates with the `load` lifecycle event and then waits for form selectors to appear before filling/clicking. This makes delayed SPA login pages more reliable while avoiding `networkidle` hangs on pages with long-lived background requests.
|
||||
|
||||
Custom selectors can be specified if auto-detection fails:
|
||||
|
||||
```bash
|
||||
agent-browser auth save myapp \
|
||||
--url https://app.example.com/login \
|
||||
--username user --password pass \
|
||||
--username-selector "#email" \
|
||||
--password-selector "#password" \
|
||||
--submit-selector "button.login"
|
||||
```
|
||||
|
||||
Profiles are stored in `~/.agent-browser/auth/` and always encrypted with AES-256-GCM. If `AGENT_BROWSER_ENCRYPTION_KEY` is not set, a key is auto-generated at `~/.agent-browser/.encryption-key` on first use. Back up this file or set the environment variable explicitly for portability.
|
||||
|
||||
File permissions are enforced on both Unix (`chmod 600`/`700`) and Windows (`icacls` restricted to the current user) to prevent other users from reading encryption keys or auth profiles.
|
||||
|
||||
## Content Boundary Markers
|
||||
|
||||
When `--content-boundaries` is enabled, all page-sourced output is wrapped in structural markers so LLMs can distinguish tool output from untrusted page content:
|
||||
|
||||
```
|
||||
--- AGENT_BROWSER_PAGE_CONTENT nonce=a1b2c3d4 origin=https://example.com ---
|
||||
[snapshot / text / html / eval output here]
|
||||
--- END_AGENT_BROWSER_PAGE_CONTENT nonce=a1b2c3d4 ---
|
||||
```
|
||||
|
||||
The nonce is a random value generated per CLI process invocation, making it unpredictable to page content that might attempt to spoof the boundary.
|
||||
|
||||
Enable via flag or environment variable:
|
||||
|
||||
```bash
|
||||
agent-browser --content-boundaries snapshot
|
||||
# or
|
||||
export AGENT_BROWSER_CONTENT_BOUNDARIES=1
|
||||
```
|
||||
|
||||
Affected output types: `snapshot`, `get text`, `get html`, `eval`, `console`.
|
||||
|
||||
In `--json` mode, boundary metadata is injected into the JSON response as a `_boundary` object containing `nonce` and `origin` fields, allowing orchestrators to verify provenance programmatically:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": { "snapshot": "...", "origin": "https://example.com" },
|
||||
"_boundary": { "nonce": "a1b2c3d4e5f6...", "origin": "https://example.com" }
|
||||
}
|
||||
```
|
||||
|
||||
## Domain Allowlist
|
||||
|
||||
Restrict which domains the browser can interact with, preventing redirect-based attacks and data exfiltration:
|
||||
|
||||
```bash
|
||||
agent-browser --allowed-domains "example.com,*.example.com,github.com" open https://example.com
|
||||
# or
|
||||
export AGENT_BROWSER_ALLOWED_DOMAINS="example.com,*.example.com"
|
||||
```
|
||||
|
||||
Supports exact match (`github.com`) and wildcard prefix (`*.example.com`, which also matches the bare domain `example.com`). Both page navigations and sub-resource requests (scripts, images, fetch, XHR, etc.) to non-allowed domains are blocked, preventing data exfiltration. WebSocket and EventSource connections are also blocked via constructor-level patching. Non-http(s) sub-resources (data URIs, blobs) are still allowed. When a request is blocked, the command returns an error.
|
||||
|
||||
> **Note:** The WebSocket/EventSource blocking is best-effort -- it works by overriding the browser constructors via an init script. If the `eval` action category is allowed, page scripts could theoretically restore the original constructors. For maximum protection, deny the `eval` category via `--action-policy` when using `--allowed-domains`.
|
||||
|
||||
Config file:
|
||||
|
||||
```json
|
||||
{
|
||||
"allowedDomains": ["example.com", "*.example.com", "github.com"]
|
||||
}
|
||||
```
|
||||
|
||||
> **CDN and third-party resources:** The domain filter blocks all sub-resource requests (scripts, stylesheets, images, fonts, fetch/XHR) to non-allowed domains. Most websites load assets from CDN domains. Include these in your allowlist or pages will break. For example:
|
||||
>
|
||||
> ```bash
|
||||
> --allowed-domains "myapp.com,*.myapp.com,cdn.jsdelivr.net,fonts.googleapis.com,fonts.gstatic.com"
|
||||
> ```
|
||||
|
||||
## Action Policy
|
||||
|
||||
Gate actions using a static policy file. The policy is enforced by the daemon -- denied actions fail immediately.
|
||||
|
||||
```bash
|
||||
agent-browser --action-policy ./policy.json open https://example.com
|
||||
# or
|
||||
export AGENT_BROWSER_ACTION_POLICY=./policy.json
|
||||
```
|
||||
|
||||
Example policy (permissive with specific denials):
|
||||
|
||||
```json
|
||||
{
|
||||
"default": "allow",
|
||||
"deny": ["eval", "download", "upload"]
|
||||
}
|
||||
```
|
||||
|
||||
Example policy (restrictive):
|
||||
|
||||
```json
|
||||
{
|
||||
"default": "deny",
|
||||
"allow": ["navigate", "snapshot", "click", "scroll", "wait", "get"]
|
||||
}
|
||||
```
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Category</th><th>Actions</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><code>navigate</code></td><td>open, back, forward, reload, tab new</td></tr>
|
||||
<tr><td><code>click</code></td><td>click, dblclick, tap</td></tr>
|
||||
<tr><td><code>fill</code></td><td>fill, type, keyboard type/inserttext, select, check, uncheck</td></tr>
|
||||
<tr><td><code>eval</code></td><td>eval, evalhandle, addscript, addinitscript, addstyle, expose, setcontent</td></tr>
|
||||
<tr><td><code>download</code></td><td>download, waitfordownload</td></tr>
|
||||
<tr><td><code>upload</code></td><td>upload</td></tr>
|
||||
<tr><td><code>snapshot</code></td><td>snapshot, screenshot, pdf, diff</td></tr>
|
||||
<tr><td><code>scroll</code></td><td>scroll, scrollintoview</td></tr>
|
||||
<tr><td><code>wait</code></td><td>wait, waitforurl, waitforloadstate, waitforfunction</td></tr>
|
||||
<tr><td><code>get</code></td><td>get text/html/url/title, count, isvisible, getbyrole, getbytext, getbylabel, etc.</td></tr>
|
||||
<tr><td><code>interact</code></td><td>hover, focus, drag, press, keydown, keyup, mousemove, dispatch</td></tr>
|
||||
<tr><td><code>network</code></td><td>network route/unroute, requests, har start/stop</td></tr>
|
||||
<tr><td><code>state</code></td><td>state save/load, cookies set, storage set</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
Auth vault operations (`auth save`, `auth login`, `auth list`, `auth show`, `auth delete`) and other internal/meta operations bypass action policy enforcement since they are trusted local operations. Domain allowlist restrictions still apply to `auth login` navigations.
|
||||
|
||||
## Action Confirmation
|
||||
|
||||
For actions that require explicit approval, use `--confirm-actions` to specify categories that require confirmation:
|
||||
|
||||
```bash
|
||||
# Orchestrator mode: returns confirmation_required response
|
||||
agent-browser --confirm-actions eval,download eval "document.title"
|
||||
|
||||
# Then approve or deny:
|
||||
agent-browser confirm c_8f3a1234
|
||||
agent-browser deny c_8f3a1234
|
||||
```
|
||||
|
||||
For interactive (human-in-the-loop) confirmation:
|
||||
|
||||
```bash
|
||||
agent-browser --confirm-actions eval,download --confirm-interactive eval "document.title"
|
||||
# Prompts: Allow? [y/N]
|
||||
```
|
||||
|
||||
Pending confirmations auto-deny after 60 seconds.
|
||||
|
||||
> **Non-TTY behavior:** When `--confirm-interactive` is set but stdin is not a TTY (e.g., piped input or running inside an automated pipeline), actions are automatically denied. This prevents accidental approval in non-interactive contexts.
|
||||
|
||||
## Output Length Limits
|
||||
|
||||
Prevent context flooding by truncating large page outputs:
|
||||
|
||||
```bash
|
||||
agent-browser --max-output 50000 get text body
|
||||
# or
|
||||
export AGENT_BROWSER_MAX_OUTPUT=50000
|
||||
```
|
||||
|
||||
Affected output types: `snapshot`, `get text`, `get html`, `eval`, `console`.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Variable</th><th>Description</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><code>AGENT_BROWSER_CONTENT_BOUNDARIES</code></td><td>Wrap page output in boundary markers</td></tr>
|
||||
<tr><td><code>AGENT_BROWSER_MAX_OUTPUT</code></td><td>Max characters for page output</td></tr>
|
||||
<tr><td><code>AGENT_BROWSER_ALLOWED_DOMAINS</code></td><td>Comma-separated allowed domain patterns</td></tr>
|
||||
<tr><td><code>AGENT_BROWSER_ACTION_POLICY</code></td><td>Path to action policy JSON file</td></tr>
|
||||
<tr><td><code>AGENT_BROWSER_CONFIRM_ACTIONS</code></td><td>Comma-separated action categories requiring confirmation</td></tr>
|
||||
<tr><td><code>AGENT_BROWSER_CONFIRM_INTERACTIVE</code></td><td>Enable interactive confirmation prompts</td></tr>
|
||||
<tr><td><code>AGENT_BROWSER_ENCRYPTION_KEY</code></td><td>64-char hex key for AES-256-GCM encryption (auth vault + sessions)</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## Recommended Configuration
|
||||
|
||||
For production AI agent deployments:
|
||||
|
||||
```json
|
||||
{
|
||||
"contentBoundaries": true,
|
||||
"maxOutput": 50000,
|
||||
"allowedDomains": ["your-app.com", "*.your-app.com"],
|
||||
"actionPolicy": "./policy.json"
|
||||
}
|
||||
```
|
||||
@@ -1,7 +0,0 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata";
|
||||
|
||||
export const metadata = pageMetadata("selectors");
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return children;
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
# Selectors
|
||||
|
||||
## Refs (recommended)
|
||||
|
||||
Refs provide deterministic element selection from snapshots. Best for AI agents.
|
||||
|
||||
```bash
|
||||
# 1. Get snapshot with refs
|
||||
agent-browser snapshot
|
||||
# Output:
|
||||
# - heading "Example Domain" [ref=e1] [level=1]
|
||||
# - button "Submit" [ref=e2]
|
||||
# - textbox "Email" [ref=e3]
|
||||
# - link "Learn more" [ref=e4]
|
||||
|
||||
# 2. Use refs to interact
|
||||
agent-browser click @e2 # Click the button
|
||||
agent-browser fill @e3 "test@example.com" # Fill the textbox
|
||||
agent-browser get text @e1 # Get heading text
|
||||
agent-browser hover @e4 # Hover the link
|
||||
```
|
||||
|
||||
### Why refs?
|
||||
|
||||
- **Deterministic** - Ref points to exact element from snapshot
|
||||
- **Fast** - No DOM re-query needed
|
||||
- **AI-friendly** - LLMs can reliably parse and use refs
|
||||
|
||||
## CSS selectors
|
||||
|
||||
```bash
|
||||
agent-browser click "#id"
|
||||
agent-browser click ".class"
|
||||
agent-browser click "div > button"
|
||||
agent-browser click "[data-testid='submit']"
|
||||
```
|
||||
|
||||
## Text & XPath
|
||||
|
||||
```bash
|
||||
agent-browser click "text=Submit"
|
||||
agent-browser click "xpath=//button[@type='submit']"
|
||||
```
|
||||
|
||||
## Semantic locators
|
||||
|
||||
Find elements by role, label, or other semantic properties:
|
||||
|
||||
```bash
|
||||
agent-browser find role button click --name "Submit"
|
||||
agent-browser find label "Email" fill "test@test.com"
|
||||
agent-browser find placeholder "Search..." fill "query"
|
||||
agent-browser find testid "submit-btn" click
|
||||
```
|
||||
@@ -1,7 +0,0 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata";
|
||||
|
||||
export const metadata = pageMetadata("sessions");
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return children;
|
||||
}
|
||||
@@ -1,267 +0,0 @@
|
||||
# Sessions
|
||||
|
||||
Run multiple isolated browser instances:
|
||||
|
||||
```bash
|
||||
# Different sessions
|
||||
agent-browser --session agent1 open site-a.com
|
||||
agent-browser --session agent2 open site-b.com
|
||||
|
||||
# Or via environment variable
|
||||
AGENT_BROWSER_SESSION=agent1 agent-browser click "#btn"
|
||||
|
||||
# List active sessions
|
||||
agent-browser session list
|
||||
# Output:
|
||||
# Active sessions:
|
||||
# -> default
|
||||
# agent1
|
||||
|
||||
# Show current session
|
||||
agent-browser session
|
||||
```
|
||||
|
||||
## Session isolation
|
||||
|
||||
Each session has its own:
|
||||
|
||||
- Browser instance
|
||||
- Cookies and storage
|
||||
- Navigation history
|
||||
- Authentication state
|
||||
|
||||
## Chrome profile reuse
|
||||
|
||||
The simplest way to reuse your existing login state: pass a Chrome profile name to `--profile`. agent-browser copies the profile to a temp directory (read-only snapshot) and launches Chrome with your existing cookies and sessions.
|
||||
|
||||
```bash
|
||||
# List available Chrome profiles
|
||||
agent-browser profiles
|
||||
|
||||
# Reuse your default Chrome profile's login state
|
||||
agent-browser --profile Default open https://gmail.com
|
||||
|
||||
# Use a named profile (by display name or directory name)
|
||||
agent-browser --profile "Work" open https://app.example.com
|
||||
|
||||
# Or via environment variable
|
||||
AGENT_BROWSER_PROFILE=Default agent-browser open https://gmail.com
|
||||
```
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Detail</th><th>Description</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>Supported browsers</td><td>Chrome, Chrome Canary, Chromium, Brave</td></tr>
|
||||
<tr><td>What's copied</td><td>Cookies, local storage, extensions state (cache dirs excluded for speed)</td></tr>
|
||||
<tr><td>Original profile</td><td>Never modified (read-only snapshot)</td></tr>
|
||||
<tr><td>Cleanup</td><td>Temp copy deleted when browser closes</td></tr>
|
||||
<tr><td>Windows note</td><td>Close Chrome before using <code>--profile <name></code> if Chrome is running</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## Persistent profiles
|
||||
|
||||
For a custom profile directory that persists state across browser restarts, pass a path to `--profile`:
|
||||
|
||||
```bash
|
||||
# Use a persistent profile directory
|
||||
agent-browser --profile ~/.myapp-profile open myapp.com
|
||||
|
||||
# Login once, then reuse the authenticated session
|
||||
agent-browser --profile ~/.myapp-profile open myapp.com/dashboard
|
||||
|
||||
# Or via environment variable
|
||||
AGENT_BROWSER_PROFILE=~/.myapp-profile agent-browser open myapp.com
|
||||
```
|
||||
|
||||
The profile directory stores:
|
||||
|
||||
- Cookies and localStorage
|
||||
- IndexedDB data
|
||||
- Service workers
|
||||
- Browser cache
|
||||
- Login sessions
|
||||
|
||||
## Import auth from your browser
|
||||
|
||||
If you are already logged in to a site in Chrome, you can grab that auth state and reuse it in agent-browser. This is the fastest way to bypass login flows, OAuth, SSO, or 2FA.
|
||||
|
||||
**Step 1:** Start Chrome with remote debugging:
|
||||
|
||||
```bash
|
||||
# macOS
|
||||
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --remote-debugging-port=9222
|
||||
|
||||
# Linux
|
||||
google-chrome --remote-debugging-port=9222
|
||||
```
|
||||
|
||||
Log in to your target site(s) in this Chrome window.
|
||||
|
||||
`--remote-debugging-port` exposes full browser control on localhost. Any local process can connect. Only use on trusted machines and close Chrome when done.
|
||||
|
||||
**Step 2:** Connect and save the authenticated state:
|
||||
|
||||
```bash
|
||||
agent-browser --auto-connect state save ./my-auth.json
|
||||
```
|
||||
|
||||
**Step 3:** Use the saved auth in future sessions:
|
||||
|
||||
```bash
|
||||
# Load auth at launch
|
||||
agent-browser --state ./my-auth.json open https://app.example.com/dashboard
|
||||
|
||||
# Or load into an existing session
|
||||
agent-browser state load ./my-auth.json
|
||||
agent-browser open https://app.example.com/dashboard
|
||||
```
|
||||
|
||||
Combine with `--session-name` so the imported auth auto-persists across restarts:
|
||||
|
||||
```bash
|
||||
agent-browser --session-name myapp state load ./my-auth.json
|
||||
# From now on, state auto-saves/restores for "myapp"
|
||||
```
|
||||
|
||||
State files contain session tokens in plaintext. Add them to `.gitignore` and delete when no longer needed. For encryption at rest, see [State encryption](#state-encryption) below.
|
||||
|
||||
## Session persistence
|
||||
|
||||
Use `--session-name` to automatically save and restore cookies and localStorage across browser restarts:
|
||||
|
||||
```bash
|
||||
# Auto-save/load state for "twitter" session
|
||||
agent-browser --session-name twitter open twitter.com
|
||||
|
||||
# Login once, then state persists automatically
|
||||
agent-browser --session-name twitter click "#login"
|
||||
|
||||
# Or via environment variable
|
||||
export AGENT_BROWSER_SESSION_NAME=twitter
|
||||
agent-browser open twitter.com
|
||||
```
|
||||
|
||||
State files are stored in `~/.agent-browser/sessions/` and automatically loaded on daemon start.
|
||||
|
||||
### Session name rules
|
||||
|
||||
Session names must contain only alphanumeric characters, hyphens, and underscores:
|
||||
|
||||
```bash
|
||||
# Valid session names
|
||||
agent-browser --session-name my-project open example.com
|
||||
agent-browser --session-name test_session_v2 open example.com
|
||||
|
||||
# Invalid (will be rejected)
|
||||
agent-browser --session-name "../bad" open example.com # path traversal
|
||||
agent-browser --session-name "my session" open example.com # spaces
|
||||
agent-browser --session-name "foo/bar" open example.com # slashes
|
||||
```
|
||||
|
||||
## State encryption
|
||||
|
||||
Encrypt saved state files (cookies, localStorage) using AES-256-GCM:
|
||||
|
||||
```bash
|
||||
# Generate a 256-bit key (64 hex characters)
|
||||
openssl rand -hex 32
|
||||
|
||||
# Set the encryption key
|
||||
export AGENT_BROWSER_ENCRYPTION_KEY=<your-64-char-hex-key>
|
||||
|
||||
# State files are now encrypted automatically
|
||||
agent-browser --session-name secure-session open example.com
|
||||
|
||||
# List states shows encryption status
|
||||
agent-browser state list
|
||||
```
|
||||
|
||||
## State auto-expiration
|
||||
|
||||
Automatically delete old state files to prevent accumulation:
|
||||
|
||||
```bash
|
||||
# Set expiration (default: 30 days)
|
||||
export AGENT_BROWSER_STATE_EXPIRE_DAYS=7
|
||||
|
||||
# Manually clean old states
|
||||
agent-browser state clean --older-than 7
|
||||
```
|
||||
|
||||
## State management commands
|
||||
|
||||
```bash
|
||||
# List all saved states
|
||||
agent-browser state list
|
||||
|
||||
# Show state summary (cookies, origins, domains)
|
||||
agent-browser state show my-session-default.json
|
||||
|
||||
# Rename a state file
|
||||
agent-browser state rename old-name new-name
|
||||
|
||||
# Clear states for a specific session name
|
||||
agent-browser state clear my-session
|
||||
|
||||
# Clear all saved states
|
||||
agent-browser state clear --all
|
||||
|
||||
# Manual save/load (for custom paths)
|
||||
agent-browser state save ./backup.json
|
||||
agent-browser state load ./backup.json
|
||||
```
|
||||
|
||||
## Authenticated sessions
|
||||
|
||||
Use `--headers` to set HTTP headers for a specific origin:
|
||||
|
||||
```bash
|
||||
# Headers scoped to api.example.com only
|
||||
agent-browser open api.example.com --headers '{"Authorization": "Bearer <token>"}'
|
||||
|
||||
# Requests to api.example.com include the auth header
|
||||
agent-browser snapshot -i --json
|
||||
agent-browser click @e2
|
||||
|
||||
# Navigate to another domain - headers NOT sent
|
||||
agent-browser open other-site.com
|
||||
```
|
||||
|
||||
Useful for:
|
||||
|
||||
- **Skipping login flows** - Authenticate via headers
|
||||
- **Switching users** - Different auth tokens per session
|
||||
- **API testing** - Access protected endpoints
|
||||
- **Security** - Headers scoped to origin, not leaked
|
||||
|
||||
## Multiple origins
|
||||
|
||||
```bash
|
||||
agent-browser open api.example.com --headers '{"Authorization": "Bearer token1"}'
|
||||
agent-browser open api.acme.com --headers '{"Authorization": "Bearer token2"}'
|
||||
```
|
||||
|
||||
## Global headers
|
||||
|
||||
For headers on all domains:
|
||||
|
||||
```bash
|
||||
agent-browser set headers '{"X-Custom-Header": "value"}'
|
||||
```
|
||||
|
||||
## Environment variables
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Variable</th><th>Description</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><code>AGENT_BROWSER_SESSION</code></td><td>Browser session ID (default: "default")</td></tr>
|
||||
<tr><td><code>AGENT_BROWSER_SESSION_NAME</code></td><td>Auto-save/load state persistence name</td></tr>
|
||||
<tr><td><code>AGENT_BROWSER_ENCRYPTION_KEY</code></td><td>64-char hex key for AES-256-GCM encryption</td></tr>
|
||||
<tr><td><code>AGENT_BROWSER_STATE_EXPIRE_DAYS</code></td><td>Auto-delete states older than N days (default: 30)</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -1,7 +0,0 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata";
|
||||
|
||||
export const metadata = pageMetadata("skills");
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return children;
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
# Skills
|
||||
|
||||
agent-browser ships with skills that teach AI coding agents how to use it for specific workflows. Install a skill and your agent in Cursor, Claude Code, or Codex can automate browser tasks without manual guidance.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npx skills add vercel-labs/agent-browser
|
||||
```
|
||||
|
||||
This installs a single discovery skill that teaches your agent about agent-browser and directs it to use the `agent-browser skills` CLI command for current instructions. The discovery skill contains trigger words so agents prefer agent-browser over built-in browser tools.
|
||||
|
||||
## CLI Command
|
||||
|
||||
Agents retrieve skill content at runtime using the `agent-browser skills` command. This always serves content matching the installed CLI version, so instructions never go stale.
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Command</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>agent-browser skills</code></td>
|
||||
<td>List all available skills (same as <code>skills list</code>)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>agent-browser skills list</code></td>
|
||||
<td>List all available skills with names and descriptions</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>agent-browser skills get <name></code></td>
|
||||
<td>Output a skill's full content</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>agent-browser skills get <name> --full</code></td>
|
||||
<td>Include references and templates alongside the skill</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>agent-browser skills get --all</code></td>
|
||||
<td>Output every skill</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>agent-browser skills path [name]</code></td>
|
||||
<td>Print the filesystem path to a skill directory</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
All commands support `--json` for structured output.
|
||||
|
||||
Set the `AGENT_BROWSER_SKILLS_DIR` environment variable to override the skills directory path.
|
||||
|
||||
## How It Works
|
||||
|
||||
The discovery skill installed via `npx skills add` is intentionally thin and stable. It makes agents aware of agent-browser, provides trigger words for activation, and points to the `agent-browser skills` command. Actual usage instructions, command references, workflows, and specialized knowledge all live in the CLI-served skills.
|
||||
|
||||
This design solves the version drift problem: the installed SKILL.md rarely changes, while the CLI always serves content matching its own version.
|
||||
|
||||
## Available Skills
|
||||
|
||||
- **core** — Core browser automation: navigation, snapshots, forms, screenshots, data extraction, sessions, authentication, diffing, and the full command reference. Start here for most browser tasks.
|
||||
- **dogfood** — Systematic exploratory testing. Navigates an app like a real user, finds bugs and UX issues, and produces a structured report with screenshots and repro videos.
|
||||
- **electron** — Automate any Electron app (VS Code, Slack, Discord, Figma, etc.) by connecting to its built-in Chrome DevTools Protocol port.
|
||||
- **slack** — Browser-based Slack automation. Check unreads, navigate channels, search conversations, send messages, and extract data.
|
||||
- **vercel-sandbox** — Run agent-browser + headless Chrome inside ephemeral Vercel Sandbox microVMs.
|
||||
- **agentcore** — Run agent-browser on AWS Bedrock AgentCore cloud browsers.
|
||||
|
||||
Use `agent-browser skills list` to see all available skills, then `agent-browser skills get <name>` to load one. `agent-browser skills get core --full` is the recommended starting point for most browser tasks.
|
||||
|
||||
## Source
|
||||
|
||||
All skill files are in the [`skills/`](https://github.com/vercel-labs/agent-browser/tree/main/skills) and [`skill-data/`](https://github.com/vercel-labs/agent-browser/tree/main/skill-data) directories of the repository. The `skills/` directory holds the discovery stub that `npx skills add` installs; the `skill-data/` directory holds the runtime skill content served by the CLI.
|
||||
@@ -1,7 +0,0 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata";
|
||||
|
||||
export const metadata = pageMetadata("snapshots");
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return children;
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
# Snapshots
|
||||
|
||||
The `snapshot` command returns a compact accessibility tree with refs for element interaction.
|
||||
|
||||
## Options
|
||||
|
||||
Filter output to reduce size:
|
||||
|
||||
```bash
|
||||
agent-browser snapshot # Full accessibility tree
|
||||
agent-browser snapshot -i # Interactive elements only (recommended)
|
||||
agent-browser snapshot -c # Compact (remove empty elements)
|
||||
agent-browser snapshot -d 3 # Limit depth to 3 levels
|
||||
agent-browser snapshot -s "#main" # Scope to CSS selector
|
||||
agent-browser snapshot -i -c -d 5 # Combine options
|
||||
```
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Option</th><th>Description</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><code>-i, --interactive</code></td><td>Only interactive elements (buttons, links, inputs)</td></tr>
|
||||
<tr><td><code>-u, --urls</code></td><td>Include href URLs for link elements</td></tr>
|
||||
<tr><td><code>-c, --compact</code></td><td>Remove empty structural elements</td></tr>
|
||||
<tr><td><code>-d, --depth</code></td><td>Limit tree depth</td></tr>
|
||||
<tr><td><code>-s, --selector</code></td><td>Scope to CSS selector</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## Output format
|
||||
|
||||
The default text output is compact and AI-friendly:
|
||||
|
||||
```bash
|
||||
agent-browser snapshot -i
|
||||
# Output:
|
||||
# @e1 [heading] "Example Domain" [level=1]
|
||||
# @e2 [button] "Submit"
|
||||
# @e3 [input type="email"] placeholder="Email"
|
||||
# @e4 [link] "Learn more"
|
||||
```
|
||||
|
||||
## Using refs
|
||||
|
||||
Refs from the snapshot map directly to commands:
|
||||
|
||||
```bash
|
||||
agent-browser click @e2 # Click the Submit button
|
||||
agent-browser fill @e3 "a@b.com" # Fill the email input
|
||||
agent-browser get text @e1 # Get heading text
|
||||
```
|
||||
|
||||
## Ref lifecycle
|
||||
|
||||
Refs are invalidated when the page changes. Always re-snapshot after navigation or DOM updates:
|
||||
|
||||
```bash
|
||||
agent-browser click @e4 # Navigates to new page
|
||||
agent-browser snapshot -i # Get fresh refs
|
||||
agent-browser click @e1 # Use new refs
|
||||
```
|
||||
|
||||
## Annotated screenshots
|
||||
|
||||
For visual context alongside text snapshots, use `screenshot --annotate` to overlay numbered labels on interactive elements. Each label `[N]` maps to ref `@eN`:
|
||||
|
||||
In native mode, annotated screenshots currently work on the CDP-backed browser path (Chromium/Lightpanda). The Safari/WebDriver backend does not yet support `--annotate`.
|
||||
|
||||
```bash
|
||||
agent-browser screenshot --annotate ./page.png
|
||||
# -> Screenshot saved to ./page.png
|
||||
# [1] @e1 button "Submit"
|
||||
# [2] @e2 link "Home"
|
||||
# [3] @e3 textbox "Email"
|
||||
agent-browser click @e2
|
||||
```
|
||||
|
||||
Annotated screenshots also cache refs, so you can interact with elements immediately. This is useful when the text snapshot is insufficient -- unlabeled icons, canvas content, or visual layout verification.
|
||||
|
||||
## Iframes
|
||||
|
||||
Snapshots automatically detect and inline iframe content. Each `Iframe` node in the main frame is resolved and its child accessibility tree is included directly beneath it. Refs assigned to elements inside iframes carry frame context, so interactions work without switching frames first.
|
||||
|
||||
```bash
|
||||
agent-browser snapshot -i
|
||||
# @e1 [heading] "Checkout"
|
||||
# @e2 [Iframe] "payment-frame"
|
||||
# @e3 [input] "Card number"
|
||||
# @e4 [button] "Pay"
|
||||
|
||||
agent-browser fill @e3 "4111111111111111"
|
||||
agent-browser click @e4
|
||||
```
|
||||
|
||||
Only one level of iframe nesting is expanded. Cross-origin iframes that block accessibility tree access and empty iframes are silently omitted.
|
||||
|
||||
To scope a snapshot to a single iframe, switch into it first:
|
||||
|
||||
```bash
|
||||
agent-browser frame @e2
|
||||
agent-browser snapshot -i # Only elements inside that iframe
|
||||
agent-browser frame main # Return to main frame
|
||||
```
|
||||
|
||||
## Best practices
|
||||
|
||||
1. Use `-i` to reduce output to actionable elements
|
||||
2. Re-snapshot after page changes to get updated refs
|
||||
3. Scope with `-s` for specific page sections
|
||||
4. Use `-d` to limit depth on complex pages
|
||||
5. Use `screenshot --annotate` when visual context is needed alongside refs
|
||||
|
||||
## JSON output
|
||||
|
||||
For programmatic parsing in scripts:
|
||||
|
||||
```bash
|
||||
agent-browser snapshot --json
|
||||
# {"success":true,"data":{"snapshot":"...","refs":{"e1":{"role":"heading","name":"Title"},...}}}
|
||||
```
|
||||
|
||||
Note: JSON uses more tokens than text output. The default text format is preferred for AI agents.
|
||||
@@ -1,7 +0,0 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata";
|
||||
|
||||
export const metadata = pageMetadata("streaming");
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return children;
|
||||
}
|
||||
@@ -1,258 +0,0 @@
|
||||
# Streaming
|
||||
|
||||
Stream the browser viewport via WebSocket for live preview or "pair browsing"
|
||||
where a human can watch and interact alongside an AI agent.
|
||||
|
||||
## Streaming
|
||||
|
||||
Every session automatically starts a WebSocket stream server on an OS-assigned port. The server streams viewport frames and accepts input events (mouse, keyboard, touch).
|
||||
|
||||
To bind to a specific port, set `AGENT_BROWSER_STREAM_PORT`:
|
||||
|
||||
```bash
|
||||
AGENT_BROWSER_STREAM_PORT=9223 agent-browser open example.com
|
||||
```
|
||||
|
||||
You can also manage streaming at runtime:
|
||||
|
||||
```bash
|
||||
agent-browser stream status # Show streaming state and bound port
|
||||
agent-browser stream enable --port 9223 # Re-enable on a specific port
|
||||
agent-browser stream disable # Stop streaming for the session
|
||||
```
|
||||
|
||||
`stream status` returns the enabled state, active port, browser connection state, and whether screencasting is active. `stream disable` tears the server down and removes the session's `.stream` metadata file.
|
||||
|
||||
## Runtime status response
|
||||
|
||||
`agent-browser stream status --json` returns data like:
|
||||
|
||||
```json
|
||||
{
|
||||
"enabled": true,
|
||||
"port": 9223,
|
||||
"connected": true,
|
||||
"screencasting": true
|
||||
}
|
||||
```
|
||||
|
||||
`connected` reports whether the daemon currently has a browser attached. `screencasting` reports whether frames are actively being produced for the stream server.
|
||||
|
||||
## Relationship to screencast commands
|
||||
|
||||
`stream enable` creates the WebSocket server and keeps it available for the session. WebSocket clients then trigger live frame delivery automatically.
|
||||
|
||||
The lower-level `screencast_start` and `screencast_stop` commands still control explicit CDP screencasts directly. Use them when you want a screencast without the WebSocket runtime server.
|
||||
|
||||
## WebSocket protocol
|
||||
|
||||
Connect to `ws://localhost:9223` to receive frames and send input.
|
||||
|
||||
### Frame messages
|
||||
|
||||
The server sends frame messages with base64-encoded images:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "frame",
|
||||
"data": "<base64-encoded-jpeg>",
|
||||
"metadata": {
|
||||
"deviceWidth": 1280,
|
||||
"deviceHeight": 720,
|
||||
"pageScaleFactor": 1,
|
||||
"offsetTop": 0,
|
||||
"scrollOffsetX": 0,
|
||||
"scrollOffsetY": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Status messages
|
||||
|
||||
Connection and screencast status:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "status",
|
||||
"connected": true,
|
||||
"screencasting": true,
|
||||
"viewportWidth": 1280,
|
||||
"viewportHeight": 720
|
||||
}
|
||||
```
|
||||
|
||||
## Input injection
|
||||
|
||||
Send input events to control the browser remotely.
|
||||
|
||||
### Mouse events
|
||||
|
||||
```json
|
||||
// Click
|
||||
{
|
||||
"type": "input_mouse",
|
||||
"eventType": "mousePressed",
|
||||
"x": 100,
|
||||
"y": 200,
|
||||
"button": "left",
|
||||
"clickCount": 1
|
||||
}
|
||||
|
||||
// Release
|
||||
{
|
||||
"type": "input_mouse",
|
||||
"eventType": "mouseReleased",
|
||||
"x": 100,
|
||||
"y": 200,
|
||||
"button": "left"
|
||||
}
|
||||
|
||||
// Move
|
||||
{
|
||||
"type": "input_mouse",
|
||||
"eventType": "mouseMoved",
|
||||
"x": 150,
|
||||
"y": 250
|
||||
}
|
||||
|
||||
// Scroll
|
||||
{
|
||||
"type": "input_mouse",
|
||||
"eventType": "mouseWheel",
|
||||
"x": 100,
|
||||
"y": 200,
|
||||
"deltaX": 0,
|
||||
"deltaY": 100
|
||||
}
|
||||
```
|
||||
|
||||
### Keyboard events
|
||||
|
||||
```json
|
||||
// Key down
|
||||
{
|
||||
"type": "input_keyboard",
|
||||
"eventType": "keyDown",
|
||||
"key": "Enter",
|
||||
"code": "Enter"
|
||||
}
|
||||
|
||||
// Key up
|
||||
{
|
||||
"type": "input_keyboard",
|
||||
"eventType": "keyUp",
|
||||
"key": "Enter",
|
||||
"code": "Enter"
|
||||
}
|
||||
|
||||
// Type character
|
||||
{
|
||||
"type": "input_keyboard",
|
||||
"eventType": "char",
|
||||
"text": "a"
|
||||
}
|
||||
|
||||
// With modifiers (1=Alt, 2=Ctrl, 4=Meta, 8=Shift)
|
||||
{
|
||||
"type": "input_keyboard",
|
||||
"eventType": "keyDown",
|
||||
"key": "c",
|
||||
"code": "KeyC",
|
||||
"modifiers": 2
|
||||
}
|
||||
```
|
||||
|
||||
### Touch events
|
||||
|
||||
```json
|
||||
// Touch start
|
||||
{
|
||||
"type": "input_touch",
|
||||
"eventType": "touchStart",
|
||||
"touchPoints": [{ "x": 100, "y": 200 }]
|
||||
}
|
||||
|
||||
// Touch move
|
||||
{
|
||||
"type": "input_touch",
|
||||
"eventType": "touchMove",
|
||||
"touchPoints": [{ "x": 150, "y": 250 }]
|
||||
}
|
||||
|
||||
// Touch end
|
||||
{
|
||||
"type": "input_touch",
|
||||
"eventType": "touchEnd",
|
||||
"touchPoints": []
|
||||
}
|
||||
|
||||
// Multi-touch (pinch zoom)
|
||||
{
|
||||
"type": "input_touch",
|
||||
"eventType": "touchStart",
|
||||
"touchPoints": [
|
||||
{ "x": 100, "y": 200, "id": 0 },
|
||||
{ "x": 200, "y": 200, "id": 1 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Programmatic API
|
||||
|
||||
For advanced use, control streaming directly via the TypeScript API:
|
||||
|
||||
```typescript
|
||||
import { BrowserManager } from 'agent-browser';
|
||||
|
||||
const browser = new BrowserManager();
|
||||
await browser.launch({ headless: true });
|
||||
await browser.navigate('https://example.com');
|
||||
|
||||
// Start screencast with callback
|
||||
await browser.startScreencast((frame) => {
|
||||
console.log('Frame:', frame.metadata.deviceWidth, 'x', frame.metadata.deviceHeight);
|
||||
// frame.data is base64-encoded image
|
||||
}, {
|
||||
format: 'jpeg', // or 'png'
|
||||
quality: 80, // 0-100, jpeg only
|
||||
maxWidth: 1280,
|
||||
maxHeight: 720,
|
||||
everyNthFrame: 1
|
||||
});
|
||||
|
||||
// Inject mouse event
|
||||
await browser.injectMouseEvent({
|
||||
type: 'mousePressed',
|
||||
x: 100,
|
||||
y: 200,
|
||||
button: 'left',
|
||||
clickCount: 1
|
||||
});
|
||||
|
||||
// Inject keyboard event
|
||||
await browser.injectKeyboardEvent({
|
||||
type: 'keyDown',
|
||||
key: 'Enter',
|
||||
code: 'Enter'
|
||||
});
|
||||
|
||||
// Inject touch event
|
||||
await browser.injectTouchEvent({
|
||||
type: 'touchStart',
|
||||
touchPoints: [{ x: 100, y: 200 }]
|
||||
});
|
||||
|
||||
// Check if screencasting
|
||||
console.log('Active:', browser.isScreencasting());
|
||||
|
||||
// Stop screencast
|
||||
await browser.stopScreencast();
|
||||
```
|
||||
|
||||
## Use cases
|
||||
|
||||
- **Pair browsing** - Human watches and assists AI agent in real-time
|
||||
- **Remote preview** - View browser output in a separate UI
|
||||
- **Recording** - Capture frames for video generation
|
||||
- **Mobile testing** - Inject touch events for mobile emulation
|
||||
- **Accessibility testing** - Manual interaction during automated tests
|
||||
@@ -1,178 +0,0 @@
|
||||
import { codeToHtml } from "shiki";
|
||||
import { CopyButton } from "./copy-button";
|
||||
|
||||
const vercelDarkTheme = {
|
||||
name: "vercel-dark",
|
||||
type: "dark" as const,
|
||||
colors: {
|
||||
"editor.background": "transparent",
|
||||
"editor.foreground": "#EDEDED",
|
||||
},
|
||||
settings: [
|
||||
{
|
||||
scope: ["comment", "punctuation.definition.comment"],
|
||||
settings: { foreground: "#A1A1A1" },
|
||||
},
|
||||
{
|
||||
scope: ["string", "string.quoted", "string.template", "punctuation.definition.string"],
|
||||
settings: { foreground: "#00CA50" },
|
||||
},
|
||||
{
|
||||
scope: ["constant.numeric", "constant.language.boolean", "constant.language.null"],
|
||||
settings: { foreground: "#47A8FF" },
|
||||
},
|
||||
{
|
||||
scope: ["keyword", "storage.type", "storage.modifier"],
|
||||
settings: { foreground: "#FF4D8D" },
|
||||
},
|
||||
{
|
||||
scope: ["keyword.operator", "keyword.control"],
|
||||
settings: { foreground: "#FF4D8D" },
|
||||
},
|
||||
{
|
||||
scope: ["entity.name.function", "support.function", "meta.function-call"],
|
||||
settings: { foreground: "#C472FB" },
|
||||
},
|
||||
{
|
||||
scope: ["variable", "variable.other"],
|
||||
settings: { foreground: "#EDEDED" },
|
||||
},
|
||||
{
|
||||
scope: ["variable.parameter"],
|
||||
settings: { foreground: "#FF9300" },
|
||||
},
|
||||
{
|
||||
scope: ["entity.name.tag", "support.class.component", "entity.name.type"],
|
||||
settings: { foreground: "#FF4D8D" },
|
||||
},
|
||||
{
|
||||
scope: ["punctuation", "meta.brace", "meta.bracket"],
|
||||
settings: { foreground: "#EDEDED" },
|
||||
},
|
||||
{
|
||||
scope: [
|
||||
"support.type.property-name",
|
||||
"entity.name.tag.json",
|
||||
"meta.object-literal.key",
|
||||
"punctuation.support.type.property-name",
|
||||
],
|
||||
settings: { foreground: "#FF4D8D" },
|
||||
},
|
||||
{
|
||||
scope: ["entity.other.attribute-name"],
|
||||
settings: { foreground: "#00CA50" },
|
||||
},
|
||||
{
|
||||
scope: ["support.type.primitive", "entity.name.type.primitive"],
|
||||
settings: { foreground: "#00CA50" },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const vercelLightTheme = {
|
||||
name: "vercel-light",
|
||||
type: "light" as const,
|
||||
colors: {
|
||||
"editor.background": "transparent",
|
||||
"editor.foreground": "#171717",
|
||||
},
|
||||
settings: [
|
||||
{
|
||||
scope: ["comment", "punctuation.definition.comment"],
|
||||
settings: { foreground: "#6B7280" },
|
||||
},
|
||||
{
|
||||
scope: ["string", "string.quoted", "string.template", "punctuation.definition.string"],
|
||||
settings: { foreground: "#067A6E" },
|
||||
},
|
||||
{
|
||||
scope: ["constant.numeric", "constant.language.boolean", "constant.language.null"],
|
||||
settings: { foreground: "#0070C0" },
|
||||
},
|
||||
{
|
||||
scope: ["keyword", "storage.type", "storage.modifier"],
|
||||
settings: { foreground: "#D6409F" },
|
||||
},
|
||||
{
|
||||
scope: ["keyword.operator", "keyword.control"],
|
||||
settings: { foreground: "#D6409F" },
|
||||
},
|
||||
{
|
||||
scope: ["entity.name.function", "support.function", "meta.function-call"],
|
||||
settings: { foreground: "#6E56CF" },
|
||||
},
|
||||
{
|
||||
scope: ["variable", "variable.other"],
|
||||
settings: { foreground: "#171717" },
|
||||
},
|
||||
{
|
||||
scope: ["variable.parameter"],
|
||||
settings: { foreground: "#B45309" },
|
||||
},
|
||||
{
|
||||
scope: ["entity.name.tag", "support.class.component", "entity.name.type"],
|
||||
settings: { foreground: "#D6409F" },
|
||||
},
|
||||
{
|
||||
scope: ["punctuation", "meta.brace", "meta.bracket"],
|
||||
settings: { foreground: "#6B7280" },
|
||||
},
|
||||
{
|
||||
scope: [
|
||||
"support.type.property-name",
|
||||
"entity.name.tag.json",
|
||||
"meta.object-literal.key",
|
||||
"punctuation.support.type.property-name",
|
||||
],
|
||||
settings: { foreground: "#D6409F" },
|
||||
},
|
||||
{
|
||||
scope: ["entity.other.attribute-name"],
|
||||
settings: { foreground: "#067A6E" },
|
||||
},
|
||||
{
|
||||
scope: ["support.type.primitive", "entity.name.type.primitive"],
|
||||
settings: { foreground: "#067A6E" },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const PLACEHOLDER_PREFIX = "\u200B\u200B";
|
||||
const PLACEHOLDER_SUFFIX = "\u200B\u200B";
|
||||
|
||||
function shieldPlaceholders(code: string): string {
|
||||
return code.replace(/<([\w|]+)>/g, `${PLACEHOLDER_PREFIX}$1${PLACEHOLDER_SUFFIX}`);
|
||||
}
|
||||
|
||||
function restorePlaceholders(html: string): string {
|
||||
return html.replace(
|
||||
new RegExp(`${PLACEHOLDER_PREFIX}([\\w|]+)${PLACEHOLDER_SUFFIX}`, "g"),
|
||||
"<$1>",
|
||||
);
|
||||
}
|
||||
|
||||
interface CodeBlockProps {
|
||||
code: string;
|
||||
lang?: string;
|
||||
}
|
||||
|
||||
export async function CodeBlock({ code, lang = "bash" }: CodeBlockProps) {
|
||||
const trimmedCode = code.trim();
|
||||
const shielded = shieldPlaceholders(trimmedCode);
|
||||
let html = await codeToHtml(shielded, {
|
||||
lang,
|
||||
themes: {
|
||||
light: vercelLightTheme,
|
||||
dark: vercelDarkTheme,
|
||||
},
|
||||
defaultColor: false,
|
||||
});
|
||||
html = restorePlaceholders(html);
|
||||
|
||||
return (
|
||||
<div className="code-block relative group">
|
||||
<CopyButton code={trimmedCode} />
|
||||
<div dangerouslySetInnerHTML={{ __html: html }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
interface CopyButtonProps {
|
||||
code: string;
|
||||
}
|
||||
|
||||
export function CopyButton({ code }: CopyButtonProps) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const handleCopy = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(code);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch (error) {
|
||||
console.error("Failed to copy to clipboard:", error);
|
||||
// Optionally, you could set an error state or show a toast notification here
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="absolute top-2 right-2 p-1.5 rounded text-[#666] hover:text-[#999] hover:bg-[#333] opacity-0 group-hover:opacity-100 transition-all"
|
||||
aria-label="Copy code"
|
||||
>
|
||||
{copied ? (
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
export function CopyPageButton() {
|
||||
const pathname = usePathname();
|
||||
const [state, setState] = useState<"idle" | "loading" | "copied">("idle");
|
||||
|
||||
const handleCopy = async () => {
|
||||
setState("loading");
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/docs-markdown?path=${encodeURIComponent(pathname)}`,
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch markdown");
|
||||
}
|
||||
const markdown = await response.text();
|
||||
await navigator.clipboard.writeText(markdown);
|
||||
setState("copied");
|
||||
setTimeout(() => setState("idle"), 2000);
|
||||
} catch {
|
||||
setState("idle");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
disabled={state === "loading"}
|
||||
className="flex items-center gap-1.5 px-2.5 py-1.5 text-xs text-muted-foreground hover:text-foreground border border-border rounded-md hover:bg-muted transition-colors disabled:opacity-50"
|
||||
aria-label="Copy page as Markdown"
|
||||
>
|
||||
{state === "copied" ? (
|
||||
<>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
</svg>
|
||||
Copied
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
|
||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
||||
</svg>
|
||||
Copy Page
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -1,282 +0,0 @@
|
||||
"use client";
|
||||
|
||||
function DiffLine({ line }: { line: string }) {
|
||||
if (line.startsWith("+ ")) {
|
||||
return <div className="text-green-400">{line}</div>;
|
||||
}
|
||||
if (line.startsWith("- ")) {
|
||||
return <div className="text-red-400">{line}</div>;
|
||||
}
|
||||
return <div className="opacity-50">{line}</div>;
|
||||
}
|
||||
|
||||
function CommandLine({ children }: { children: string }) {
|
||||
return (
|
||||
<div>
|
||||
<span className="opacity-40">$ </span>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Terminal({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div
|
||||
className="rounded border font-mono text-[0.8125rem] leading-[1.7] overflow-x-auto"
|
||||
style={{
|
||||
background: "var(--card)",
|
||||
borderColor: "var(--border)",
|
||||
padding: "0.875rem",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PageMockup({
|
||||
label,
|
||||
buttonColor,
|
||||
diffMode,
|
||||
}: {
|
||||
label: string;
|
||||
buttonColor: string;
|
||||
diffMode?: boolean;
|
||||
}) {
|
||||
const dimOpacity = diffMode ? 0.15 : 1;
|
||||
return (
|
||||
<div className="flex-1 min-w-0">
|
||||
<div
|
||||
className="text-[0.6875rem] font-medium mb-1.5 text-center"
|
||||
style={{ color: "var(--muted-foreground)" }}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
<svg
|
||||
viewBox="0 0 160 120"
|
||||
className="w-full rounded border"
|
||||
style={{ borderColor: "var(--border)" }}
|
||||
>
|
||||
<rect width="160" height="120" fill={diffMode ? "#1a1a1a" : "#111"} />
|
||||
|
||||
{/* Nav bar */}
|
||||
<rect
|
||||
x="0"
|
||||
y="0"
|
||||
width="160"
|
||||
height="16"
|
||||
fill="#222"
|
||||
opacity={dimOpacity}
|
||||
/>
|
||||
<rect
|
||||
x="8"
|
||||
y="5"
|
||||
width="24"
|
||||
height="6"
|
||||
rx="1"
|
||||
fill="#555"
|
||||
opacity={dimOpacity}
|
||||
/>
|
||||
<rect
|
||||
x="120"
|
||||
y="5"
|
||||
width="12"
|
||||
height="6"
|
||||
rx="1"
|
||||
fill="#444"
|
||||
opacity={dimOpacity}
|
||||
/>
|
||||
<rect
|
||||
x="136"
|
||||
y="5"
|
||||
width="12"
|
||||
height="6"
|
||||
rx="1"
|
||||
fill="#444"
|
||||
opacity={dimOpacity}
|
||||
/>
|
||||
|
||||
{/* Heading */}
|
||||
<rect
|
||||
x="20"
|
||||
y="26"
|
||||
width="80"
|
||||
height="6"
|
||||
rx="1"
|
||||
fill="#666"
|
||||
opacity={dimOpacity}
|
||||
/>
|
||||
|
||||
{/* Subtext */}
|
||||
<rect
|
||||
x="30"
|
||||
y="38"
|
||||
width="60"
|
||||
height="4"
|
||||
rx="1"
|
||||
fill="#444"
|
||||
opacity={dimOpacity}
|
||||
/>
|
||||
|
||||
{/* Input field */}
|
||||
<rect
|
||||
x="30"
|
||||
y="52"
|
||||
width="100"
|
||||
height="14"
|
||||
rx="2"
|
||||
fill="#1a1a1a"
|
||||
stroke="#333"
|
||||
strokeWidth="0.5"
|
||||
opacity={dimOpacity}
|
||||
/>
|
||||
|
||||
{/* Button -- this is what changes */}
|
||||
{diffMode ? (
|
||||
<>
|
||||
<rect
|
||||
x="55"
|
||||
y="76"
|
||||
width="50"
|
||||
height="14"
|
||||
rx="2"
|
||||
fill="#ef4444"
|
||||
opacity="0.85"
|
||||
/>
|
||||
<rect
|
||||
x="55"
|
||||
y="76"
|
||||
width="50"
|
||||
height="14"
|
||||
rx="2"
|
||||
fill="none"
|
||||
stroke="#ef4444"
|
||||
strokeWidth="1.5"
|
||||
strokeDasharray="3 2"
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<rect
|
||||
x="55"
|
||||
y="76"
|
||||
width="50"
|
||||
height="14"
|
||||
rx="2"
|
||||
fill={buttonColor}
|
||||
/>
|
||||
)}
|
||||
<text
|
||||
x="80"
|
||||
y="85.5"
|
||||
textAnchor="middle"
|
||||
fill="white"
|
||||
fontSize="6"
|
||||
fontFamily="system-ui, sans-serif"
|
||||
opacity={diffMode ? 0.9 : 1}
|
||||
>
|
||||
Submit
|
||||
</text>
|
||||
|
||||
{/* Footer line */}
|
||||
<rect
|
||||
x="40"
|
||||
y="102"
|
||||
width="80"
|
||||
height="3"
|
||||
rx="1"
|
||||
fill="#333"
|
||||
opacity={dimOpacity}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const snapshotDiffLines = [
|
||||
" heading \"Sign Up\" [ref=e1]",
|
||||
" text \"Create your account\" [ref=e2]",
|
||||
"- textbox \"Email\" [ref=e3]",
|
||||
"+ textbox \"Email\" [ref=e3]: \"test@example.com\"",
|
||||
"- button \"Submit\" [ref=e4]",
|
||||
"+ button \"Submit\" [ref=e4] [disabled]",
|
||||
"+ status \"Sending...\" [ref=e7]",
|
||||
" link \"Already have an account?\" [ref=e5]",
|
||||
];
|
||||
|
||||
export function DiffDemo() {
|
||||
return (
|
||||
<div className="grid gap-8 my-8">
|
||||
{/* Panel 1: Snapshot diff */}
|
||||
<div>
|
||||
<div
|
||||
className="text-xs font-medium uppercase tracking-wider mb-3"
|
||||
style={{ color: "var(--muted-foreground)" }}
|
||||
>
|
||||
Verify an action changed the page
|
||||
</div>
|
||||
<Terminal>
|
||||
<div className="opacity-60 mb-2">
|
||||
<CommandLine>agent-browser snapshot -i</CommandLine>
|
||||
<CommandLine>
|
||||
agent-browser fill @e3 "test@example.com"
|
||||
</CommandLine>
|
||||
<CommandLine>agent-browser click @e4</CommandLine>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<CommandLine>agent-browser diff snapshot</CommandLine>
|
||||
</div>
|
||||
<div
|
||||
className="border-t pt-3"
|
||||
style={{ borderColor: "var(--border)" }}
|
||||
>
|
||||
{snapshotDiffLines.map((line, i) => (
|
||||
<DiffLine key={i} line={line} />
|
||||
))}
|
||||
<div className="mt-2 opacity-60">
|
||||
<span className="text-green-400">3</span> additions,{" "}
|
||||
<span className="text-red-400">2</span> removals,{" "}
|
||||
<span>3</span> unchanged
|
||||
</div>
|
||||
</div>
|
||||
</Terminal>
|
||||
</div>
|
||||
|
||||
{/* Panel 2: Screenshot diff */}
|
||||
<div>
|
||||
<div
|
||||
className="text-xs font-medium uppercase tracking-wider mb-3"
|
||||
style={{ color: "var(--muted-foreground)" }}
|
||||
>
|
||||
Catch a visual regression
|
||||
</div>
|
||||
<Terminal>
|
||||
<div className="mb-3">
|
||||
<CommandLine>
|
||||
agent-browser diff screenshot --baseline before-deploy.png
|
||||
</CommandLine>
|
||||
</div>
|
||||
<div
|
||||
className="border-t pt-3"
|
||||
style={{ borderColor: "var(--border)" }}
|
||||
>
|
||||
<div className="text-red-400">
|
||||
✗ 2.37% pixels differ
|
||||
</div>
|
||||
<div className="opacity-50">
|
||||
Diff image: ~/.agent-browser/tmp/diffs/diff-1708473621.png
|
||||
</div>
|
||||
<div className="opacity-50">
|
||||
<span className="text-red-400">1,137</span> different /{" "}
|
||||
48,000 total pixels
|
||||
</div>
|
||||
</div>
|
||||
</Terminal>
|
||||
<div className="flex gap-2 mt-3">
|
||||
<PageMockup label="Baseline" buttonColor="#3b82f6" />
|
||||
<PageMockup label="Current" buttonColor="#22c55e" />
|
||||
<PageMockup label="Diff" buttonColor="#ef4444" diffMode />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,538 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
useRef,
|
||||
useEffect,
|
||||
useState,
|
||||
useCallback,
|
||||
type PointerEvent as ReactPointerEvent,
|
||||
} from "react";
|
||||
import { useChat } from "@ai-sdk/react";
|
||||
import { DefaultChatTransport } from "ai";
|
||||
import { Streamdown } from "streamdown";
|
||||
import Link from "next/link";
|
||||
import { Sheet, SheetContent, SheetTitle } from "@/components/ui/sheet";
|
||||
|
||||
const STORAGE_KEY = "docs-chat-messages";
|
||||
const transport = new DefaultChatTransport({ api: "/api/docs-chat" });
|
||||
|
||||
const DESKTOP_DEFAULT_WIDTH = 400;
|
||||
const DESKTOP_MIN_WIDTH = 300;
|
||||
const DESKTOP_MAX_WIDTH = 700;
|
||||
|
||||
function setCookie(name: string, value: string) {
|
||||
document.cookie = `${name}=${encodeURIComponent(value)};path=/;max-age=${60 * 60 * 24 * 365};samesite=lax`;
|
||||
}
|
||||
|
||||
const TOOL_LABELS: Record<
|
||||
string,
|
||||
{ label: string; pastLabel: string; argKey?: string }
|
||||
> = {
|
||||
readFile: { label: "Reading", pastLabel: "Read", argKey: "path" },
|
||||
bash: { label: "Running", pastLabel: "Ran", argKey: "command" },
|
||||
};
|
||||
|
||||
function isToolPart(part: { type: string }): part is {
|
||||
type: string;
|
||||
toolCallId: string;
|
||||
toolName?: string;
|
||||
state: string;
|
||||
input?: Record<string, unknown>;
|
||||
output?: unknown;
|
||||
errorText?: string;
|
||||
} {
|
||||
return part.type.startsWith("tool-") || part.type === "dynamic-tool";
|
||||
}
|
||||
|
||||
function getToolName(part: { type: string; toolName?: string }): string {
|
||||
if (part.type === "dynamic-tool") return part.toolName ?? "tool";
|
||||
return part.type.replace(/^tool-/, "");
|
||||
}
|
||||
|
||||
function ToolCallDisplay({
|
||||
part,
|
||||
}: {
|
||||
part: {
|
||||
type: string;
|
||||
toolCallId: string;
|
||||
toolName?: string;
|
||||
state: string;
|
||||
input?: Record<string, unknown>;
|
||||
output?: unknown;
|
||||
errorText?: string;
|
||||
};
|
||||
}) {
|
||||
const toolName = getToolName(part);
|
||||
const config = TOOL_LABELS[toolName] ?? {
|
||||
label: toolName,
|
||||
pastLabel: toolName,
|
||||
};
|
||||
const isDone = part.state === "output-available";
|
||||
const isError = part.state === "output-error";
|
||||
const isRunning = !isDone && !isError;
|
||||
const displayLabel = isRunning ? config.label : config.pastLabel;
|
||||
|
||||
const args = (part.input ?? {}) as Record<string, unknown>;
|
||||
const argValue = config.argKey ? args[config.argKey] : undefined;
|
||||
const argPreview =
|
||||
argValue != null
|
||||
? String(argValue)
|
||||
.replace(/^\/workspace\//, "/")
|
||||
.replace(/\.md$/, "")
|
||||
.replace(/\/index$/, "") || "/"
|
||||
: "";
|
||||
|
||||
// Link to the docs page if it's a readFile path
|
||||
const docsLink =
|
||||
toolName === "readFile" && argPreview.startsWith("/") ? argPreview : null;
|
||||
|
||||
const argEl = argPreview ? (
|
||||
docsLink ? (
|
||||
<Link href={docsLink} className="truncate underline underline-offset-2">
|
||||
{argPreview}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="truncate">{argPreview}</span>
|
||||
)
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div className="text-xs py-0.5 min-w-0">
|
||||
{isRunning ? (
|
||||
<span className="inline-flex items-center gap-1 font-mono text-muted-foreground animate-tool-shimmer min-w-0 max-w-full">
|
||||
<span className="shrink-0">{displayLabel}</span>
|
||||
{argEl}
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1 font-mono text-muted-foreground/60 min-w-0 max-w-full">
|
||||
<span className="shrink-0">{displayLabel}</span>
|
||||
{argEl}
|
||||
{isError && <span className="text-destructive">failed</span>}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const SUGGESTIONS = [
|
||||
"What is agent-browser?",
|
||||
"How do I install it?",
|
||||
"What commands are available?",
|
||||
"How do snapshots work?",
|
||||
"How do I use CDP mode?",
|
||||
];
|
||||
|
||||
export function DocsChat({
|
||||
defaultOpen = false,
|
||||
defaultWidth = DESKTOP_DEFAULT_WIDTH,
|
||||
}: {
|
||||
defaultOpen?: boolean;
|
||||
defaultWidth?: number;
|
||||
}) {
|
||||
const [open, setOpen] = useState(defaultOpen);
|
||||
const [input, setInput] = useState("");
|
||||
const [isDesktop, setIsDesktop] = useState(false);
|
||||
const [hasMounted, setHasMounted] = useState(false);
|
||||
const [desktopWidth, setDesktopWidth] = useState(
|
||||
Math.min(DESKTOP_MAX_WIDTH, Math.max(DESKTOP_MIN_WIDTH, defaultWidth)),
|
||||
);
|
||||
const messagesScrollRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||
const restoredRef = useRef(false);
|
||||
const isDraggingRef = useRef(false);
|
||||
|
||||
const { messages, sendMessage, status, setMessages, error } = useChat({
|
||||
transport,
|
||||
});
|
||||
|
||||
const isLoading = status === "streaming" || status === "submitted";
|
||||
const showMessages = messages.length > 0 || !!error || isLoading;
|
||||
|
||||
// Detect desktop vs mobile. Close sidebar on mobile if it was open from cookie.
|
||||
useEffect(() => {
|
||||
const mq = window.matchMedia("(min-width: 640px)");
|
||||
setIsDesktop(mq.matches);
|
||||
setHasMounted(true);
|
||||
// If on mobile but sidebar was open from cookie, close it
|
||||
if (!mq.matches && defaultOpen) {
|
||||
setOpen(false);
|
||||
}
|
||||
const handler = (e: MediaQueryListEvent) => setIsDesktop(e.matches);
|
||||
mq.addEventListener("change", handler);
|
||||
return () => mq.removeEventListener("change", handler);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// Persist open state to cookie (only after mount to avoid overwriting on mobile)
|
||||
useEffect(() => {
|
||||
if (hasMounted) {
|
||||
setCookie("docs-chat-open", String(open));
|
||||
}
|
||||
}, [open, hasMounted]);
|
||||
|
||||
// Push page content on desktop when pane is open.
|
||||
// Use padding on body so the page scrollbar stays at the viewport edge (behind the sidebar)
|
||||
// instead of appearing right next to the sidebar's scrollbar.
|
||||
useEffect(() => {
|
||||
const body = document.body;
|
||||
if (isDesktop && open) {
|
||||
body.style.paddingRight = `${desktopWidth}px`;
|
||||
if (!isDraggingRef.current) {
|
||||
body.style.transition = "padding-right 150ms ease";
|
||||
}
|
||||
} else if (isDesktop) {
|
||||
body.style.paddingRight = "0px";
|
||||
body.style.transition = "padding-right 150ms ease";
|
||||
}
|
||||
return () => {
|
||||
body.style.paddingRight = "0px";
|
||||
body.style.transition = "";
|
||||
};
|
||||
}, [isDesktop, open, desktopWidth]);
|
||||
|
||||
// Resize handle drag
|
||||
const handleResizePointerDown = useCallback(
|
||||
(e: ReactPointerEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
isDraggingRef.current = true;
|
||||
document.documentElement.style.transition = "none";
|
||||
const startX = e.clientX;
|
||||
const startWidth = desktopWidth;
|
||||
|
||||
const onPointerMove = (ev: globalThis.PointerEvent) => {
|
||||
const delta = startX - ev.clientX;
|
||||
const newWidth = Math.min(
|
||||
DESKTOP_MAX_WIDTH,
|
||||
Math.max(DESKTOP_MIN_WIDTH, startWidth + delta),
|
||||
);
|
||||
setDesktopWidth(newWidth);
|
||||
};
|
||||
|
||||
const onPointerUp = () => {
|
||||
isDraggingRef.current = false;
|
||||
document.documentElement.style.transition = "";
|
||||
document.removeEventListener("pointermove", onPointerMove);
|
||||
document.removeEventListener("pointerup", onPointerUp);
|
||||
};
|
||||
|
||||
document.addEventListener("pointermove", onPointerMove);
|
||||
document.addEventListener("pointerup", onPointerUp);
|
||||
},
|
||||
[desktopWidth],
|
||||
);
|
||||
|
||||
// Persist width to cookie
|
||||
useEffect(() => {
|
||||
setCookie("docs-chat-width", String(desktopWidth));
|
||||
}, [desktopWidth]);
|
||||
|
||||
// Restore messages from sessionStorage on mount
|
||||
useEffect(() => {
|
||||
if (restoredRef.current) return;
|
||||
restoredRef.current = true;
|
||||
try {
|
||||
const stored = sessionStorage.getItem(STORAGE_KEY);
|
||||
if (stored) {
|
||||
const parsed = JSON.parse(stored);
|
||||
if (Array.isArray(parsed) && parsed.length > 0) {
|
||||
setMessages(parsed);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore parse errors
|
||||
}
|
||||
}, [setMessages]);
|
||||
|
||||
// Save completed messages to sessionStorage
|
||||
useEffect(() => {
|
||||
if (!restoredRef.current) return;
|
||||
if (isLoading) return;
|
||||
if (messages.length === 0) {
|
||||
sessionStorage.removeItem(STORAGE_KEY);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(messages));
|
||||
} catch {
|
||||
// ignore quota errors
|
||||
}
|
||||
}, [messages, isLoading]);
|
||||
|
||||
// Cmd+K to open sidebar and focus prompt, Escape to close
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "i" && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault();
|
||||
setOpen((prev) => {
|
||||
if (!prev) {
|
||||
setTimeout(() => inputRef.current?.focus(), 200);
|
||||
}
|
||||
return !prev;
|
||||
});
|
||||
}
|
||||
if (e.key === "Escape" && open && isDesktop) {
|
||||
setOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||
}, [open, isDesktop]);
|
||||
|
||||
// Auto-focus input when opened
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
const timer = setTimeout(() => inputRef.current?.focus(), 200);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
// Auto-open when error occurs
|
||||
useEffect(() => {
|
||||
if (error) setOpen(true);
|
||||
}, [error]);
|
||||
|
||||
// Scroll to bottom when messages change or error occurs
|
||||
useEffect(() => {
|
||||
const el = messagesScrollRef.current;
|
||||
if (!el) return;
|
||||
requestAnimationFrame(() => {
|
||||
el.scrollTop = el.scrollHeight;
|
||||
});
|
||||
}, [messages, error]);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!input.trim() || isLoading) return;
|
||||
sendMessage({ text: input });
|
||||
setInput("");
|
||||
},
|
||||
[input, isLoading, sendMessage],
|
||||
);
|
||||
|
||||
const handleClear = useCallback(() => {
|
||||
setMessages([]);
|
||||
sessionStorage.removeItem(STORAGE_KEY);
|
||||
}, [setMessages]);
|
||||
|
||||
const hasVisibleContent = (
|
||||
parts: (typeof messages)[number]["parts"],
|
||||
): boolean => {
|
||||
return parts.some(
|
||||
(p) => (p.type === "text" && p.text.length > 0) || isToolPart(p),
|
||||
);
|
||||
};
|
||||
|
||||
// Shared chat panel content used by both desktop and mobile
|
||||
const chatPanel = (
|
||||
<>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-border/50 shrink-0">
|
||||
<span className="text-sm font-medium">agent-browser Docs</span>
|
||||
<div className="flex items-center gap-3">
|
||||
{showMessages && (
|
||||
<button
|
||||
onClick={handleClear}
|
||||
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||
aria-label="Clear conversation"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setOpen(false)}
|
||||
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||
aria-label="Close panel"
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<line x1="18" y1="6" x2="6" y2="18" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content: suggestions or messages */}
|
||||
{showMessages ? (
|
||||
<div
|
||||
ref={messagesScrollRef}
|
||||
className="flex-1 min-h-0 p-4 space-y-4 overflow-y-auto"
|
||||
>
|
||||
{messages.map((message) => {
|
||||
if (!hasVisibleContent(message.parts)) return null;
|
||||
return (
|
||||
<div key={message.id}>
|
||||
{message.role === "user" ? (
|
||||
<div className="text-sm text-muted-foreground whitespace-pre-wrap leading-relaxed">
|
||||
{message.parts
|
||||
.filter(
|
||||
(p): p is Extract<typeof p, { type: "text" }> =>
|
||||
p.type === "text",
|
||||
)
|
||||
.map((p) => p.text)
|
||||
.join("")}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{message.parts.map((part, i) => {
|
||||
if (part.type === "text" && part.text) {
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className="docs-chat-content text-sm text-foreground leading-relaxed prose prose-sm dark:prose-invert max-w-none"
|
||||
>
|
||||
<Streamdown>{part.text}</Streamdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (isToolPart(part)) {
|
||||
return (
|
||||
<ToolCallDisplay key={part.toolCallId} part={part} />
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{error && (
|
||||
<div className="text-sm text-destructive/80 bg-destructive/10 rounded-md px-3 py-2">
|
||||
{(() => {
|
||||
try {
|
||||
const parsed = JSON.parse(error.message);
|
||||
return parsed.message || parsed.error || error.message;
|
||||
} catch {
|
||||
return (
|
||||
error.message || "Something went wrong. Please try again."
|
||||
);
|
||||
}
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-1 min-h-0 flex flex-col">
|
||||
<div className="flex flex-wrap gap-2 p-4">
|
||||
{SUGGESTIONS.map((s) => (
|
||||
<button
|
||||
key={s}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
sendMessage({ text: s });
|
||||
}}
|
||||
className="text-xs px-3 py-1.5 rounded-full border bg-secondary font-medium text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
{s}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Input bar */}
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="flex items-end gap-2 px-4 py-3 border-t border-border/50 shrink-0"
|
||||
>
|
||||
<textarea
|
||||
ref={inputRef}
|
||||
value={input}
|
||||
onChange={(e) => {
|
||||
setInput(e.target.value);
|
||||
e.target.style.height = "auto";
|
||||
e.target.style.height = `${e.target.scrollHeight}px`;
|
||||
}}
|
||||
rows={1}
|
||||
enterKeyHint="send"
|
||||
placeholder="Ask a question..."
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSubmit(e);
|
||||
}
|
||||
}}
|
||||
className="flex-1 bg-transparent text-base sm:text-sm text-foreground outline-none disabled:opacity-50 resize-none max-h-32 leading-relaxed placeholder:text-muted-foreground"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading || !input.trim()}
|
||||
className="bg-primary text-primary-foreground rounded-full p-1.5 hover:bg-primary/90 transition-colors disabled:opacity-30 shrink-0"
|
||||
aria-label="Send message"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<line x1="12" y1="19" x2="12" y2="5" />
|
||||
<polyline points="5 12 12 5 19 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Ask AI trigger button */}
|
||||
{!open && (
|
||||
<button
|
||||
onClick={() => setOpen(true)}
|
||||
className="fixed z-50 bottom-4 left-1/2 -translate-x-1/2 sm:left-auto sm:translate-x-0 sm:right-4 flex items-center gap-2 px-4 py-2 rounded-lg bg-primary text-primary-foreground shadow-lg hover:opacity-90 transition-opacity text-sm font-medium"
|
||||
aria-label="Ask AI"
|
||||
>
|
||||
Ask AI
|
||||
<kbd className="hidden sm:inline-flex items-center gap-0.5 text-xs opacity-60 font-mono">
|
||||
<span>⌘</span>I
|
||||
</kbd>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Desktop: resizable side pane -- always rendered, hidden on mobile via CSS */}
|
||||
<aside
|
||||
className={`hidden sm:flex fixed top-0 right-0 bottom-0 z-40 border-l border-border/50 bg-background transition-transform duration-150 ease-in-out ${open ? "translate-x-0" : "translate-x-full"}`}
|
||||
style={{ width: desktopWidth }}
|
||||
aria-hidden={!open}
|
||||
>
|
||||
{/* Resize handle */}
|
||||
<div
|
||||
onPointerDown={handleResizePointerDown}
|
||||
className="absolute top-0 bottom-0 left-0 w-1.5 cursor-col-resize hover:bg-ring/30 active:bg-ring/50 transition-colors z-10"
|
||||
/>
|
||||
<div className="flex flex-col flex-1 min-w-0">{chatPanel}</div>
|
||||
</aside>
|
||||
|
||||
{/* Mobile: Sheet overlay/drawer -- only after mount to avoid flash on desktop */}
|
||||
{hasMounted && !isDesktop && (
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetContent
|
||||
side="right"
|
||||
showCloseButton={false}
|
||||
overlayClassName="bg-background!"
|
||||
className="inset-0! w-full! h-full! max-w-none! border-l-0! p-0 flex flex-col"
|
||||
style={{ backgroundColor: "var(--background)", opacity: 1 }}
|
||||
>
|
||||
<SheetTitle className="sr-only">AI Chat</SheetTitle>
|
||||
{chatPanel}
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useMemo } from "react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import {
|
||||
Sheet,
|
||||
SheetTrigger,
|
||||
SheetContent,
|
||||
SheetTitle,
|
||||
} from "@/components/ui/sheet";
|
||||
import { navigation, allDocsPages } from "@/lib/docs-navigation";
|
||||
|
||||
export function DocsMobileNav() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const pathname = usePathname();
|
||||
|
||||
const currentPage = useMemo(() => {
|
||||
const page = allDocsPages.find((p) => p.href === pathname);
|
||||
return page ?? allDocsPages[0];
|
||||
}, [pathname]);
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger className="lg:hidden sticky top-14 z-40 w-full px-6 py-3 bg-background/80 backdrop-blur-sm border-b border-border flex items-center justify-between focus:outline-none">
|
||||
<div className="text-sm font-medium">{currentPage?.name}</div>
|
||||
<div className="w-8 h-8 flex items-center justify-center">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
<line x1="8" y1="6" x2="21" y2="6" />
|
||||
<line x1="8" y1="12" x2="21" y2="12" />
|
||||
<line x1="8" y1="18" x2="21" y2="18" />
|
||||
<line x1="3" y1="6" x2="3.01" y2="6" />
|
||||
<line x1="3" y1="12" x2="3.01" y2="12" />
|
||||
<line x1="3" y1="18" x2="3.01" y2="18" />
|
||||
</svg>
|
||||
</div>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="left" showCloseButton={false} className="overflow-y-auto p-6">
|
||||
<SheetTitle className="mb-6">Table of Contents</SheetTitle>
|
||||
<nav className="space-y-6">
|
||||
{navigation.map((section, sectionIndex) => (
|
||||
<div key={section.title ?? sectionIndex}>
|
||||
{section.title && (
|
||||
<h4 className="text-xs font-medium text-muted-foreground uppercase tracking-wider mb-2">
|
||||
{section.title}
|
||||
</h4>
|
||||
)}
|
||||
<ul className="space-y-1">
|
||||
{section.items.map((item) => (
|
||||
<li key={item.href}>
|
||||
<Link
|
||||
href={item.href}
|
||||
onClick={() => setOpen(false)}
|
||||
className={`text-sm block py-2 transition-colors ${
|
||||
pathname === item.href
|
||||
? "text-primary font-medium"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { navigation } from "@/lib/docs-navigation";
|
||||
|
||||
export function DocsSidebar() {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<nav className="space-y-6 pb-8">
|
||||
{navigation.map((section, sectionIndex) => (
|
||||
<div key={section.title ?? sectionIndex}>
|
||||
{section.title && (
|
||||
<h4 className="text-xs font-normal text-muted-foreground/50 uppercase tracking-wider mb-2">
|
||||
{section.title}
|
||||
</h4>
|
||||
)}
|
||||
<ul className="space-y-1">
|
||||
{section.items.map((item) => {
|
||||
const isActive = pathname === item.href;
|
||||
return (
|
||||
<li key={item.href}>
|
||||
<Link
|
||||
href={item.href}
|
||||
className={cn(
|
||||
"text-sm transition-colors block py-1",
|
||||
isActive
|
||||
? "text-primary font-medium"
|
||||
: "text-muted-foreground hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
import Link from "next/link";
|
||||
import { ThemeToggle } from "./theme-toggle";
|
||||
import { Search } from "./search";
|
||||
import { getStarCount } from "@/lib/github";
|
||||
|
||||
export async function Header() {
|
||||
const stars = await getStarCount();
|
||||
return (
|
||||
<header className="sticky top-0 z-50 bg-white/90 backdrop-blur-sm dark:bg-neutral-950/90">
|
||||
<div className="flex h-14 items-center justify-between px-4 gap-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<Link href="https://vercel.com" title="Made with love by Vercel">
|
||||
<svg
|
||||
data-testid="geist-icon"
|
||||
height="18"
|
||||
strokeLinejoin="round"
|
||||
viewBox="0 0 16 16"
|
||||
width="18"
|
||||
style={{ color: "currentcolor" }}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M8 1L16 15H0L8 1Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</Link>
|
||||
<span className="text-neutral-300 dark:text-neutral-700">
|
||||
<svg
|
||||
data-testid="geist-icon"
|
||||
height="16"
|
||||
strokeLinejoin="round"
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
style={{ color: "currentcolor" }}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M4.01526 15.3939L4.3107 14.7046L10.3107 0.704556L10.6061 0.0151978L11.9849 0.606077L11.6894 1.29544L5.68942 15.2954L5.39398 15.9848L4.01526 15.3939Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<Link href="/">
|
||||
<span
|
||||
className="font-medium tracking-tight text-lg"
|
||||
style={{ fontFamily: "var(--font-geist-pixel-square)" }}
|
||||
>
|
||||
agent-browser
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
<nav className="flex items-center gap-4">
|
||||
<Search />
|
||||
<a
|
||||
href="https://github.com/vercel-labs/agent-browser"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1.5 text-sm text-neutral-500 hover:text-neutral-900 transition-colors dark:text-neutral-400 dark:hover:text-neutral-100"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
className="h-4 w-4"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" />
|
||||
</svg>
|
||||
{stars && <span>{stars}</span>}
|
||||
</a>
|
||||
<a
|
||||
href="https://www.npmjs.com/package/agent-browser"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-neutral-500 hover:text-neutral-900 transition-colors dark:text-neutral-400 dark:hover:text-neutral-100"
|
||||
>
|
||||
npm
|
||||
</a>
|
||||
<ThemeToggle />
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -1,265 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type SearchResult = {
|
||||
title: string;
|
||||
href: string;
|
||||
section: string;
|
||||
snippet: string;
|
||||
};
|
||||
|
||||
export function Search() {
|
||||
const router = useRouter();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [query, setQuery] = useState("");
|
||||
const [results, setResults] = useState<SearchResult[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const listRef = useRef<HTMLDivElement>(null);
|
||||
const abortRef = useRef<AbortController | null>(null);
|
||||
|
||||
const navigate = useCallback(
|
||||
(href: string) => {
|
||||
setOpen(false);
|
||||
setQuery("");
|
||||
setResults([]);
|
||||
router.push(href);
|
||||
},
|
||||
[router],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
|
||||
e.preventDefault();
|
||||
setOpen((prev) => !prev);
|
||||
}
|
||||
}
|
||||
document.addEventListener("keydown", onKeyDown);
|
||||
return () => document.removeEventListener("keydown", onKeyDown);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setTimeout(() => inputRef.current?.focus(), 0);
|
||||
} else {
|
||||
setQuery("");
|
||||
setResults([]);
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
const q = query.trim();
|
||||
if (!q) {
|
||||
setResults([]);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
abortRef.current?.abort();
|
||||
const controller = new AbortController();
|
||||
abortRef.current = controller;
|
||||
|
||||
const timeout = setTimeout(async () => {
|
||||
try {
|
||||
const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`, {
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setResults(data.results);
|
||||
}
|
||||
} catch {
|
||||
// aborted or network error
|
||||
} finally {
|
||||
if (!controller.signal.aborted) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
}, 150);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
controller.abort();
|
||||
};
|
||||
}, [query]);
|
||||
|
||||
useEffect(() => {
|
||||
setActiveIndex(0);
|
||||
}, [results]);
|
||||
|
||||
function handleKeyDown(e: React.KeyboardEvent) {
|
||||
if (e.key === "ArrowDown") {
|
||||
e.preventDefault();
|
||||
setActiveIndex((i) => Math.min(i + 1, results.length - 1));
|
||||
} else if (e.key === "ArrowUp") {
|
||||
e.preventDefault();
|
||||
setActiveIndex((i) => Math.max(i - 1, 0));
|
||||
} else if (e.key === "Enter" && results[activeIndex]) {
|
||||
e.preventDefault();
|
||||
navigate(results[activeIndex].href);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const active = listRef.current?.querySelector("[data-active='true']");
|
||||
active?.scrollIntoView({ block: "nearest" });
|
||||
}, [activeIndex]);
|
||||
|
||||
const hasQuery = query.trim().length > 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setOpen(true)}
|
||||
className="hidden sm:flex items-center gap-2 rounded-md border border-border/50 bg-muted/50 px-3 py-1.5 text-sm text-muted-foreground hover:text-foreground hover:border-foreground/25 transition-colors"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
<path d="m21 21-4.3-4.3" />
|
||||
</svg>
|
||||
Search docs
|
||||
<kbd className="pointer-events-none ml-1 inline-flex items-center gap-0.5 rounded border border-border/50 bg-background px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground">
|
||||
<span>⌘</span>K
|
||||
</kbd>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setOpen(true)}
|
||||
className="sm:hidden flex items-center text-muted-foreground hover:text-foreground transition-colors"
|
||||
aria-label="Search docs"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
<path d="m21 21-4.3-4.3" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent showCloseButton={false} className="gap-0 p-0 sm:max-w-lg">
|
||||
<DialogTitle className="sr-only">Search documentation</DialogTitle>
|
||||
<div className="flex items-center gap-2 border-b border-border/50 px-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="shrink-0 text-muted-foreground"
|
||||
>
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
<path d="m21 21-4.3-4.3" />
|
||||
</svg>
|
||||
<input
|
||||
ref={inputRef}
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Search docs..."
|
||||
className="flex-1 bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground"
|
||||
/>
|
||||
{query && (
|
||||
<button
|
||||
onClick={() => setQuery("")}
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M18 6 6 18" />
|
||||
<path d="m6 6 12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref={listRef}
|
||||
className="max-h-[min(60vh,400px)] overflow-y-auto p-2"
|
||||
>
|
||||
{loading && hasQuery ? (
|
||||
<div className="flex items-center justify-center py-6">
|
||||
<div className="h-4 w-4 animate-spin rounded-full border-2 border-muted-foreground border-t-transparent" />
|
||||
</div>
|
||||
) : hasQuery && results.length === 0 ? (
|
||||
<p className="py-6 text-center text-sm text-muted-foreground">
|
||||
No results found.
|
||||
</p>
|
||||
) : !hasQuery ? (
|
||||
<p className="py-6 text-center text-sm text-muted-foreground">
|
||||
Type to search documentation...
|
||||
</p>
|
||||
) : (
|
||||
results.map((item, i) => (
|
||||
<button
|
||||
key={item.href}
|
||||
data-active={i === activeIndex}
|
||||
onClick={() => navigate(item.href)}
|
||||
onMouseEnter={() => setActiveIndex(i)}
|
||||
className={cn(
|
||||
"flex w-full flex-col gap-1 rounded-md px-3 py-2 text-left transition-colors",
|
||||
i === activeIndex
|
||||
? "bg-muted text-foreground"
|
||||
: "text-foreground",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-sm font-medium">{item.title}</span>
|
||||
{item.section && (
|
||||
<span className="shrink-0 text-xs text-muted-foreground">
|
||||
{item.section}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{item.snippet && (
|
||||
<span className="line-clamp-2 text-xs text-muted-foreground leading-relaxed">
|
||||
{item.snippet}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
||||
|
||||
export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<NextThemesProvider
|
||||
attribute="class"
|
||||
defaultTheme="dark"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
{children}
|
||||
</NextThemesProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export function ThemeToggle() {
|
||||
const { theme, setTheme } = useTheme();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!mounted) {
|
||||
return <div className="w-8 h-8" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
|
||||
className="w-8 h-8 flex items-center justify-center rounded-md text-neutral-500 hover:text-neutral-900 hover:bg-neutral-100 transition-colors dark:text-neutral-400 dark:hover:text-neutral-100 dark:hover:bg-neutral-800"
|
||||
aria-label="Toggle theme"
|
||||
>
|
||||
{theme === "dark" ? (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="4" />
|
||||
<path d="M12 2v2" />
|
||||
<path d="M12 20v2" />
|
||||
<path d="m4.93 4.93 1.41 1.41" />
|
||||
<path d="m17.66 17.66 1.41 1.41" />
|
||||
<path d="M2 12h2" />
|
||||
<path d="M20 12h2" />
|
||||
<path d="m6.34 17.66-1.41 1.41" />
|
||||
<path d="m19.07 4.93-1.41 1.41" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { Dialog as DialogPrimitive } from "radix-ui";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Dialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
|
||||
}
|
||||
|
||||
function DialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
|
||||
}
|
||||
|
||||
function DialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<DialogPrimitive.Overlay
|
||||
data-slot="dialog-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogContent({
|
||||
className,
|
||||
children,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||
showCloseButton?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border border-border/50 p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<line x1="18" y1="6" x2="6" y2="18" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" />
|
||||
</svg>
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
data-slot="dialog-title"
|
||||
className={cn("text-foreground font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Dialog, DialogPortal, DialogOverlay, DialogContent, DialogTitle };
|
||||
@@ -1,147 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Dialog as SheetPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
|
||||
return <SheetPrimitive.Root data-slot="sheet" {...props} />
|
||||
}
|
||||
|
||||
function SheetTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
|
||||
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
|
||||
}
|
||||
|
||||
function SheetClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
|
||||
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
|
||||
}
|
||||
|
||||
function SheetPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
|
||||
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
|
||||
}
|
||||
|
||||
function SheetOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
|
||||
return (
|
||||
<SheetPrimitive.Overlay
|
||||
data-slot="sheet-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetContent({
|
||||
className,
|
||||
children,
|
||||
side = "right",
|
||||
showCloseButton = true,
|
||||
overlayClassName,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
||||
side?: "top" | "right" | "bottom" | "left"
|
||||
showCloseButton?: boolean
|
||||
overlayClassName?: string
|
||||
}) {
|
||||
return (
|
||||
<SheetPortal>
|
||||
<SheetOverlay className={overlayClassName} />
|
||||
<SheetPrimitive.Content
|
||||
data-slot="sheet-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
||||
side === "right" &&
|
||||
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
|
||||
side === "left" &&
|
||||
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r border-border/50 sm:max-w-sm",
|
||||
side === "top" &&
|
||||
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
|
||||
side === "bottom" &&
|
||||
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" />
|
||||
</svg>
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
)}
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sheet-header"
|
||||
className={cn("flex flex-col gap-1.5 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sheet-footer"
|
||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
|
||||
return (
|
||||
<SheetPrimitive.Title
|
||||
data-slot="sheet-title"
|
||||
className={cn("text-foreground font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
|
||||
return (
|
||||
<SheetPrimitive.Description
|
||||
data-slot="sheet-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Sheet,
|
||||
SheetTrigger,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
export type NavItem = {
|
||||
name: string;
|
||||
href: string;
|
||||
};
|
||||
|
||||
export type NavSection = {
|
||||
title: string | null;
|
||||
items: NavItem[];
|
||||
};
|
||||
|
||||
export const navigation: NavSection[] = [
|
||||
{
|
||||
title: null,
|
||||
items: [
|
||||
{ name: "Introduction", href: "/" },
|
||||
{ name: "Installation", href: "/installation" },
|
||||
{ name: "Quick Start", href: "/quick-start" },
|
||||
{ name: "Skills", href: "/skills" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Reference",
|
||||
items: [
|
||||
{ name: "Commands", href: "/commands" },
|
||||
{ name: "Configuration", href: "/configuration" },
|
||||
{ name: "Selectors", href: "/selectors" },
|
||||
{ name: "Snapshots", href: "/snapshots" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Features",
|
||||
items: [
|
||||
{ name: "Sessions", href: "/sessions" },
|
||||
{ name: "Dashboard", href: "/dashboard" },
|
||||
{ name: "Diffing", href: "/diffing" },
|
||||
{ name: "CDP Mode", href: "/cdp-mode" },
|
||||
{ name: "Streaming", href: "/streaming" },
|
||||
{ name: "Profiler", href: "/profiler" },
|
||||
{ name: "iOS Simulator", href: "/ios" },
|
||||
{ name: "Security", href: "/security" },
|
||||
{ name: "Next.js + Vercel", href: "/next" },
|
||||
{ name: "Native Mode", href: "/native-mode" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Providers",
|
||||
items: [
|
||||
{ name: "AgentCore", href: "/providers/agentcore" },
|
||||
{ name: "Browser Use", href: "/providers/browser-use" },
|
||||
{ name: "Browserbase", href: "/providers/browserbase" },
|
||||
{ name: "Browserless", href: "/providers/browserless" },
|
||||
{ name: "Kernel", href: "/providers/kernel" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Engines",
|
||||
items: [
|
||||
{ name: "Chrome", href: "/engines/chrome" },
|
||||
{ name: "Lightpanda", href: "/engines/lightpanda" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: null,
|
||||
items: [{ name: "Changelog", href: "/changelog" }],
|
||||
},
|
||||
];
|
||||
|
||||
export const allDocsPages: NavItem[] = navigation.flatMap(
|
||||
(section) => section.items
|
||||
);
|
||||
@@ -1,47 +0,0 @@
|
||||
/**
|
||||
* Converts raw MDX content to clean Markdown suitable for AI agents.
|
||||
*
|
||||
* Strips export/import statements and standalone JSX divs with className
|
||||
* attributes, passing everything else through as valid Markdown.
|
||||
*/
|
||||
export function mdxToCleanMarkdown(raw: string): string {
|
||||
const lines = raw.split("\n");
|
||||
const out: string[] = [];
|
||||
let inJsxBlock = false;
|
||||
let jsxDepth = 0;
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
|
||||
if (trimmed.startsWith("export ") || trimmed.startsWith("import ")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
!inJsxBlock &&
|
||||
trimmed.startsWith("<div ") &&
|
||||
trimmed.includes("className=")
|
||||
) {
|
||||
inJsxBlock = true;
|
||||
jsxDepth = 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inJsxBlock) {
|
||||
const opens = (line.match(/<div[\s>]/g) || []).length;
|
||||
const closes = (line.match(/<\/div>/g) || []).length;
|
||||
jsxDepth += opens - closes;
|
||||
if (jsxDepth <= 0) {
|
||||
inJsxBlock = false;
|
||||
jsxDepth = 0;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
out.push(line);
|
||||
}
|
||||
|
||||
let result = out.join("\n");
|
||||
result = result.replace(/^\n+/, "\n").trim();
|
||||
return result;
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import type { Metadata } from "next";
|
||||
import { PAGE_TITLES } from "./page-titles";
|
||||
|
||||
const DESCRIPTION =
|
||||
"Browser automation CLI for AI agents";
|
||||
|
||||
export function pageMetadata(slug: string): Metadata {
|
||||
const title = PAGE_TITLES[slug];
|
||||
if (!title) return {};
|
||||
|
||||
const displayTitle = title.replace(/\n/g, " ");
|
||||
const fullTitle = `${displayTitle} | agent-browser`;
|
||||
const ogImageUrl = slug ? `/og/${slug}` : "/og";
|
||||
|
||||
return {
|
||||
title: displayTitle,
|
||||
openGraph: {
|
||||
type: "website",
|
||||
locale: "en_US",
|
||||
siteName: "agent-browser",
|
||||
title: fullTitle,
|
||||
description: DESCRIPTION,
|
||||
images: [
|
||||
{
|
||||
url: ogImageUrl,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: `${displayTitle} - agent-browser`,
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: fullTitle,
|
||||
description: DESCRIPTION,
|
||||
images: [ogImageUrl],
|
||||
},
|
||||
};
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user