Compare commits
238
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a257ad2c1 | ||
|
|
d97e2016f5 | ||
|
|
6aea316c82 | ||
|
|
c7fa10cb1b | ||
|
|
44c0361fcd | ||
|
|
907ca8c808 | ||
|
|
b304a4188c | ||
|
|
e912f541f2 | ||
|
|
7238b7da4c | ||
|
|
79d8dfe34c | ||
|
|
14ec5b5ffa | ||
|
|
74fda70b67 | ||
|
|
2a397de59f | ||
|
|
bf672ee7f9 | ||
|
|
74910cfef1 | ||
|
|
41830dff71 | ||
|
|
79b05877a8 | ||
|
|
7bd8ce937b | ||
|
|
b455a58aa2 | ||
|
|
b59dc4c82c | ||
|
|
2e38882664 | ||
|
|
bc1e917e87 | ||
|
|
e005c7251b | ||
|
|
11eab471f1 | ||
|
|
aa256e30c7 | ||
|
|
c0e2b80f8c | ||
|
|
f319195974 | ||
|
|
77f2caa1bc | ||
|
|
6f1dd39121 | ||
|
|
85d18799a4 | ||
|
|
43e781a8d3 | ||
|
|
25e8719e51 | ||
|
|
ec011f46ff | ||
|
|
aef8fcc038 | ||
|
|
96582b79fd | ||
|
|
058a286326 | ||
|
|
b1f27236d8 | ||
|
|
a5a9327b7d | ||
|
|
699ccbd3cb | ||
|
|
893ddfd259 | ||
|
|
ea2e93dbba | ||
|
|
4c6afe3e69 | ||
|
|
9f9a90cf63 | ||
|
|
0443e4ed7a | ||
|
|
c5b2292caa | ||
|
|
2ed0c6f8ec | ||
|
|
3a91aef4c9 | ||
|
|
02ebc9f328 | ||
|
|
955543b757 | ||
|
|
ecad112707 | ||
|
|
8932f28926 | ||
|
|
2fe7394dbe | ||
|
|
b7665e52b6 | ||
|
|
16c4ef2da6 | ||
|
|
ad6e206a90 | ||
|
|
f10f3f6425 | ||
|
|
c0f8f32a55 | ||
|
|
12d79e4428 | ||
|
|
467b830974 | ||
|
|
4412899379 | ||
|
|
fca9d7ab5d | ||
|
|
2b8a51b9a6 | ||
|
|
ebd87173e4 | ||
|
|
d5a667ea2d | ||
|
|
9732031087 | ||
|
|
69ffad0f04 | ||
|
|
e2e259f1e2 | ||
|
|
06a32f4191 | ||
|
|
c6fc7df443 | ||
|
|
98f49da196 | ||
|
|
85340cb432 | ||
|
|
5dc40b4ea4 | ||
|
|
59fa36b6e2 | ||
|
|
9ca182a4df | ||
|
|
76df589aea | ||
|
|
19dd2d0c0b | ||
|
|
f9b33ac23d | ||
|
|
01efe418af | ||
|
|
b7b0da5dfa | ||
|
|
d441843cca | ||
|
|
9cbb363190 | ||
|
|
1112a160bd | ||
|
|
697b788af0 | ||
|
|
cdd10ebb54 | ||
|
|
323b6cdd9d | ||
|
|
604c0b9632 | ||
|
|
4b776c7ba6 | ||
|
|
9a01e8b3b5 | ||
|
|
9c20979bfe | ||
|
|
14029d2450 | ||
|
|
d03e238516 | ||
|
|
221d22c14f | ||
|
|
b3b9fccd72 | ||
|
|
ec9c6a2ed9 | ||
|
|
03a8cb95d0 | ||
|
|
66a11aeb4c | ||
|
|
ffe29b8a26 | ||
|
|
76d23db1a9 | ||
|
|
67cdc293f0 | ||
|
|
dc53fedac0 | ||
|
|
cd4473aa64 | ||
|
|
8e5ead85c8 | ||
|
|
e8ceafcbe1 | ||
|
|
4d8097a56f | ||
|
|
76c30690f5 | ||
|
|
ae349451b7 | ||
|
|
07c2372766 | ||
|
|
74be667c80 | ||
|
|
d34ce8c2d0 | ||
|
|
79ef5764fa | ||
|
|
9d021bdf62 | ||
|
|
a1b992411e | ||
|
|
3c6ae7df9d | ||
|
|
daeede49c5 | ||
|
|
03eea8a90f | ||
|
|
de859d8f6b | ||
|
|
17dba8f7a8 | ||
|
|
0dc36f2cff | ||
|
|
f770593c66 | ||
|
|
27715884e5 | ||
|
|
e52aa49706 | ||
|
|
9c45f82193 | ||
|
|
bdf674a27e | ||
|
|
d24f753f51 | ||
|
|
c00dd44750 | ||
|
|
97fd2828b5 | ||
|
|
d75350a99e | ||
|
|
775f166bce | ||
|
|
32a0207ffa | ||
|
|
cb2f8c3f73 | ||
|
|
3d24ea38fa | ||
|
|
71a79f64e8 | ||
|
|
72cbdc7f89 | ||
|
|
759302ead5 | ||
|
|
3f74bd2171 | ||
|
|
3ce441bc4e | ||
|
|
523d7d57f1 | ||
|
|
4116a8ac7f | ||
|
|
18a1abda6e | ||
|
|
28950b8ad2 | ||
|
|
7e6336f65b | ||
|
|
143c8a8f3e | ||
|
|
0256c8f2e9 | ||
|
|
ddfaa392e4 | ||
|
|
8eec634c6f | ||
|
|
6a17379aaf | ||
|
|
bf5ba0a557 | ||
|
|
efb1923fbb | ||
|
|
9a1cc0ed6a | ||
|
|
e0597304ec | ||
|
|
e831b07f47 | ||
|
|
12abdbd671 | ||
|
|
1b26ff886c | ||
|
|
f862e2f7df | ||
|
|
fcee8f70d1 | ||
|
|
a99f59cd20 | ||
|
|
45506fbff0 | ||
|
|
a22af0e675 | ||
|
|
79863a5180 | ||
|
|
ae09fdd431 | ||
|
|
53187a603c | ||
|
|
60534dfd63 | ||
|
|
a4d0c2624b | ||
|
|
36ea8ecb55 | ||
|
|
d10fd2d545 | ||
|
|
8c2a6ec5d2 | ||
|
|
c0fd1be132 | ||
|
|
957b5e5994 | ||
|
|
65d4df84ac | ||
|
|
161d8f5c8d | ||
|
|
f3ed1be409 | ||
|
|
6afede28b3 | ||
|
|
6f1c83de1b | ||
|
|
28129df124 | ||
|
|
eb8325e9b4 | ||
|
|
9281f46823 | ||
|
|
316e649740 | ||
|
|
35d345b2b4 | ||
|
|
fff4312d16 | ||
|
|
57dc7602fc | ||
|
|
ea17db8564 | ||
|
|
f74924cd0c | ||
|
|
9f3c3ad933 | ||
|
|
c046de2ec7 | ||
|
|
55f4eaa728 | ||
|
|
307f970d53 | ||
|
|
36cca10c10 | ||
|
|
c6a92a1472 | ||
|
|
c4f66a5922 | ||
|
|
946d236d9f | ||
|
|
cb37630ccf | ||
|
|
61c004db94 | ||
|
|
083a946aac | ||
|
|
e892bceadf | ||
|
|
c4139fa389 | ||
|
|
7123d46e7f | ||
|
|
399fd7a434 | ||
|
|
62f9b4dd6b | ||
|
|
818d9fa95e | ||
|
|
a8dcbb1222 | ||
|
|
a9fcef4579 | ||
|
|
59baf97e51 | ||
|
|
d02ef66c89 | ||
|
|
1689cf9eca | ||
|
|
03a53c9f36 | ||
|
|
b1c0c6a366 | ||
|
|
c88734da89 | ||
|
|
28740acecf | ||
|
|
4112234371 | ||
|
|
5e08e5d077 | ||
|
|
42879c337a | ||
|
|
412ac63b68 | ||
|
|
e6e832d2bc | ||
|
|
b19ca760aa | ||
|
|
e7c4936bc7 | ||
|
|
7aad47d3bd | ||
|
|
e196ed3e35 | ||
|
|
1f31452fea | ||
|
|
3675e6bd7a | ||
|
|
fff9a146bd | ||
|
|
34dcb7195a | ||
|
|
7bdfcf8541 | ||
|
|
6abee37641 | ||
|
|
7b43d408da | ||
|
|
2dc093cd62 | ||
|
|
673e2e266e | ||
|
|
4713c8b520 | ||
|
|
b4bc761168 | ||
|
|
6eafe50952 | ||
|
|
95675e9d55 | ||
|
|
97b17c98fb | ||
|
|
57a04385c1 | ||
|
|
1a88d7f585 | ||
|
|
4f6fd8ec5c | ||
|
|
3cd0ab468f | ||
|
|
a4fcc1c198 | ||
|
|
574037080c | ||
|
|
278466764b |
@@ -0,0 +1,23 @@
|
||||
# Changesets
|
||||
|
||||
This project uses [Changesets](https://github.com/changesets/changesets) for versioning and changelog generation.
|
||||
|
||||
## Adding a changeset
|
||||
|
||||
When you make a change that should be released, run:
|
||||
|
||||
```bash
|
||||
pnpm changeset
|
||||
```
|
||||
|
||||
This will prompt you to:
|
||||
1. Select the type of change (patch, minor, major)
|
||||
2. Write a summary of your changes
|
||||
|
||||
The changeset file will be committed with your PR.
|
||||
|
||||
## Release process
|
||||
|
||||
When changesets are merged to `main`, the release workflow will:
|
||||
1. Create a "Version Packages" PR that updates version numbers and changelogs
|
||||
2. When that PR is merged, packages are automatically published to npm
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
|
||||
"changelog": "@changesets/cli/changelog",
|
||||
"commit": false,
|
||||
"fixed": [],
|
||||
"linked": [],
|
||||
"access": "public",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": []
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
|
||||
"name": "agent-browser",
|
||||
"description": "Headless browser automation for AI agents",
|
||||
"owner": {
|
||||
"name": "Vercel",
|
||||
"email": "support@vercel.com"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "agent-browser",
|
||||
"description": "Automates browser interactions for web testing, form filling, screenshots, and data extraction",
|
||||
"source": "./",
|
||||
"strict": false,
|
||||
"skills": ["./skills/agent-browser"],
|
||||
"category": "development"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -7,6 +7,16 @@ on:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
version-sync:
|
||||
name: Version Sync Check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check version sync
|
||||
run: node scripts/check-version-sync.js
|
||||
|
||||
typescript:
|
||||
name: TypeScript (Node ${{ matrix.node-version }})
|
||||
runs-on: ubuntu-latest
|
||||
@@ -83,3 +93,216 @@ jobs:
|
||||
|
||||
- name: Build release binary
|
||||
run: cargo build --release --manifest-path cli/Cargo.toml --target ${{ matrix.target }}
|
||||
|
||||
- name: Run Rust tests
|
||||
run: cargo test --manifest-path cli/Cargo.toml --target ${{ matrix.target }}
|
||||
|
||||
windows-integration:
|
||||
name: Windows Integration Test
|
||||
runs-on: windows-latest
|
||||
needs: rust
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
|
||||
- name: Setup Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: x86_64-pc-windows-msvc
|
||||
|
||||
- name: Cache Cargo dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
cli/target/
|
||||
key: windows-cargo-x86_64-pc-windows-msvc-${{ hashFiles('cli/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
windows-cargo-x86_64-pc-windows-msvc-
|
||||
|
||||
- name: Build Rust CLI
|
||||
run: cargo build --release --manifest-path cli/Cargo.toml --target x86_64-pc-windows-msvc
|
||||
|
||||
- name: Install npm dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build TypeScript
|
||||
run: pnpm build
|
||||
|
||||
- name: Copy CLI binary to bin directory
|
||||
run: |
|
||||
Copy-Item cli/target/x86_64-pc-windows-msvc/release/agent-browser.exe bin/agent-browser-win32-x64.exe
|
||||
|
||||
- name: Test agent-browser install command
|
||||
run: |
|
||||
$env:PATH = "$pwd\bin;$env:PATH"
|
||||
for ($i = 1; $i -le 3; $i++) {
|
||||
bin/agent-browser-win32-x64.exe install
|
||||
if ($LASTEXITCODE -eq 0) { exit 0 }
|
||||
Write-Host "Attempt $i failed, retrying in 10 seconds..."
|
||||
Start-Sleep -Seconds 10
|
||||
}
|
||||
exit 1
|
||||
shell: pwsh
|
||||
timeout-minutes: 10
|
||||
|
||||
- name: Verify Chromium was installed
|
||||
run: |
|
||||
$playwrightPath = "$env:LOCALAPPDATA\ms-playwright"
|
||||
if (Test-Path $playwrightPath) {
|
||||
Write-Host "Playwright browsers installed at: $playwrightPath"
|
||||
Get-ChildItem $playwrightPath -Recurse -Depth 2 | Select-Object -First 20
|
||||
} else {
|
||||
Write-Error "Playwright browsers not found!"
|
||||
exit 1
|
||||
}
|
||||
shell: pwsh
|
||||
|
||||
serverless-chromium:
|
||||
name: Serverless Chromium (@sparticuz/chromium)
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Install @sparticuz/chromium
|
||||
run: pnpm add -D @sparticuz/chromium
|
||||
|
||||
- name: Build TypeScript
|
||||
run: pnpm build
|
||||
|
||||
- name: Run serverless integration test
|
||||
run: pnpm exec vitest run test/serverless.test.ts
|
||||
|
||||
global-install:
|
||||
name: Global Install (${{ matrix.os }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: rust
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
target: x86_64-unknown-linux-gnu
|
||||
binary: agent-browser-linux-x64
|
||||
- os: macos-latest
|
||||
target: aarch64-apple-darwin
|
||||
binary: agent-browser-darwin-arm64
|
||||
- os: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
binary: agent-browser-win32-x64.exe
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
|
||||
- name: Setup Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Cache Cargo dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
cli/target/
|
||||
key: ${{ runner.os }}-cargo-${{ matrix.target }}-${{ hashFiles('cli/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-cargo-${{ matrix.target }}-
|
||||
|
||||
- name: Build Rust CLI
|
||||
run: cargo build --release --manifest-path cli/Cargo.toml --target ${{ matrix.target }}
|
||||
|
||||
- name: Install npm dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build TypeScript
|
||||
run: pnpm build
|
||||
|
||||
- name: Copy CLI binary to bin directory (Unix)
|
||||
if: runner.os != 'Windows'
|
||||
run: cp cli/target/${{ matrix.target }}/release/agent-browser bin/${{ matrix.binary }}
|
||||
|
||||
- name: Copy CLI binary to bin directory (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
run: Copy-Item cli/target/${{ matrix.target }}/release/agent-browser.exe bin/${{ matrix.binary }}
|
||||
|
||||
- name: Test npm global install
|
||||
run: |
|
||||
npm pack
|
||||
npm install -g agent-browser-*.tgz
|
||||
agent-browser --version
|
||||
shell: bash
|
||||
|
||||
- name: Verify symlink points to native binary (Unix)
|
||||
if: runner.os != 'Windows'
|
||||
run: |
|
||||
SYMLINK=$(npm prefix -g)/bin/agent-browser
|
||||
TARGET=$(readlink "$SYMLINK")
|
||||
echo "Symlink: $SYMLINK"
|
||||
echo "Target: $TARGET"
|
||||
if [[ "$TARGET" != *"${{ matrix.binary }}"* ]]; then
|
||||
echo "ERROR: Symlink should point to native binary, not JS wrapper"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ Symlink correctly points to native binary"
|
||||
shell: bash
|
||||
|
||||
- name: Verify shim points to native binary (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
run: |
|
||||
$shimPath = "$(npm prefix -g)\agent-browser.cmd"
|
||||
$content = Get-Content $shimPath -Raw
|
||||
echo "Shim path: $shimPath"
|
||||
echo "Shim content:"
|
||||
echo $content
|
||||
if ($content -notmatch "agent-browser-win32-x64\.exe") {
|
||||
echo "ERROR: Shim should point to native .exe, not JS wrapper"
|
||||
exit 1
|
||||
}
|
||||
echo "✓ Shim correctly points to native binary"
|
||||
shell: pwsh
|
||||
|
||||
@@ -0,0 +1,314 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
# Build native binaries for all platforms first
|
||||
build-binaries:
|
||||
name: Build ${{ matrix.name }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- name: Linux x64
|
||||
os: ubuntu-latest
|
||||
target: x86_64-unknown-linux-gnu
|
||||
binary: agent-browser-linux-x64
|
||||
use_zigbuild: true
|
||||
- name: Linux ARM64
|
||||
os: ubuntu-latest
|
||||
target: aarch64-unknown-linux-gnu
|
||||
binary: agent-browser-linux-arm64
|
||||
use_zigbuild: true
|
||||
- name: Windows x64
|
||||
os: ubuntu-latest
|
||||
target: x86_64-pc-windows-gnu
|
||||
binary: agent-browser-win32-x64.exe
|
||||
use_zigbuild: false
|
||||
- name: macOS x64
|
||||
os: macos-latest
|
||||
target: x86_64-apple-darwin
|
||||
binary: agent-browser-darwin-x64
|
||||
use_zigbuild: false
|
||||
- name: macOS ARM64
|
||||
os: macos-latest
|
||||
target: aarch64-apple-darwin
|
||||
binary: agent-browser-darwin-arm64
|
||||
use_zigbuild: false
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: pnpm
|
||||
|
||||
- name: Install npm dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Sync version
|
||||
run: pnpm run version:sync
|
||||
|
||||
- name: Setup Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Install cross-compilation tools (Linux)
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y gcc-aarch64-linux-gnu gcc-x86-64-linux-gnu mingw-w64
|
||||
|
||||
- name: Install cargo-zigbuild
|
||||
if: matrix.use_zigbuild
|
||||
run: |
|
||||
pip3 install ziglang
|
||||
cargo install cargo-zigbuild
|
||||
|
||||
- name: Configure Rust linkers
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
mkdir -p ~/.cargo
|
||||
cat >> ~/.cargo/config.toml << 'EOF'
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "aarch64-linux-gnu-gcc"
|
||||
|
||||
[target.x86_64-pc-windows-gnu]
|
||||
linker = "x86_64-w64-mingw32-gcc"
|
||||
EOF
|
||||
|
||||
- name: Cache Cargo dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
cli/target/
|
||||
key: ${{ runner.os }}-cargo-${{ matrix.target }}-${{ hashFiles('cli/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-cargo-${{ matrix.target }}-
|
||||
|
||||
- name: Build with zigbuild
|
||||
if: matrix.use_zigbuild
|
||||
run: cargo zigbuild --release --manifest-path cli/Cargo.toml --target ${{ matrix.target }}
|
||||
|
||||
- name: Build with cargo
|
||||
if: '!matrix.use_zigbuild'
|
||||
run: cargo build --release --manifest-path cli/Cargo.toml --target ${{ matrix.target }}
|
||||
|
||||
- name: Copy binary
|
||||
run: |
|
||||
mkdir -p artifacts
|
||||
if [[ "${{ matrix.target }}" == *"windows"* ]]; then
|
||||
cp cli/target/${{ matrix.target }}/release/agent-browser.exe artifacts/${{ matrix.binary }}
|
||||
else
|
||||
cp cli/target/${{ matrix.target }}/release/agent-browser artifacts/${{ matrix.binary }}
|
||||
chmod +x artifacts/${{ matrix.binary }}
|
||||
fi
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.binary }}
|
||||
path: artifacts/${{ matrix.binary }}
|
||||
retention-days: 7
|
||||
|
||||
# Create release PR or publish to npm (with binaries)
|
||||
release:
|
||||
name: Release
|
||||
needs: build-binaries
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
published: ${{ steps.publish_metadata.outputs.published }}
|
||||
publishedPackages: ${{ steps.publish_metadata.outputs.publishedPackages }}
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: pnpm
|
||||
|
||||
- name: Install Dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Download all binary artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts/
|
||||
|
||||
- name: Move binaries to bin directory
|
||||
run: |
|
||||
mkdir -p bin
|
||||
find artifacts -type f -name 'agent-browser-*' -exec mv {} bin/ \;
|
||||
rm -rf artifacts
|
||||
chmod +x bin/agent-browser-* 2>/dev/null || true
|
||||
echo "Binaries in bin/:"
|
||||
ls -la bin/
|
||||
|
||||
- name: Verify all binaries exist
|
||||
run: |
|
||||
EXPECTED_BINARIES=(
|
||||
"agent-browser-linux-x64"
|
||||
"agent-browser-linux-arm64"
|
||||
"agent-browser-win32-x64.exe"
|
||||
"agent-browser-darwin-x64"
|
||||
"agent-browser-darwin-arm64"
|
||||
)
|
||||
MIN_SIZE=100000 # Binaries should be at least 100KB
|
||||
ERRORS=0
|
||||
for binary in "${EXPECTED_BINARIES[@]}"; do
|
||||
if [ ! -f "bin/$binary" ]; then
|
||||
echo "ERROR: Missing bin/$binary"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
SIZE=$(stat -c%s "bin/$binary" 2>/dev/null || stat -f%z "bin/$binary")
|
||||
if [ "$SIZE" -lt "$MIN_SIZE" ]; then
|
||||
echo "ERROR: bin/$binary is too small ($SIZE bytes, expected >= $MIN_SIZE)"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo "OK: bin/$binary ($SIZE bytes)"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
if [ "$ERRORS" -gt 0 ]; then
|
||||
echo "Error: $ERRORS binary issues found"
|
||||
exit 1
|
||||
fi
|
||||
echo "All 5 platform binaries present and valid"
|
||||
|
||||
- name: Create Release Pull Request or Publish to npm
|
||||
id: changesets
|
||||
uses: changesets/action@v1
|
||||
with:
|
||||
version: pnpm ci:version
|
||||
title: 'chore: version packages'
|
||||
commit: 'chore: version packages'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Check if publish is needed
|
||||
id: publish_check
|
||||
if: steps.changesets.outputs.hasChangesets == 'false'
|
||||
run: |
|
||||
LOCAL_VERSION=$(node -p "require('./package.json').version")
|
||||
REMOTE_VERSION=$(npm view agent-browser-stealth version 2>/dev/null || echo "")
|
||||
echo "local_version=$LOCAL_VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "remote_version=$REMOTE_VERSION" >> "$GITHUB_OUTPUT"
|
||||
if [ "$LOCAL_VERSION" != "$REMOTE_VERSION" ]; then
|
||||
echo "needs_publish=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "needs_publish=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
echo "Local: $LOCAL_VERSION"
|
||||
echo "Remote: ${REMOTE_VERSION:-<none>}"
|
||||
|
||||
- name: Publish to npm (trusted publishing)
|
||||
id: publish_npm
|
||||
if: steps.changesets.outputs.hasChangesets == 'false' && steps.publish_check.outputs.needs_publish == 'true'
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ""
|
||||
NPM_CONFIG_USERCONFIG: /home/runner/work/_temp/trusted-npmrc
|
||||
NPM_CONFIG_PROVENANCE: "true"
|
||||
run: |
|
||||
npm install -g npm@^11
|
||||
npm --version
|
||||
printf "registry=https://registry.npmjs.org/\n" > "$NPM_CONFIG_USERCONFIG"
|
||||
pnpm ci:publish
|
||||
|
||||
- name: Set release outputs
|
||||
id: publish_metadata
|
||||
run: |
|
||||
if [ "${{ steps.publish_npm.outcome }}" = "success" ]; then
|
||||
echo "published=true" >> "$GITHUB_OUTPUT"
|
||||
echo "publishedPackages=[{\"name\":\"agent-browser-stealth\",\"version\":\"${{ steps.publish_check.outputs.local_version }}\"}]" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "published=false" >> "$GITHUB_OUTPUT"
|
||||
echo "publishedPackages=[]" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
# Create GitHub release with binaries after npm publish
|
||||
github-release:
|
||||
name: Create GitHub Release
|
||||
needs: release
|
||||
if: needs.release.outputs.published == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts/
|
||||
|
||||
- name: Move binaries to bin directory
|
||||
run: |
|
||||
mkdir -p bin
|
||||
find artifacts -type f -name 'agent-browser-*' -exec mv {} bin/ \;
|
||||
rm -rf artifacts
|
||||
chmod +x bin/agent-browser-* 2>/dev/null || true
|
||||
ls -la bin/
|
||||
|
||||
- name: Verify binaries exist
|
||||
run: |
|
||||
BINARY_COUNT=$(ls bin/agent-browser-* 2>/dev/null | wc -l)
|
||||
if [ "$BINARY_COUNT" -lt 5 ]; then
|
||||
echo "Error: Expected 5 binaries, found $BINARY_COUNT"
|
||||
ls -la bin/
|
||||
exit 1
|
||||
fi
|
||||
echo "Found $BINARY_COUNT binaries"
|
||||
|
||||
- name: Create GitHub Release
|
||||
run: |
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
TAG="v$VERSION"
|
||||
|
||||
# Check if release already exists
|
||||
if gh release view "$TAG" &>/dev/null; then
|
||||
echo "Release $TAG already exists, uploading binaries..."
|
||||
gh release upload "$TAG" bin/agent-browser-* --clobber
|
||||
else
|
||||
echo "Creating release $TAG..."
|
||||
gh release create "$TAG" \
|
||||
--title "$TAG" \
|
||||
--generate-notes \
|
||||
bin/agent-browser-*
|
||||
fi
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
+14
@@ -27,10 +27,15 @@ npm-debug.log*
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
|
||||
# Test artifacts
|
||||
*.png
|
||||
*.jpeg
|
||||
*.jpg
|
||||
*.webm
|
||||
test/e2e/.dogfood-output/
|
||||
|
||||
# Package manager
|
||||
package-lock.json
|
||||
@@ -42,3 +47,12 @@ yarn.lock
|
||||
|
||||
# opensrc - source code for packages
|
||||
opensrc/
|
||||
|
||||
# Docs site
|
||||
docs/node_modules/
|
||||
docs/.next/
|
||||
docs/out/
|
||||
docs/package-lock.json
|
||||
|
||||
# pnpm
|
||||
.pnpm-store/
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
pnpm lint-staged
|
||||
node scripts/sync-version.js
|
||||
git add cli/Cargo.toml cli/Cargo.lock
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
@@ -2,9 +2,29 @@
|
||||
|
||||
Instructions for AI coding agents working with this codebase.
|
||||
|
||||
## Package Manager
|
||||
|
||||
This project uses **pnpm**. Always use `pnpm` instead of `npm` or `yarn` for installing dependencies, running scripts, etc. (e.g., `pnpm install`, `pnpm run build`).
|
||||
|
||||
## Code Style
|
||||
|
||||
- Do not use emojis in code, output, or documentation. Unicode symbols (✓, ✗, →, ⚠) are acceptable.
|
||||
- CLI colored output uses `cli/src/color.rs`. This module respects the `NO_COLOR` environment variable. Never use hardcoded ANSI color codes.
|
||||
- CLI flags must always use kebab-case (e.g., `--auto-connect`, `--allow-file-access`). Never use camelCase for flags (e.g., `--autoConnect` is wrong).
|
||||
|
||||
## Documentation
|
||||
|
||||
When adding or changing user-facing features (new flags, commands, behaviors, environment variables, etc.), update **all** of the following:
|
||||
|
||||
1. `cli/src/output.rs` -- `--help` output (flags list, examples, environment variables)
|
||||
2. `README.md` -- Options table, relevant feature sections, examples
|
||||
3. `skills/agent-browser/SKILL.md` -- so AI agents know about the feature
|
||||
4. `docs/src/app/` -- the Next.js docs site (MDX pages)
|
||||
5. Inline doc comments in the relevant source files
|
||||
|
||||
This applies to changes that either human users or AI agents would need to know about. Do not skip any of these locations.
|
||||
|
||||
In the `docs/src/app/` MDX files, always use HTML `<table>` syntax for tables (not markdown pipe tables). This matches the existing convention across the docs site.
|
||||
|
||||
<!-- opensrc:start -->
|
||||
|
||||
|
||||
+244
@@ -0,0 +1,244 @@
|
||||
# agent-browser
|
||||
|
||||
## 0.15.2-fork.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Merge upstream `v0.15.2` updates, including fixes for cookies clear/tab close output, daemon EPERM liveness checks, unnamed element reference matching, and docs/skills refresh.
|
||||
|
||||
## 0.15.1-fork.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Auto-attach existing browser more reliably by trying CDP localhost:9333 first, then falling back to auto-discovery before failing.
|
||||
Align daemon behavior and user-facing docs/skill guidance with the same attachment policy.
|
||||
|
||||
## 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
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Fix CLI typing delay parsing so `--delay` is treated as an option instead of typed text.
|
||||
- Add `--delay <ms>` parsing for `type` and `keyboard type`
|
||||
- Support `--` to type literal `--delay` text
|
||||
- Add regression tests for parsing and delay behavior
|
||||
- Update CLI help, README, skills, and docs command references
|
||||
|
||||
## 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,451 +1,219 @@
|
||||
# agent-browser
|
||||
# agent-browser-stealth
|
||||
|
||||
Headless browser automation CLI for AI agents. Fast Rust CLI with Node.js fallback.
|
||||
Stealth-first fork of `agent-browser` for production browser automation under anti-bot pressure.
|
||||
|
||||
## Installation
|
||||
This README focuses on stealth architecture and principles. For full command coverage inherited from upstream, use:
|
||||
|
||||
### npm (recommended)
|
||||
- upstream docs: <https://github.com/vercel-labs/agent-browser>
|
||||
- local help: `agent-browser --help`
|
||||
|
||||
```bash
|
||||
npm install -g agent-browser
|
||||
agent-browser install # Download Chromium
|
||||
```
|
||||
## What This Fork Optimizes
|
||||
|
||||
### From Source
|
||||
- Stealth is always on (legacy `launch.stealth` is accepted but ignored).
|
||||
- Fingerprint surfaces are patched at multiple layers (launch args, CDP overrides, init scripts).
|
||||
- Behavioral signals are humanized (typing cadence, cursor path, pacing, retry backoff).
|
||||
- Region signals are auto-aligned (locale/timezone/Accept-Language) to reduce mismatch risk.
|
||||
- Verification/captcha handling is policy-driven (`--risk-mode off|warn|block`).
|
||||
|
||||
```bash
|
||||
git clone https://github.com/vercel-labs/agent-browser
|
||||
cd agent-browser
|
||||
pnpm install
|
||||
pnpm build
|
||||
agent-browser install
|
||||
```
|
||||
## FAQ: `agent-browser` vs `agent-browser-stealth`
|
||||
|
||||
### Linux Dependencies
|
||||
People often ask this: "What's the anti-detection approach compared to `agent-browser-stealth` on npm?"
|
||||
|
||||
On Linux, install system dependencies:
|
||||
- `agent-browser-stealth` on npm is the package name for this fork.
|
||||
- The CLI keeps upstream-compatible command names (`agent-browser` is still the main executable, with `agent-browser-stealth` as an alias).
|
||||
- The practical difference vs upstream `agent-browser` is not one single "stealth switch"; it is a defense-in-depth stack designed for anti-bot pressure.
|
||||
|
||||
```bash
|
||||
agent-browser install --with-deps
|
||||
# or manually: npx playwright install-deps chromium
|
||||
```
|
||||
The core idea is layered hardening across the full automation lifecycle:
|
||||
|
||||
1. Connection-aware policy: choose the best available stealth capability by mode (local launch/CDP/cloud provider).
|
||||
2. Fingerprint hardening: patch launch args, CDP metadata, and init-script surfaces before page code runs.
|
||||
3. Behavioral humanization: non-uniform typing/mouse/wait patterns instead of perfectly mechanical actions.
|
||||
4. Region coherence: auto-align locale/timezone/language signals to target geography.
|
||||
5. Risk-aware control loop: detect verification/captcha signals and handle them with explicit `risk-mode` policy.
|
||||
|
||||
Goal: reduce detection probability and improve stability in production automation. Non-goal: "guaranteed bypass" on every target.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Install
|
||||
|
||||
```bash
|
||||
agent-browser open example.com
|
||||
agent-browser snapshot # Get accessibility tree with refs
|
||||
agent-browser click @e2 # Click by ref from snapshot
|
||||
agent-browser fill @e3 "test@example.com" # Fill by ref
|
||||
agent-browser get text @e1 # Get text by ref
|
||||
agent-browser screenshot page.png
|
||||
agent-browser close
|
||||
npm install -g agent-browser-stealth
|
||||
agent-browser install
|
||||
```
|
||||
|
||||
### Traditional Selectors (also supported)
|
||||
### Minimal Usage
|
||||
|
||||
```bash
|
||||
agent-browser click "#submit"
|
||||
agent-browser fill "#email" "test@example.com"
|
||||
agent-browser find role button click --name "Submit"
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
### Core Commands
|
||||
|
||||
```bash
|
||||
agent-browser open <url> # Navigate to URL
|
||||
agent-browser click <sel> # Click element
|
||||
agent-browser dblclick <sel> # Double-click element
|
||||
agent-browser focus <sel> # Focus element
|
||||
agent-browser type <sel> <text> # Type into element
|
||||
agent-browser fill <sel> <text> # Clear and fill
|
||||
agent-browser press <key> # Press key (Enter, Tab, Control+a)
|
||||
agent-browser keydown <key> # Hold key down
|
||||
agent-browser keyup <key> # Release key
|
||||
agent-browser hover <sel> # Hover 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)
|
||||
agent-browser scrollintoview <sel> # Scroll element into view
|
||||
agent-browser drag <src> <tgt> # Drag and drop
|
||||
agent-browser upload <sel> <files> # Upload files
|
||||
agent-browser screenshot [path] # Take screenshot (--full for full page)
|
||||
agent-browser pdf <path> # Save as PDF
|
||||
agent-browser snapshot # Accessibility tree with refs (best for AI)
|
||||
agent-browser eval <js> # Run JavaScript
|
||||
agent-browser close # Close browser
|
||||
```
|
||||
|
||||
### 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 count <sel> # Count matching elements
|
||||
agent-browser get box <sel> # Get bounding box
|
||||
```
|
||||
|
||||
### 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)
|
||||
|
||||
```bash
|
||||
agent-browser find role <role> <action> [value] # By ARIA role
|
||||
agent-browser find text <text> <action> # By text content
|
||||
agent-browser find label <label> <action> [value] # By label
|
||||
agent-browser find placeholder <ph> <action> [value] # By placeholder
|
||||
agent-browser find alt <text> <action> # By alt text
|
||||
agent-browser find title <text> <action> # By title attr
|
||||
agent-browser find testid <id> <action> [value] # By data-testid
|
||||
agent-browser find first <sel> <action> [value] # First match
|
||||
agent-browser find last <sel> <action> [value] # Last match
|
||||
agent-browser find nth <n> <sel> <action> [value] # Nth match
|
||||
```
|
||||
|
||||
**Actions:** `click`, `fill`, `check`, `hover`, `text`
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
agent-browser find role button click --name "Submit"
|
||||
agent-browser find text "Sign In" click
|
||||
agent-browser find label "Email" fill "test@test.com"
|
||||
agent-browser find first ".item" click
|
||||
agent-browser find nth 2 "a" text
|
||||
```
|
||||
|
||||
### Wait
|
||||
|
||||
```bash
|
||||
agent-browser wait <selector> # Wait for element
|
||||
agent-browser wait <ms> # Wait for time
|
||||
agent-browser wait --text "Welcome" # Wait for text
|
||||
agent-browser wait --url "**/dash" # Wait for URL pattern
|
||||
agent-browser wait --load networkidle # Wait for load state
|
||||
agent-browser wait --fn "window.ready === true" # Wait for JS condition
|
||||
```
|
||||
|
||||
**Load states:** `load`, `domcontentloaded`, `networkidle`
|
||||
|
||||
### Mouse Control
|
||||
|
||||
```bash
|
||||
agent-browser mouse move <x> <y> # Move mouse
|
||||
agent-browser mouse down [button] # Press button (left/right/middle)
|
||||
agent-browser mouse up [button] # Release button
|
||||
agent-browser mouse wheel <dy> [dx] # Scroll wheel
|
||||
```
|
||||
|
||||
### Browser Settings
|
||||
|
||||
```bash
|
||||
agent-browser set viewport <w> <h> # Set viewport size
|
||||
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
|
||||
```
|
||||
|
||||
### 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 unroute [url] # Remove routes
|
||||
agent-browser network requests # View tracked requests
|
||||
agent-browser network requests --filter api # Filter requests
|
||||
```
|
||||
|
||||
### Tabs & Windows
|
||||
|
||||
```bash
|
||||
agent-browser tab # List tabs
|
||||
agent-browser tab new [url] # New tab (optionally with URL)
|
||||
agent-browser tab <n> # Switch to tab n
|
||||
agent-browser tab close [n] # Close tab
|
||||
agent-browser window new # New window
|
||||
```
|
||||
|
||||
### Frames
|
||||
|
||||
```bash
|
||||
agent-browser frame <sel> # Switch to iframe
|
||||
agent-browser frame main # Back to main frame
|
||||
```
|
||||
|
||||
### Dialogs
|
||||
|
||||
```bash
|
||||
agent-browser dialog accept [text] # Accept (with optional prompt text)
|
||||
agent-browser dialog dismiss # Dismiss
|
||||
```
|
||||
|
||||
### Debug
|
||||
|
||||
```bash
|
||||
agent-browser trace start [path] # Start recording trace
|
||||
agent-browser trace stop [path] # Stop and save trace
|
||||
agent-browser console # View console messages
|
||||
agent-browser console --clear # Clear console
|
||||
agent-browser errors # View page errors
|
||||
agent-browser errors --clear # Clear errors
|
||||
agent-browser highlight <sel> # Highlight element
|
||||
agent-browser state save <path> # Save auth state
|
||||
agent-browser state load <path> # Load auth state
|
||||
```
|
||||
|
||||
### Navigation
|
||||
|
||||
```bash
|
||||
agent-browser back # Go back
|
||||
agent-browser forward # Go forward
|
||||
agent-browser reload # Reload page
|
||||
```
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
agent-browser install # Download Chromium browser
|
||||
agent-browser install --with-deps # Also install system deps (Linux)
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
# Show current session
|
||||
agent-browser session
|
||||
```
|
||||
|
||||
Each session has its own:
|
||||
- Browser instance
|
||||
- Cookies and storage
|
||||
- Navigation history
|
||||
- Authentication state
|
||||
|
||||
## Snapshot Options
|
||||
|
||||
The `snapshot` command supports filtering to reduce output size:
|
||||
|
||||
```bash
|
||||
agent-browser snapshot # Full accessibility tree
|
||||
agent-browser snapshot -i # Interactive elements only (buttons, inputs, links)
|
||||
agent-browser snapshot -c # Compact (remove empty structural 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
|
||||
```
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `-i, --interactive` | Only show interactive elements (buttons, links, inputs) |
|
||||
| `-c, --compact` | Remove empty structural elements |
|
||||
| `-d, --depth <n>` | Limit tree depth |
|
||||
| `-s, --selector <sel>` | Scope to CSS selector |
|
||||
|
||||
## Options
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--session <name>` | Use isolated session (or `AGENT_BROWSER_SESSION` env) |
|
||||
| `--json` | JSON output (for agents) |
|
||||
| `--full, -f` | Full page screenshot |
|
||||
| `--name, -n` | Locator name filter |
|
||||
| `--exact` | Exact text match |
|
||||
| `--headed` | Show browser window (not headless) |
|
||||
| `--debug` | Debug output |
|
||||
|
||||
## Selectors
|
||||
|
||||
### Refs (Recommended for AI)
|
||||
|
||||
Refs provide deterministic element selection from snapshots:
|
||||
|
||||
```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 use refs?**
|
||||
- **Deterministic**: Ref points to exact element from snapshot
|
||||
- **Fast**: No DOM re-query needed
|
||||
- **AI-friendly**: Snapshot + ref workflow is optimal for LLMs
|
||||
|
||||
### CSS Selectors
|
||||
|
||||
```bash
|
||||
agent-browser click "#id"
|
||||
agent-browser click ".class"
|
||||
agent-browser click "div > button"
|
||||
```
|
||||
|
||||
### Text & XPath
|
||||
|
||||
```bash
|
||||
agent-browser click "text=Submit"
|
||||
agent-browser click "xpath=//button"
|
||||
```
|
||||
|
||||
### Semantic Locators
|
||||
|
||||
```bash
|
||||
agent-browser find role button click --name "Submit"
|
||||
agent-browser find label "Email" fill "test@test.com"
|
||||
```
|
||||
|
||||
## Agent Mode
|
||||
|
||||
Use `--json` for machine-readable output:
|
||||
|
||||
```bash
|
||||
agent-browser snapshot --json
|
||||
# Returns: {"success":true,"data":{"snapshot":"...","refs":{"e1":{"role":"heading","name":"Title"},...}}}
|
||||
|
||||
agent-browser get text @e1 --json
|
||||
agent-browser is visible @e2 --json
|
||||
```
|
||||
|
||||
### Optimal AI Workflow
|
||||
|
||||
```bash
|
||||
# 1. Navigate and get snapshot
|
||||
agent-browser open example.com
|
||||
agent-browser snapshot -i --json # AI parses tree and refs
|
||||
|
||||
# 2. AI identifies target refs from snapshot
|
||||
# 3. Execute actions using refs
|
||||
agent-browser open https://example.com
|
||||
agent-browser snapshot -i
|
||||
agent-browser click @e2
|
||||
agent-browser fill @e3 "input text"
|
||||
|
||||
# 4. Get new snapshot if page changed
|
||||
agent-browser snapshot -i --json
|
||||
```
|
||||
|
||||
## Headed Mode
|
||||
## Stealth Architecture
|
||||
|
||||
Show the browser window for debugging:
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["Command Input"] --> B["Stealth Policy Resolver"]
|
||||
B --> C["Connection Mode Detection"]
|
||||
C --> D["Launch Layer: Chromium Args"]
|
||||
C --> E["CDP Layer: UA + Metadata Override"]
|
||||
C --> F["Context Layer: Init Script Patches"]
|
||||
D --> G["Behavior Layer: Humanized Interaction"]
|
||||
E --> G
|
||||
F --> G
|
||||
G --> H["Risk Layer: Verification Detection and Handling"]
|
||||
H --> I["Response with warnings and riskSignals"]
|
||||
```
|
||||
|
||||
### Policy by Connection Mode
|
||||
|
||||
| Mode | Stealth Capabilities | Notes |
|
||||
| --------------------------------------- | ------------------------------------------------------------- | -------------------------------------------- |
|
||||
| Local Chromium launch | Chromium launch args + CDP UA override + context init scripts | Most complete stack |
|
||||
| Existing browser via CDP | CDP UA override + context init scripts | No local Chromium arg injection |
|
||||
| Cloud provider (browserbase/browseruse) | Context init scripts | Remote browser runtime controls launch layer |
|
||||
| Kernel provider | Context init scripts + provider-managed stealth | Provider-side stealth may also apply |
|
||||
|
||||
## Principle 1: Always-On Stealth with Explicit Boundaries
|
||||
|
||||
- Stealth defaults to enabled and does not depend on a runtime toggle.
|
||||
- Project policy forbids:
|
||||
- `--profile` / `AGENT_BROWSER_PROFILE`
|
||||
- `--channel` / `AGENT_BROWSER_CHANNEL`
|
||||
- Default CLI policy auto-attaches an existing browser: try CDP `localhost:9333` first, then auto-discovery unless explicit connection options are provided.
|
||||
|
||||
## Principle 2: Multi-Layer Fingerprint Hardening
|
||||
|
||||
### 2.1 Launch Layer (Local Chromium)
|
||||
|
||||
Injected Chromium args:
|
||||
|
||||
- `--disable-blink-features=AutomationControlled`
|
||||
- `--use-gl=angle`
|
||||
- `--use-angle=default`
|
||||
|
||||
If no custom UA is set, the runtime UA is normalized to remove `HeadlessChrome` tokens.
|
||||
|
||||
### 2.2 CDP Layer (Browser/Page Targets)
|
||||
|
||||
- Uses `Emulation.setUserAgentOverride` to align:
|
||||
- `userAgent`
|
||||
- `acceptLanguage`
|
||||
- `userAgentMetadata` brands and versions
|
||||
- Applies overrides for existing/new targets, including worker-relevant contexts.
|
||||
- Forces opaque white background (`Emulation.setDefaultBackgroundColorOverride`) to avoid headless transparency fingerprints.
|
||||
|
||||
### 2.3 Context Init-Script Layer (Patch Inventory)
|
||||
|
||||
The init script patch set is injected before page scripts and currently includes:
|
||||
|
||||
1. `navigator.webdriver` removal (including prototype-level cleanup).
|
||||
2. CSS webdriver heuristic neutralization (`CSS.supports('border-end-end-radius: initial')` probe).
|
||||
3. `window.chrome.runtime` bootstrap for missing runtime surfaces.
|
||||
4. Locale/language normalization (`navigator.language`, `navigator.languages`).
|
||||
5. Realistic `navigator.plugins` and `navigator.mimeTypes`.
|
||||
6. `navigator.permissions.query` normalization for notifications.
|
||||
7. WebGL vendor/renderer masking when SwiftShader indicators are present.
|
||||
8. `cdc_` property cleanup on document/documentElement.
|
||||
9. Window/screen dimension normalization (`outerWidth/outerHeight/screenX/screenY`).
|
||||
10. Screen availability patching (`availWidth/availHeight`).
|
||||
11. Hardware concurrency stabilization.
|
||||
12. Notification permission consistency.
|
||||
13. Active text color heuristic patching.
|
||||
14. `navigator.connection` normalization.
|
||||
15. Worker network signal normalization (`downlinkMax`).
|
||||
16. `prefers-color-scheme` light-mode heuristic neutralization.
|
||||
17. `navigator.share` exposure.
|
||||
18. `navigator.contacts` exposure.
|
||||
19. `contentIndex` exposure.
|
||||
20. `navigator.pdfViewerEnabled` normalization.
|
||||
21. Media devices surface normalization.
|
||||
22. `navigator.userAgent` cleanup (strip `HeadlessChrome`).
|
||||
23. `navigator.userAgentData` brand cleanup.
|
||||
24. `performance.memory` stabilization.
|
||||
25. Default background color patching at script level.
|
||||
|
||||
## Principle 3: Behavioral Humanization
|
||||
|
||||
- Navigation pacing jitter before `goto` (short randomized delay).
|
||||
- Typing jitter for `type --delay` and `keyboard type --delay`:
|
||||
- per-character randomized delay around the requested base delay (about ±40%).
|
||||
- Click path humanization:
|
||||
- cursor moves on a Bezier-like curve before click.
|
||||
- Wait supports random ranges (`wait min-max`) for non-uniform timing.
|
||||
|
||||
## Principle 4: Region Signal Alignment
|
||||
|
||||
Before navigation, the runtime derives region hints from target URL TLD and aligns:
|
||||
|
||||
- locale
|
||||
- timezone
|
||||
- `Accept-Language`
|
||||
|
||||
Examples of built-in mappings include `tw`, `jp`, `kr`, `sg`, `de`, `fr`, `uk`, `in`, `au`.
|
||||
|
||||
Manual overrides are supported:
|
||||
|
||||
- `AGENT_BROWSER_LOCALE`
|
||||
- `AGENT_BROWSER_TIMEZONE` (or `TZ`)
|
||||
|
||||
## Principle 5: Verification-Aware Risk Control
|
||||
|
||||
When a navigation lands on verification/captcha pages, structured risk signals are generated from URL/title evidence.
|
||||
|
||||
`riskSignals` include:
|
||||
|
||||
- `code`
|
||||
- `source` (`url` or `title`)
|
||||
- `evidence`
|
||||
- `confidence`
|
||||
|
||||
### Risk Mode
|
||||
|
||||
- `warn` (default): retry with randomized backoff and return warnings + `riskSignals`.
|
||||
- `block`: fail fast once verification/captcha interstitial is detected.
|
||||
- `off`: skip detection/retry path.
|
||||
|
||||
```bash
|
||||
agent-browser open example.com --headed
|
||||
agent-browser --risk-mode warn open https://example.com
|
||||
agent-browser --risk-mode block open https://example.com
|
||||
AGENT_BROWSER_RISK_MODE=off agent-browser open https://example.com
|
||||
```
|
||||
|
||||
This opens a visible browser window instead of running headless.
|
||||
|
||||
## Architecture
|
||||
|
||||
agent-browser uses a client-daemon architecture:
|
||||
|
||||
1. **Rust CLI** (fast native binary) - Parses commands, communicates with daemon
|
||||
2. **Node.js Daemon** - Manages Playwright browser instance
|
||||
3. **Fallback** - If native binary unavailable, uses Node.js directly
|
||||
|
||||
The daemon starts automatically on first command and persists between commands for fast subsequent operations.
|
||||
|
||||
## Platforms
|
||||
|
||||
| Platform | Binary | Fallback |
|
||||
|----------|--------|----------|
|
||||
| macOS ARM64 | ✅ Native Rust | Node.js |
|
||||
| macOS x64 | ✅ Native Rust | Node.js |
|
||||
| Linux ARM64 | ✅ Native Rust | Node.js |
|
||||
| Linux x64 | ✅ Native Rust | Node.js |
|
||||
| Windows | - | Node.js |
|
||||
|
||||
## Usage with AI Agents
|
||||
|
||||
### Just ask the agent
|
||||
|
||||
The simplest approach - just tell your agent to use it:
|
||||
|
||||
```
|
||||
Use agent-browser to test the login flow. Run agent-browser --help to see available commands.
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["Navigate"] --> B["Collect URL and Title Signals"]
|
||||
B --> C{"risk-mode"}
|
||||
C -->|off| D["Return Success"]
|
||||
C -->|block| E["Return Error with First Signal"]
|
||||
C -->|warn| F["Retry up to 2 times"]
|
||||
F --> G{"Signals Cleared"}
|
||||
G -->|yes| H["Return Success + recovery warning + riskSignals"]
|
||||
G -->|no| I["Return Success + warning + riskSignals"]
|
||||
```
|
||||
|
||||
The `--help` output is comprehensive and most agents can figure it out from there.
|
||||
## Operational Recommendations
|
||||
|
||||
### AGENTS.md / CLAUDE.md
|
||||
- Prefer `--headed` for high-friction targets.
|
||||
- Reuse session state with `--session-name` for continuity.
|
||||
- Keep locale/timezone consistent with target market.
|
||||
- Use `--risk-mode block` in strict pipelines that require explicit operator intervention on verification pages.
|
||||
- For `cookies set`, use either `--url <url>`, or `--domain <domain> --path <path>` together.
|
||||
- If `--url`, `--domain`, and `--path` are all omitted, the cookie is scoped from the current page URL.
|
||||
|
||||
For more consistent results, add to your project or global instructions file:
|
||||
## Validation Scripts
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
### Claude Code Skill
|
||||
|
||||
For Claude Code, a [skill](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices) provides richer context:
|
||||
Run public detector checks after stealth changes:
|
||||
|
||||
```bash
|
||||
cp -r node_modules/agent-browser/skills/agent-browser .claude/skills/
|
||||
node scripts/check-sannysoft-webdriver.js --binary ./cli/target/release/agent-browser
|
||||
node scripts/check-creepjs-headless.js --binary ./cli/target/release/agent-browser
|
||||
```
|
||||
|
||||
Or download:
|
||||
## Upstream Compatibility
|
||||
|
||||
```bash
|
||||
mkdir -p .claude/skills/agent-browser
|
||||
curl -o .claude/skills/agent-browser/SKILL.md \
|
||||
https://raw.githubusercontent.com/vercel-labs/agent-browser/main/skills/agent-browser/SKILL.md
|
||||
```
|
||||
This fork intentionally keeps command workflows close to upstream while concentrating custom behavior in stealth, policy, and anti-detection handling.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
#!/bin/sh
|
||||
# agent-browser CLI wrapper
|
||||
# Detects OS/arch and runs the appropriate native binary
|
||||
|
||||
SCRIPT="$0"
|
||||
while [ -L "$SCRIPT" ]; do
|
||||
SCRIPT_DIR="$(cd "$(dirname "$SCRIPT")" && pwd)"
|
||||
SCRIPT="$(readlink "$SCRIPT")"
|
||||
case "$SCRIPT" in /*) ;; *) SCRIPT="$SCRIPT_DIR/$SCRIPT" ;; esac
|
||||
done
|
||||
SCRIPT_DIR="$(cd "$(dirname "$SCRIPT")" && pwd)"
|
||||
|
||||
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||
ARCH=$(uname -m)
|
||||
case "$OS" in darwin) OS="darwin" ;; linux) OS="linux" ;; mingw*|msys*|cygwin*) OS="win32" ;; esac
|
||||
case "$ARCH" in x86_64|amd64) ARCH="x64" ;; aarch64|arm64) ARCH="arm64" ;; esac
|
||||
|
||||
BINARY="$SCRIPT_DIR/agent-browser-${OS}-${ARCH}"
|
||||
|
||||
if [ -f "$BINARY" ] && [ -x "$BINARY" ]; then
|
||||
exec "$BINARY" "$@"
|
||||
fi
|
||||
|
||||
echo "Error: No binary found for ${OS}-${ARCH}" >&2
|
||||
echo "Run 'npm run build:native' to build for your platform" >&2
|
||||
exit 1
|
||||
@@ -1,5 +0,0 @@
|
||||
@echo off
|
||||
setlocal
|
||||
set "SCRIPT_DIR=%~dp0"
|
||||
node "%SCRIPT_DIR%..\dist\index.js" %*
|
||||
exit /b %errorlevel%
|
||||
Executable
+109
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Cross-platform CLI wrapper for agent-browser
|
||||
*
|
||||
* This wrapper enables npx support on Windows where shell scripts don't work.
|
||||
* For global installs, postinstall.js patches the shims to invoke the native
|
||||
* binary directly (zero overhead).
|
||||
*/
|
||||
|
||||
import { spawn } from 'child_process';
|
||||
import { existsSync, accessSync, chmodSync, constants } from 'fs';
|
||||
import { dirname, join } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { platform, arch } from 'os';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
// Map Node.js platform/arch to binary naming convention
|
||||
function getBinaryName() {
|
||||
const os = platform();
|
||||
const cpuArch = arch();
|
||||
|
||||
let osKey;
|
||||
switch (os) {
|
||||
case 'darwin':
|
||||
osKey = 'darwin';
|
||||
break;
|
||||
case 'linux':
|
||||
osKey = 'linux';
|
||||
break;
|
||||
case 'win32':
|
||||
osKey = 'win32';
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
let archKey;
|
||||
switch (cpuArch) {
|
||||
case 'x64':
|
||||
case 'x86_64':
|
||||
archKey = 'x64';
|
||||
break;
|
||||
case 'arm64':
|
||||
case 'aarch64':
|
||||
archKey = 'arm64';
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
const ext = os === 'win32' ? '.exe' : '';
|
||||
return `agent-browser-${osKey}-${archKey}${ext}`;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const binaryName = getBinaryName();
|
||||
|
||||
if (!binaryName) {
|
||||
console.error(`Error: Unsupported platform: ${platform()}-${arch()}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const binaryPath = join(__dirname, binaryName);
|
||||
|
||||
if (!existsSync(binaryPath)) {
|
||||
console.error(`Error: No binary found for ${platform()}-${arch()}`);
|
||||
console.error(`Expected: ${binaryPath}`);
|
||||
console.error('');
|
||||
console.error('Run "npm run build:native" to build for your platform,');
|
||||
console.error('or reinstall the package to trigger the postinstall download.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Ensure binary is executable (fixes EACCES on macOS/Linux when postinstall didn't run,
|
||||
// e.g., when using bun which blocks lifecycle scripts by default)
|
||||
if (platform() !== 'win32') {
|
||||
try {
|
||||
accessSync(binaryPath, constants.X_OK);
|
||||
} catch {
|
||||
// Binary exists but isn't executable - fix it
|
||||
try {
|
||||
chmodSync(binaryPath, 0o755);
|
||||
} catch (chmodErr) {
|
||||
console.error(`Error: Cannot make binary executable: ${chmodErr.message}`);
|
||||
console.error('Try running: chmod +x ' + binaryPath);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Spawn the native binary with inherited stdio
|
||||
const child = spawn(binaryPath, process.argv.slice(2), {
|
||||
stdio: 'inherit',
|
||||
windowsHide: false,
|
||||
});
|
||||
|
||||
child.on('error', (err) => {
|
||||
console.error(`Error executing binary: ${err.message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
process.exit(code ?? 0);
|
||||
});
|
||||
}
|
||||
|
||||
main();
|
||||
Generated
+183
-11
@@ -3,13 +3,66 @@
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "agent-browser"
|
||||
version = "0.4.2"
|
||||
name = "agent-browser-stealth"
|
||||
version = "0.15.2-fork.0"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"dirs",
|
||||
"getrandom",
|
||||
"libc",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"windows-sys",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "5.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
|
||||
dependencies = [
|
||||
"dirs-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-sys"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -24,12 +77,28 @@ version = "0.2.180"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.105"
|
||||
@@ -48,6 +117,17 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"libredox",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
@@ -102,19 +182,69 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
|
||||
dependencies = [
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.48.5",
|
||||
"windows_aarch64_msvc 0.48.5",
|
||||
"windows_i686_gnu 0.48.5",
|
||||
"windows_i686_msvc 0.48.5",
|
||||
"windows_x86_64_gnu 0.48.5",
|
||||
"windows_x86_64_gnullvm 0.48.5",
|
||||
"windows_x86_64_msvc 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -123,28 +253,46 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_aarch64_gnullvm 0.52.6",
|
||||
"windows_aarch64_msvc 0.52.6",
|
||||
"windows_i686_gnu 0.52.6",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
"windows_i686_msvc 0.52.6",
|
||||
"windows_x86_64_gnu 0.52.6",
|
||||
"windows_x86_64_gnullvm 0.52.6",
|
||||
"windows_x86_64_msvc 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
@@ -157,24 +305,48 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
|
||||
+14
-3
@@ -1,13 +1,24 @@
|
||||
[package]
|
||||
name = "agent-browser"
|
||||
version = "0.4.2"
|
||||
name = "agent-browser-stealth"
|
||||
version = "0.15.2-fork.0"
|
||||
edition = "2021"
|
||||
description = "Fast browser automation CLI for AI agents"
|
||||
description = "Stealth browser automation CLI for AI agents with anti-bot evasions"
|
||||
license = "Apache-2.0"
|
||||
|
||||
[[bin]]
|
||||
name = "agent-browser"
|
||||
path = "src/main.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "agent-browser-stealth"
|
||||
path = "src/main_stealth.rs"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
dirs = "5.0"
|
||||
base64 = "0.22"
|
||||
getrandom = "0.2"
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = "0.2"
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
//! Color output utilities respecting NO_COLOR environment variable.
|
||||
//!
|
||||
//! When the NO_COLOR environment variable is present (regardless of value),
|
||||
//! all color formatting is disabled per https://no-color.org/
|
||||
|
||||
use std::env;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
/// Returns true if color output is enabled (NO_COLOR is NOT set)
|
||||
pub fn is_enabled() -> bool {
|
||||
static COLORS_ENABLED: OnceLock<bool> = OnceLock::new();
|
||||
*COLORS_ENABLED.get_or_init(|| env::var("NO_COLOR").is_err())
|
||||
}
|
||||
|
||||
/// Format text in red (errors)
|
||||
pub fn red(text: &str) -> String {
|
||||
if is_enabled() {
|
||||
format!("\x1b[31m{}\x1b[0m", text)
|
||||
} else {
|
||||
text.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Format text in green (success)
|
||||
pub fn green(text: &str) -> String {
|
||||
if is_enabled() {
|
||||
format!("\x1b[32m{}\x1b[0m", text)
|
||||
} else {
|
||||
text.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Format text in yellow (warnings)
|
||||
pub fn yellow(text: &str) -> String {
|
||||
if is_enabled() {
|
||||
format!("\x1b[33m{}\x1b[0m", text)
|
||||
} else {
|
||||
text.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Format text in cyan (info/progress)
|
||||
pub fn cyan(text: &str) -> String {
|
||||
if is_enabled() {
|
||||
format!("\x1b[36m{}\x1b[0m", text)
|
||||
} else {
|
||||
text.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Format text in bold
|
||||
pub fn bold(text: &str) -> String {
|
||||
if is_enabled() {
|
||||
format!("\x1b[1m{}\x1b[0m", text)
|
||||
} else {
|
||||
text.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Format text in dim
|
||||
pub fn dim(text: &str) -> String {
|
||||
if is_enabled() {
|
||||
format!("\x1b[2m{}\x1b[0m", text)
|
||||
} else {
|
||||
text.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Red X error indicator
|
||||
pub fn error_indicator() -> &'static str {
|
||||
static INDICATOR: OnceLock<String> = OnceLock::new();
|
||||
INDICATOR.get_or_init(|| {
|
||||
if is_enabled() {
|
||||
"\x1b[31m✗\x1b[0m".to_string()
|
||||
} else {
|
||||
"✗".to_string()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Green checkmark success indicator
|
||||
pub fn success_indicator() -> &'static str {
|
||||
static INDICATOR: OnceLock<String> = OnceLock::new();
|
||||
INDICATOR.get_or_init(|| {
|
||||
if is_enabled() {
|
||||
"\x1b[32m✓\x1b[0m".to_string()
|
||||
} else {
|
||||
"✓".to_string()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Yellow warning indicator
|
||||
pub fn warning_indicator() -> &'static str {
|
||||
static INDICATOR: OnceLock<String> = OnceLock::new();
|
||||
INDICATOR.get_or_init(|| {
|
||||
if is_enabled() {
|
||||
"\x1b[33m⚠\x1b[0m".to_string()
|
||||
} else {
|
||||
"⚠".to_string()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Get console log color prefix by level
|
||||
pub fn console_level_prefix(level: &str) -> String {
|
||||
if !is_enabled() {
|
||||
return format!("[{}]", level);
|
||||
}
|
||||
|
||||
let color = match level {
|
||||
"error" => "\x1b[31m",
|
||||
"warning" => "\x1b[33m",
|
||||
"info" => "\x1b[36m",
|
||||
_ => "",
|
||||
};
|
||||
if color.is_empty() {
|
||||
format!("[{}]", level)
|
||||
} else {
|
||||
format!("{}[{}]\x1b[0m", color, level)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_red_contains_ansi_codes() {
|
||||
// Test the format structure (actual color depends on NO_COLOR env)
|
||||
let formatted = format!("\x1b[31m{}\x1b[0m", "error");
|
||||
assert!(formatted.contains("\x1b[31m"));
|
||||
assert!(formatted.contains("\x1b[0m"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_green_contains_ansi_codes() {
|
||||
let formatted = format!("\x1b[32m{}\x1b[0m", "success");
|
||||
assert!(formatted.contains("\x1b[32m"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_console_level_prefix_contains_level() {
|
||||
// Regardless of color state, the level text should be present
|
||||
assert!(console_level_prefix("error").contains("error"));
|
||||
assert!(console_level_prefix("warning").contains("warning"));
|
||||
assert!(console_level_prefix("info").contains("info"));
|
||||
assert!(console_level_prefix("log").contains("log"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_indicators_contain_symbols() {
|
||||
// Regardless of color state, symbols should be present
|
||||
assert!(error_indicator().contains('✗'));
|
||||
assert!(success_indicator().contains('✓'));
|
||||
assert!(warning_indicator().contains('⚠'));
|
||||
}
|
||||
}
|
||||
+2775
-151
File diff suppressed because it is too large
Load Diff
+520
-23
@@ -81,21 +81,62 @@ impl Connection {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the base directory for socket/pid files.
|
||||
/// Priority: AGENT_BROWSER_SOCKET_DIR > XDG_RUNTIME_DIR > ~/.agent-browser > tmpdir
|
||||
pub fn get_socket_dir() -> PathBuf {
|
||||
// 1. Explicit override (ignore empty string)
|
||||
if let Ok(dir) = env::var("AGENT_BROWSER_SOCKET_DIR") {
|
||||
if !dir.is_empty() {
|
||||
return PathBuf::from(dir);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. XDG_RUNTIME_DIR (Linux standard, ignore empty string)
|
||||
if let Ok(runtime_dir) = env::var("XDG_RUNTIME_DIR") {
|
||||
if !runtime_dir.is_empty() {
|
||||
return PathBuf::from(runtime_dir).join("agent-browser");
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Home directory fallback (like Docker Desktop's ~/.docker/run/)
|
||||
if let Some(home) = dirs::home_dir() {
|
||||
return home.join(".agent-browser");
|
||||
}
|
||||
|
||||
// 4. Last resort: temp dir
|
||||
env::temp_dir().join("agent-browser")
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn get_socket_path(session: &str) -> PathBuf {
|
||||
let tmp = env::temp_dir();
|
||||
tmp.join(format!("agent-browser-{}.sock", session))
|
||||
get_socket_dir().join(format!("{}.sock", session))
|
||||
}
|
||||
|
||||
fn get_pid_path(session: &str) -> PathBuf {
|
||||
let tmp = env::temp_dir();
|
||||
tmp.join(format!("agent-browser-{}.pid", session))
|
||||
get_socket_dir().join(format!("{}.pid", session))
|
||||
}
|
||||
|
||||
/// Clean up stale socket and PID files for a session
|
||||
fn cleanup_stale_files(session: &str) {
|
||||
let pid_path = get_pid_path(session);
|
||||
let _ = fs::remove_file(&pid_path);
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let socket_path = get_socket_path(session);
|
||||
let _ = fs::remove_file(&socket_path);
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let port_path = get_port_path(session);
|
||||
let _ = fs::remove_file(&port_path);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn get_port_path(session: &str) -> PathBuf {
|
||||
let tmp = env::temp_dir();
|
||||
tmp.join(format!("agent-browser-{}.port", session))
|
||||
get_socket_dir().join(format!("{}.port", session))
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
@@ -104,7 +145,9 @@ fn get_port_for_session(session: &str) -> u16 {
|
||||
for c in session.chars() {
|
||||
hash = ((hash << 5).wrapping_sub(hash)).wrapping_add(c as i32);
|
||||
}
|
||||
49152 + ((hash.abs() as u16) % 16383)
|
||||
// Correct logic: first take absolute modulo, then cast to u16
|
||||
// Using unsigned_abs() to safely handle i32::MIN
|
||||
49152 + ((hash.unsigned_abs() as u32 % 16383) as u16)
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
@@ -116,7 +159,13 @@ fn is_daemon_running(session: &str) -> bool {
|
||||
if let Ok(pid_str) = fs::read_to_string(&pid_path) {
|
||||
if let Ok(pid) = pid_str.trim().parse::<i32>() {
|
||||
unsafe {
|
||||
return libc::kill(pid, 0) == 0;
|
||||
if libc::kill(pid, 0) == 0 {
|
||||
return true;
|
||||
}
|
||||
// EPERM means the process exists but we lack permission to
|
||||
// signal it (e.g. inside a macOS sandbox). Only ESRCH means
|
||||
// the process is genuinely gone.
|
||||
return std::io::Error::last_os_error().raw_os_error() != Some(libc::ESRCH);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -140,7 +189,8 @@ fn is_daemon_running(session: &str) -> bool {
|
||||
fn daemon_ready(session: &str) -> bool {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
get_socket_path(session).exists()
|
||||
let socket_path = get_socket_path(session);
|
||||
UnixStream::connect(&socket_path).is_ok()
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
@@ -153,30 +203,113 @@ fn daemon_ready(session: &str) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ensure_daemon(session: &str, headed: bool) -> Result<(), String> {
|
||||
/// Result of ensure_daemon indicating whether a new daemon was started
|
||||
pub struct DaemonResult {
|
||||
/// True if we connected to an existing daemon, false if we started a new one
|
||||
pub already_running: bool,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn ensure_daemon(
|
||||
session: &str,
|
||||
headed: bool,
|
||||
executable_path: Option<&str>,
|
||||
extensions: &[String],
|
||||
args: Option<&str>,
|
||||
user_agent: Option<&str>,
|
||||
proxy: Option<&str>,
|
||||
proxy_bypass: Option<&str>,
|
||||
ignore_https_errors: bool,
|
||||
allow_file_access: bool,
|
||||
state: Option<&str>,
|
||||
provider: Option<&str>,
|
||||
device: Option<&str>,
|
||||
session_name: Option<&str>,
|
||||
debug: bool,
|
||||
download_path: Option<&str>,
|
||||
) -> Result<DaemonResult, String> {
|
||||
// Check if daemon is running AND responsive
|
||||
if is_daemon_running(session) && daemon_ready(session) {
|
||||
return Ok(());
|
||||
// Double-check it's actually responsive by waiting and checking again
|
||||
// This handles the race condition where daemon is shutting down
|
||||
// (daemon has a 100ms shutdown delay, so we wait longer)
|
||||
thread::sleep(Duration::from_millis(150));
|
||||
if daemon_ready(session) {
|
||||
return Ok(DaemonResult {
|
||||
already_running: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up any stale socket/pid files before starting fresh
|
||||
cleanup_stale_files(session);
|
||||
|
||||
// Ensure socket directory exists
|
||||
let socket_dir = get_socket_dir();
|
||||
if !socket_dir.exists() {
|
||||
fs::create_dir_all(&socket_dir)
|
||||
.map_err(|e| format!("Failed to create socket directory: {}", e))?;
|
||||
}
|
||||
|
||||
// Pre-flight check: Validate socket path length (Unix limit is 104 bytes including null terminator)
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let socket_path = get_socket_path(session);
|
||||
let path_len = socket_path.as_os_str().len();
|
||||
if path_len > 103 {
|
||||
return Err(format!(
|
||||
"Session name '{}' is too long. Socket path would be {} bytes (max 103).\n\
|
||||
Use a shorter session name or set AGENT_BROWSER_SOCKET_DIR to a shorter path.",
|
||||
session, path_len
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-flight check: Verify socket directory is writable
|
||||
{
|
||||
let test_file = socket_dir.join(".write_test");
|
||||
match fs::write(&test_file, b"") {
|
||||
Ok(_) => {
|
||||
let _ = fs::remove_file(&test_file);
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(format!(
|
||||
"Socket directory '{}' is not writable: {}",
|
||||
socket_dir.display(),
|
||||
e
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let exe_path = env::current_exe().map_err(|e| e.to_string())?;
|
||||
// Canonicalize to resolve symlinks (e.g., npm global bin symlink -> actual binary)
|
||||
let exe_path = exe_path.canonicalize().unwrap_or(exe_path);
|
||||
let exe_dir = exe_path.parent().unwrap();
|
||||
|
||||
let daemon_paths = [
|
||||
let mut daemon_paths = vec![
|
||||
exe_dir.join("daemon.js"),
|
||||
exe_dir.join("../dist/daemon.js"),
|
||||
PathBuf::from("dist/daemon.js"),
|
||||
];
|
||||
|
||||
// Check AGENT_BROWSER_HOME environment variable
|
||||
if let Ok(home) = env::var("AGENT_BROWSER_HOME") {
|
||||
let home_path = PathBuf::from(&home);
|
||||
daemon_paths.insert(0, home_path.join("dist/daemon.js"));
|
||||
daemon_paths.insert(1, home_path.join("daemon.js"));
|
||||
}
|
||||
|
||||
let daemon_path = daemon_paths
|
||||
.iter()
|
||||
.find(|p| p.exists())
|
||||
.ok_or("Daemon not found. Run from project directory or ensure daemon.js is alongside binary.")?;
|
||||
.ok_or("Daemon not found. Set AGENT_BROWSER_HOME environment variable or run from project directory.")?;
|
||||
|
||||
// Spawn daemon as a fully detached background process
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::process::CommandExt;
|
||||
|
||||
|
||||
let mut cmd = Command::new("node");
|
||||
cmd.arg(daemon_path)
|
||||
.env("AGENT_BROWSER_DAEMON", "1")
|
||||
@@ -186,6 +319,62 @@ pub fn ensure_daemon(session: &str, headed: bool) -> Result<(), String> {
|
||||
cmd.env("AGENT_BROWSER_HEADED", "1");
|
||||
}
|
||||
|
||||
if let Some(path) = executable_path {
|
||||
cmd.env("AGENT_BROWSER_EXECUTABLE_PATH", path);
|
||||
}
|
||||
|
||||
if !extensions.is_empty() {
|
||||
cmd.env("AGENT_BROWSER_EXTENSIONS", extensions.join(","));
|
||||
}
|
||||
|
||||
if let Some(a) = args {
|
||||
cmd.env("AGENT_BROWSER_ARGS", a);
|
||||
}
|
||||
|
||||
if let Some(ua) = user_agent {
|
||||
cmd.env("AGENT_BROWSER_USER_AGENT", ua);
|
||||
}
|
||||
|
||||
if let Some(p) = proxy {
|
||||
cmd.env("AGENT_BROWSER_PROXY", p);
|
||||
}
|
||||
|
||||
if let Some(pb) = proxy_bypass {
|
||||
cmd.env("AGENT_BROWSER_PROXY_BYPASS", pb);
|
||||
}
|
||||
|
||||
if ignore_https_errors {
|
||||
cmd.env("AGENT_BROWSER_IGNORE_HTTPS_ERRORS", "1");
|
||||
}
|
||||
|
||||
if allow_file_access {
|
||||
cmd.env("AGENT_BROWSER_ALLOW_FILE_ACCESS", "1");
|
||||
}
|
||||
|
||||
if let Some(st) = state {
|
||||
cmd.env("AGENT_BROWSER_STATE", st);
|
||||
}
|
||||
|
||||
if let Some(p) = provider {
|
||||
cmd.env("AGENT_BROWSER_PROVIDER", p);
|
||||
}
|
||||
|
||||
if let Some(d) = device {
|
||||
cmd.env("AGENT_BROWSER_IOS_DEVICE", d);
|
||||
}
|
||||
|
||||
if let Some(sn) = session_name {
|
||||
cmd.env("AGENT_BROWSER_SESSION_NAME", sn);
|
||||
}
|
||||
|
||||
cmd.env("AGENT_BROWSER_STEALTH", "1");
|
||||
if debug {
|
||||
cmd.env("AGENT_BROWSER_DEBUG", "1");
|
||||
}
|
||||
if let Some(dp) = download_path {
|
||||
cmd.env("AGENT_BROWSER_DOWNLOAD_PATH", dp);
|
||||
}
|
||||
|
||||
// Create new process group and session to fully detach
|
||||
unsafe {
|
||||
cmd.pre_exec(|| {
|
||||
@@ -197,15 +386,17 @@ pub fn ensure_daemon(session: &str, headed: bool) -> Result<(), String> {
|
||||
|
||||
cmd.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
.stderr(Stdio::null());
|
||||
cmd.spawn()
|
||||
.map_err(|e| format!("Failed to start daemon: {}", e))?;
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
|
||||
|
||||
// On Windows, call node directly. Command::new handles PATH resolution (node.exe or node.cmd)
|
||||
// and automatically quotes arguments containing spaces.
|
||||
let mut cmd = Command::new("node");
|
||||
cmd.arg(daemon_path)
|
||||
.env("AGENT_BROWSER_DAEMON", "1")
|
||||
@@ -215,26 +406,87 @@ pub fn ensure_daemon(session: &str, headed: bool) -> Result<(), String> {
|
||||
cmd.env("AGENT_BROWSER_HEADED", "1");
|
||||
}
|
||||
|
||||
if let Some(path) = executable_path {
|
||||
cmd.env("AGENT_BROWSER_EXECUTABLE_PATH", path);
|
||||
}
|
||||
|
||||
if !extensions.is_empty() {
|
||||
cmd.env("AGENT_BROWSER_EXTENSIONS", extensions.join(","));
|
||||
}
|
||||
|
||||
if let Some(a) = args {
|
||||
cmd.env("AGENT_BROWSER_ARGS", a);
|
||||
}
|
||||
|
||||
if let Some(ua) = user_agent {
|
||||
cmd.env("AGENT_BROWSER_USER_AGENT", ua);
|
||||
}
|
||||
|
||||
if let Some(p) = proxy {
|
||||
cmd.env("AGENT_BROWSER_PROXY", p);
|
||||
}
|
||||
|
||||
if let Some(pb) = proxy_bypass {
|
||||
cmd.env("AGENT_BROWSER_PROXY_BYPASS", pb);
|
||||
}
|
||||
|
||||
if ignore_https_errors {
|
||||
cmd.env("AGENT_BROWSER_IGNORE_HTTPS_ERRORS", "1");
|
||||
}
|
||||
|
||||
if allow_file_access {
|
||||
cmd.env("AGENT_BROWSER_ALLOW_FILE_ACCESS", "1");
|
||||
}
|
||||
|
||||
if let Some(st) = state {
|
||||
cmd.env("AGENT_BROWSER_STATE", st);
|
||||
}
|
||||
|
||||
if let Some(p) = provider {
|
||||
cmd.env("AGENT_BROWSER_PROVIDER", p);
|
||||
}
|
||||
|
||||
if let Some(d) = device {
|
||||
cmd.env("AGENT_BROWSER_IOS_DEVICE", d);
|
||||
}
|
||||
|
||||
if let Some(sn) = session_name {
|
||||
cmd.env("AGENT_BROWSER_SESSION_NAME", sn);
|
||||
}
|
||||
|
||||
cmd.env("AGENT_BROWSER_STEALTH", "1");
|
||||
if debug {
|
||||
cmd.env("AGENT_BROWSER_DEBUG", "1");
|
||||
}
|
||||
if let Some(dp) = download_path {
|
||||
cmd.env("AGENT_BROWSER_DOWNLOAD_PATH", dp);
|
||||
}
|
||||
|
||||
// CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS
|
||||
const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
|
||||
const DETACHED_PROCESS: u32 = 0x00000008;
|
||||
|
||||
|
||||
cmd.creation_flags(CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
.stderr(Stdio::null());
|
||||
cmd.spawn()
|
||||
.map_err(|e| format!("Failed to start daemon: {}", e))?;
|
||||
}
|
||||
|
||||
for _ in 0..50 {
|
||||
if daemon_ready(session) {
|
||||
return Ok(());
|
||||
return Ok(DaemonResult {
|
||||
already_running: false,
|
||||
});
|
||||
}
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
|
||||
Err("Daemon failed to start".to_string())
|
||||
Err(format!(
|
||||
"Daemon failed to start (socket: {})",
|
||||
get_socket_dir().join(format!("{}.sock", session)).display()
|
||||
))
|
||||
}
|
||||
|
||||
fn connect(session: &str) -> Result<Connection, String> {
|
||||
@@ -255,12 +507,65 @@ fn connect(session: &str) -> Result<Connection, String> {
|
||||
}
|
||||
|
||||
pub fn send_command(cmd: Value, session: &str) -> Result<Response, String> {
|
||||
// Retry logic for transient errors (EAGAIN/EWOULDBLOCK/connection issues)
|
||||
const MAX_RETRIES: u32 = 5;
|
||||
const RETRY_DELAY_MS: u64 = 200;
|
||||
|
||||
let mut last_error = String::new();
|
||||
|
||||
for attempt in 0..MAX_RETRIES {
|
||||
if attempt > 0 {
|
||||
thread::sleep(Duration::from_millis(RETRY_DELAY_MS * (attempt as u64)));
|
||||
}
|
||||
|
||||
match send_command_once(&cmd, session) {
|
||||
Ok(response) => return Ok(response),
|
||||
Err(e) => {
|
||||
if is_transient_error(&e) {
|
||||
last_error = e;
|
||||
continue;
|
||||
}
|
||||
// Non-transient error, fail immediately
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(format!(
|
||||
"{} (after {} retries - daemon may be busy or unresponsive)",
|
||||
last_error, MAX_RETRIES
|
||||
))
|
||||
}
|
||||
|
||||
/// Check if an error is transient and worth retrying.
|
||||
/// Transient errors include:
|
||||
/// - EAGAIN/EWOULDBLOCK (os error 35 on macOS, 11 on Linux)
|
||||
/// - EOF errors (daemon closed connection before responding)
|
||||
/// - Connection reset/broken pipe (daemon crashed or restarting)
|
||||
/// - Connection refused/socket not found (daemon still starting)
|
||||
fn is_transient_error(error: &str) -> bool {
|
||||
error.contains("os error 35") // EAGAIN on macOS
|
||||
|| error.contains("os error 11") // EAGAIN on Linux
|
||||
|| error.contains("WouldBlock")
|
||||
|| error.contains("Resource temporarily unavailable")
|
||||
|| error.contains("EOF")
|
||||
|| error.contains("line 1 column 0") // Empty JSON response
|
||||
|| error.contains("Connection reset")
|
||||
|| error.contains("Broken pipe")
|
||||
|| error.contains("os error 54") // Connection reset by peer (macOS)
|
||||
|| error.contains("os error 104") // Connection reset by peer (Linux)
|
||||
|| error.contains("os error 2") // No such file or directory (socket gone)
|
||||
|| error.contains("os error 61") // Connection refused (macOS)
|
||||
|| error.contains("os error 111") // Connection refused (Linux)
|
||||
}
|
||||
|
||||
fn send_command_once(cmd: &Value, session: &str) -> Result<Response, String> {
|
||||
let mut stream = connect(session)?;
|
||||
|
||||
stream.set_read_timeout(Some(Duration::from_secs(30))).ok();
|
||||
stream.set_write_timeout(Some(Duration::from_secs(5))).ok();
|
||||
|
||||
let mut json_str = serde_json::to_string(&cmd).map_err(|e| e.to_string())?;
|
||||
let mut json_str = serde_json::to_string(cmd).map_err(|e| e.to_string())?;
|
||||
json_str.push('\n');
|
||||
|
||||
stream
|
||||
@@ -275,3 +580,195 @@ pub fn send_command(cmd: Value, session: &str) -> Result<Response, String> {
|
||||
|
||||
serde_json::from_str(&response_line).map_err(|e| format!("Invalid response: {}", e))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::{Mutex, MutexGuard};
|
||||
|
||||
// Mutex to prevent parallel tests from interfering with env vars
|
||||
static ENV_MUTEX: Mutex<()> = Mutex::new(());
|
||||
|
||||
/// RAII guard that locks env mutex and restores env vars on drop
|
||||
struct EnvGuard<'a> {
|
||||
_lock: MutexGuard<'a, ()>,
|
||||
vars: Vec<(String, Option<String>)>,
|
||||
}
|
||||
|
||||
impl<'a> EnvGuard<'a> {
|
||||
fn new(var_names: &[&str]) -> Self {
|
||||
let lock = ENV_MUTEX.lock().unwrap();
|
||||
let vars = var_names
|
||||
.iter()
|
||||
.map(|&name| (name.to_string(), env::var(name).ok()))
|
||||
.collect();
|
||||
Self { _lock: lock, vars }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for EnvGuard<'_> {
|
||||
fn drop(&mut self) {
|
||||
for (name, value) in &self.vars {
|
||||
match value {
|
||||
Some(v) => env::set_var(name, v),
|
||||
None => env::remove_var(name),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_socket_dir_explicit_override() {
|
||||
let _guard = EnvGuard::new(&["AGENT_BROWSER_SOCKET_DIR", "XDG_RUNTIME_DIR"]);
|
||||
|
||||
env::set_var("AGENT_BROWSER_SOCKET_DIR", "/custom/socket/path");
|
||||
env::remove_var("XDG_RUNTIME_DIR");
|
||||
|
||||
assert_eq!(get_socket_dir(), PathBuf::from("/custom/socket/path"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_socket_dir_ignores_empty_socket_dir() {
|
||||
let _guard = EnvGuard::new(&["AGENT_BROWSER_SOCKET_DIR", "XDG_RUNTIME_DIR"]);
|
||||
|
||||
env::set_var("AGENT_BROWSER_SOCKET_DIR", "");
|
||||
env::remove_var("XDG_RUNTIME_DIR");
|
||||
|
||||
assert!(get_socket_dir()
|
||||
.to_string_lossy()
|
||||
.ends_with(".agent-browser"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_socket_dir_xdg_runtime() {
|
||||
let _guard = EnvGuard::new(&["AGENT_BROWSER_SOCKET_DIR", "XDG_RUNTIME_DIR"]);
|
||||
|
||||
env::remove_var("AGENT_BROWSER_SOCKET_DIR");
|
||||
env::set_var("XDG_RUNTIME_DIR", "/run/user/1000");
|
||||
|
||||
assert_eq!(
|
||||
get_socket_dir(),
|
||||
PathBuf::from("/run/user/1000/agent-browser")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_socket_dir_ignores_empty_xdg_runtime() {
|
||||
let _guard = EnvGuard::new(&["AGENT_BROWSER_SOCKET_DIR", "XDG_RUNTIME_DIR"]);
|
||||
|
||||
env::set_var("AGENT_BROWSER_SOCKET_DIR", "");
|
||||
env::set_var("XDG_RUNTIME_DIR", "");
|
||||
|
||||
assert!(get_socket_dir()
|
||||
.to_string_lossy()
|
||||
.ends_with(".agent-browser"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_socket_dir_home_fallback() {
|
||||
let _guard = EnvGuard::new(&["AGENT_BROWSER_SOCKET_DIR", "XDG_RUNTIME_DIR"]);
|
||||
|
||||
env::remove_var("AGENT_BROWSER_SOCKET_DIR");
|
||||
env::remove_var("XDG_RUNTIME_DIR");
|
||||
|
||||
let result = get_socket_dir();
|
||||
assert!(result.to_string_lossy().ends_with(".agent-browser"));
|
||||
assert!(
|
||||
result.to_string_lossy().contains("home") || result.to_string_lossy().contains("Users")
|
||||
);
|
||||
}
|
||||
|
||||
// === Transient Error Detection Tests ===
|
||||
|
||||
#[test]
|
||||
fn test_is_transient_error_eagain_macos() {
|
||||
assert!(is_transient_error(
|
||||
"Failed to read: Resource temporarily unavailable (os error 35)"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_transient_error_eagain_linux() {
|
||||
assert!(is_transient_error(
|
||||
"Failed to read: Resource temporarily unavailable (os error 11)"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_transient_error_would_block() {
|
||||
assert!(is_transient_error("operation WouldBlock"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_transient_error_resource_unavailable() {
|
||||
assert!(is_transient_error("Resource temporarily unavailable"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_transient_error_eof() {
|
||||
assert!(is_transient_error(
|
||||
"Invalid response: EOF while parsing a value at line 1 column 0"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_transient_error_empty_json() {
|
||||
assert!(is_transient_error(
|
||||
"Invalid response: expected value at line 1 column 0"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_transient_error_connection_reset() {
|
||||
assert!(is_transient_error("Connection reset by peer"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_transient_error_broken_pipe() {
|
||||
assert!(is_transient_error("Broken pipe"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_transient_error_connection_reset_macos() {
|
||||
assert!(is_transient_error(
|
||||
"Failed to send: Connection reset by peer (os error 54)"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_transient_error_connection_reset_linux() {
|
||||
assert!(is_transient_error(
|
||||
"Failed to send: Connection reset by peer (os error 104)"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_transient_error_socket_not_found() {
|
||||
assert!(is_transient_error(
|
||||
"Failed to connect: No such file or directory (os error 2)"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_transient_error_connection_refused_macos() {
|
||||
assert!(is_transient_error(
|
||||
"Failed to connect: Connection refused (os error 61)"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_transient_error_connection_refused_linux() {
|
||||
assert!(is_transient_error(
|
||||
"Failed to connect: Connection refused (os error 111)"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_transient_error_non_transient() {
|
||||
// These should NOT be considered transient
|
||||
assert!(!is_transient_error("Unknown command: foo"));
|
||||
assert!(!is_transient_error("Invalid JSON syntax"));
|
||||
assert!(!is_transient_error("Permission denied"));
|
||||
assert!(!is_transient_error("Daemon not found"));
|
||||
}
|
||||
}
|
||||
|
||||
+1066
-15
File diff suppressed because it is too large
Load Diff
+57
-13
@@ -1,3 +1,4 @@
|
||||
use crate::color;
|
||||
use std::process::{exit, Command, Stdio};
|
||||
|
||||
pub fn run_install(with_deps: bool) {
|
||||
@@ -5,9 +6,15 @@ pub fn run_install(with_deps: bool) {
|
||||
|
||||
if is_linux {
|
||||
if with_deps {
|
||||
println!("\x1b[36mInstalling system dependencies...\x1b[0m");
|
||||
println!("{}", color::cyan("Installing system dependencies..."));
|
||||
|
||||
let (pkg_mgr, deps) = if which_exists("apt-get") {
|
||||
let libasound = if package_exists_apt("libasound2t64") {
|
||||
"libasound2t64"
|
||||
} else {
|
||||
"libasound2"
|
||||
};
|
||||
|
||||
(
|
||||
"apt-get",
|
||||
vec![
|
||||
@@ -30,7 +37,7 @@ pub fn run_install(with_deps: bool) {
|
||||
"libcairo2",
|
||||
"libgdk-pixbuf-2.0-0",
|
||||
"libxrender1",
|
||||
"libasound2",
|
||||
libasound,
|
||||
"libfreetype6",
|
||||
"libfontconfig1",
|
||||
"libdbus-1-3",
|
||||
@@ -93,7 +100,10 @@ pub fn run_install(with_deps: bool) {
|
||||
],
|
||||
)
|
||||
} else {
|
||||
eprintln!("\x1b[31m✗\x1b[0m No supported package manager found (apt-get, dnf, or yum)");
|
||||
eprintln!(
|
||||
"{} No supported package manager found (apt-get, dnf, or yum)",
|
||||
color::error_indicator()
|
||||
);
|
||||
exit(1);
|
||||
};
|
||||
|
||||
@@ -112,45 +122,68 @@ pub fn run_install(with_deps: bool) {
|
||||
|
||||
match status {
|
||||
Ok(s) if s.success() => {
|
||||
println!("\x1b[32m✓\x1b[0m System dependencies installed")
|
||||
println!("{} System dependencies installed", color::success_indicator())
|
||||
}
|
||||
Ok(_) => eprintln!(
|
||||
"\x1b[33m⚠\x1b[0m Failed to install some dependencies. You may need to run manually with sudo."
|
||||
"{} Failed to install some dependencies. You may need to run manually with sudo.",
|
||||
color::warning_indicator()
|
||||
),
|
||||
Err(e) => eprintln!("\x1b[33m⚠\x1b[0m Could not run install command: {}", e),
|
||||
Err(e) => eprintln!("{} Could not run install command: {}", color::warning_indicator(), e),
|
||||
}
|
||||
} else {
|
||||
println!("\x1b[33m⚠\x1b[0m Linux detected. If browser fails to launch, run:");
|
||||
println!(
|
||||
"{} Linux detected. If browser fails to launch, run:",
|
||||
color::warning_indicator()
|
||||
);
|
||||
println!(" agent-browser install --with-deps");
|
||||
println!(" or: npx playwright install-deps chromium");
|
||||
println!();
|
||||
}
|
||||
}
|
||||
|
||||
println!("\x1b[36mInstalling Chromium browser...\x1b[0m");
|
||||
println!("{}", color::cyan("Installing Chromium browser..."));
|
||||
|
||||
// On Windows, we need to use cmd.exe to run npx because npx is actually npx.cmd
|
||||
// and Command::new() doesn't resolve .cmd files the way the shell does.
|
||||
// Pass the entire command as a single string to /c to handle paths with spaces.
|
||||
#[cfg(windows)]
|
||||
let status = Command::new("cmd")
|
||||
.args(["/c", "npx playwright install chromium"])
|
||||
.status();
|
||||
|
||||
#[cfg(not(windows))]
|
||||
let status = Command::new("npx")
|
||||
.args(["playwright", "install", "chromium"])
|
||||
.status();
|
||||
|
||||
match status {
|
||||
Ok(s) if s.success() => {
|
||||
println!("\x1b[32m✓\x1b[0m Chromium installed successfully");
|
||||
println!(
|
||||
"{} Chromium installed successfully",
|
||||
color::success_indicator()
|
||||
);
|
||||
if is_linux && !with_deps {
|
||||
println!();
|
||||
println!("\x1b[33mNote:\x1b[0m If you see \"shared library\" errors when running, use:");
|
||||
println!(
|
||||
"{} If you see \"shared library\" errors when running, use:",
|
||||
color::yellow("Note:")
|
||||
);
|
||||
println!(" agent-browser install --with-deps");
|
||||
}
|
||||
}
|
||||
Ok(_) => {
|
||||
eprintln!("\x1b[31m✗\x1b[0m Failed to install browser");
|
||||
eprintln!("{} Failed to install browser", color::error_indicator());
|
||||
if is_linux {
|
||||
println!("\x1b[33mTip:\x1b[0m Try installing system dependencies first:");
|
||||
println!(
|
||||
"{} Try installing system dependencies first:",
|
||||
color::yellow("Tip:")
|
||||
);
|
||||
println!(" agent-browser install --with-deps");
|
||||
}
|
||||
exit(1);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("\x1b[31m✗\x1b[0m Failed to run npx: {}", e);
|
||||
eprintln!("{} Failed to run npx: {}", color::error_indicator(), e);
|
||||
eprintln!("Make sure Node.js is installed and npx is in your PATH");
|
||||
exit(1);
|
||||
}
|
||||
@@ -179,3 +212,14 @@ fn which_exists(cmd: &str) -> bool {
|
||||
.unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
fn package_exists_apt(pkg: &str) -> bool {
|
||||
Command::new("apt-cache")
|
||||
.arg("show")
|
||||
.arg(pkg)
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.status()
|
||||
.map(|s| s.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
+678
-33
@@ -1,55 +1,86 @@
|
||||
mod color;
|
||||
mod commands;
|
||||
mod connection;
|
||||
mod flags;
|
||||
mod install;
|
||||
mod output;
|
||||
mod validation;
|
||||
|
||||
use serde_json::json;
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::process::exit;
|
||||
|
||||
#[cfg(unix)]
|
||||
use libc;
|
||||
|
||||
#[cfg(windows)]
|
||||
use windows_sys::Win32::Foundation::CloseHandle;
|
||||
#[cfg(windows)]
|
||||
use windows_sys::Win32::System::Threading::{OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION};
|
||||
|
||||
use commands::{gen_id, parse_command, ParseError};
|
||||
use connection::{ensure_daemon, send_command};
|
||||
use connection::{ensure_daemon, get_socket_dir, send_command};
|
||||
use flags::{clean_args, parse_flags};
|
||||
use install::run_install;
|
||||
use output::{print_help, print_response};
|
||||
use output::{print_command_help, print_help, print_response, print_version};
|
||||
|
||||
fn parse_proxy(proxy_str: &str) -> serde_json::Value {
|
||||
let Some(protocol_end) = proxy_str.find("://") else {
|
||||
return json!({ "server": proxy_str });
|
||||
};
|
||||
let protocol = &proxy_str[..protocol_end + 3];
|
||||
let rest = &proxy_str[protocol_end + 3..];
|
||||
|
||||
let Some(at_pos) = rest.rfind('@') else {
|
||||
return json!({ "server": proxy_str });
|
||||
};
|
||||
|
||||
let creds = &rest[..at_pos];
|
||||
let server_part = &rest[at_pos + 1..];
|
||||
let server = format!("{}{}", protocol, server_part);
|
||||
|
||||
let Some(colon_pos) = creds.find(':') else {
|
||||
return json!({
|
||||
"server": server,
|
||||
"username": creds,
|
||||
"password": ""
|
||||
});
|
||||
};
|
||||
|
||||
json!({
|
||||
"server": server,
|
||||
"username": &creds[..colon_pos],
|
||||
"password": &creds[colon_pos + 1..]
|
||||
})
|
||||
}
|
||||
|
||||
fn run_session(args: &[String], session: &str, json_mode: bool) {
|
||||
let subcommand = args.get(1).map(|s| s.as_str());
|
||||
|
||||
match subcommand {
|
||||
Some("list") => {
|
||||
let tmp = env::temp_dir();
|
||||
let socket_dir = get_socket_dir();
|
||||
let mut sessions: Vec<String> = Vec::new();
|
||||
|
||||
if let Ok(entries) = fs::read_dir(&tmp) {
|
||||
if let Ok(entries) = fs::read_dir(&socket_dir) {
|
||||
for entry in entries.flatten() {
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
// Look for socket files (Unix) or pid files
|
||||
if name.starts_with("agent-browser-") && name.ends_with(".pid") {
|
||||
let session_name = name
|
||||
.strip_prefix("agent-browser-")
|
||||
.and_then(|s| s.strip_suffix(".pid"))
|
||||
.unwrap_or("");
|
||||
// Look for pid files in socket directory
|
||||
if name.ends_with(".pid") {
|
||||
let session_name = name.strip_suffix(".pid").unwrap_or("");
|
||||
if !session_name.is_empty() {
|
||||
// Check if session is actually running
|
||||
let pid_path = tmp.join(&name);
|
||||
let pid_path = socket_dir.join(&name);
|
||||
if let Ok(pid_str) = fs::read_to_string(&pid_path) {
|
||||
if let Ok(pid) = pid_str.trim().parse::<u32>() {
|
||||
#[cfg(unix)]
|
||||
let running = unsafe { libc::kill(pid as i32, 0) == 0 };
|
||||
let running = unsafe {
|
||||
libc::kill(pid as i32, 0) == 0
|
||||
|| std::io::Error::last_os_error().raw_os_error()
|
||||
!= Some(libc::ESRCH)
|
||||
};
|
||||
#[cfg(windows)]
|
||||
let running = unsafe {
|
||||
let handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid);
|
||||
let handle =
|
||||
OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid);
|
||||
if handle != 0 {
|
||||
CloseHandle(handle);
|
||||
true
|
||||
@@ -77,7 +108,11 @@ fn run_session(args: &[String], session: &str, json_mode: bool) {
|
||||
} else {
|
||||
println!("Active sessions:");
|
||||
for s in &sessions {
|
||||
let marker = if s == session { "→" } else { " " };
|
||||
let marker = if s == session {
|
||||
color::cyan("→")
|
||||
} else {
|
||||
" ".to_string()
|
||||
};
|
||||
println!("{} {}", marker, s);
|
||||
}
|
||||
}
|
||||
@@ -94,24 +129,104 @@ fn run_session(args: &[String], session: &str, json_mode: bool) {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// Ignore SIGPIPE to prevent panic when piping to head/tail
|
||||
#[cfg(unix)]
|
||||
unsafe {
|
||||
libc::signal(libc::SIGPIPE, libc::SIG_DFL);
|
||||
}
|
||||
|
||||
let args: Vec<String> = env::args().skip(1).collect();
|
||||
let flags = parse_flags(&args);
|
||||
let clean = clean_args(&args);
|
||||
|
||||
if clean.is_empty() || args.iter().any(|a| a == "--help" || a == "-h") {
|
||||
let has_help = args.iter().any(|a| a == "--help" || a == "-h");
|
||||
let has_version = args.iter().any(|a| a == "--version" || a == "-V");
|
||||
|
||||
if has_help {
|
||||
if let Some(cmd) = clean.first() {
|
||||
if print_command_help(cmd) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
print_help();
|
||||
return;
|
||||
}
|
||||
|
||||
if has_version {
|
||||
print_version();
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(ref risk_mode) = flags.risk_mode {
|
||||
if !matches!(risk_mode.as_str(), "off" | "warn" | "block") {
|
||||
let msg = format!(
|
||||
"Invalid --risk-mode value: {} (expected off, warn, or block)",
|
||||
risk_mode
|
||||
);
|
||||
if flags.json {
|
||||
println!(r#"{{"success":false,"error":"{}"}}"#, msg);
|
||||
} else {
|
||||
eprintln!("{} {}", color::error_indicator(), msg);
|
||||
}
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if args.iter().any(|a| a == "--profile") {
|
||||
let msg =
|
||||
"Project policy: --profile is forbidden. Use your existing browser and --session-name for state persistence.";
|
||||
if flags.json {
|
||||
println!(r#"{{"success":false,"error":"{}"}}"#, msg);
|
||||
} else {
|
||||
eprintln!("{} {}", color::error_indicator(), msg);
|
||||
}
|
||||
exit(1);
|
||||
}
|
||||
if env::var("AGENT_BROWSER_PROFILE").is_ok() {
|
||||
let msg =
|
||||
"Project policy: AGENT_BROWSER_PROFILE is forbidden. Remove it and use --session-name.";
|
||||
if flags.json {
|
||||
println!(r#"{{"success":false,"error":"{}"}}"#, msg);
|
||||
} else {
|
||||
eprintln!("{} {}", color::error_indicator(), msg);
|
||||
}
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if args.iter().any(|a| a == "--channel") {
|
||||
let msg = "Project policy: --channel is forbidden. Browser selection follows your existing browser session.";
|
||||
if flags.json {
|
||||
println!(r#"{{"success":false,"error":"{}"}}"#, msg);
|
||||
} else {
|
||||
eprintln!("{} {}", color::error_indicator(), msg);
|
||||
}
|
||||
exit(1);
|
||||
}
|
||||
if env::var("AGENT_BROWSER_CHANNEL").is_ok() {
|
||||
let msg =
|
||||
"Project policy: AGENT_BROWSER_CHANNEL is forbidden. Remove it and use your existing browser session.";
|
||||
if flags.json {
|
||||
println!(r#"{{"success":false,"error":"{}"}}"#, msg);
|
||||
} else {
|
||||
eprintln!("{} {}", color::error_indicator(), msg);
|
||||
}
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if clean.is_empty() {
|
||||
print_help();
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle install separately
|
||||
if clean.get(0).map(|s| s.as_str()) == Some("install") {
|
||||
if clean.first().map(|s| s.as_str()) == Some("install") {
|
||||
let with_deps = args.iter().any(|a| a == "--with-deps" || a == "-d");
|
||||
run_install(with_deps);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle session separately (doesn't need daemon)
|
||||
if clean.get(0).map(|s| s.as_str()) == Some("session") {
|
||||
if clean.first().map(|s| s.as_str()) == Some("session") {
|
||||
run_session(&clean, &flags.session, flags.json);
|
||||
return;
|
||||
}
|
||||
@@ -124,6 +239,8 @@ fn main() {
|
||||
ParseError::UnknownCommand { .. } => "unknown_command",
|
||||
ParseError::UnknownSubcommand { .. } => "unknown_subcommand",
|
||||
ParseError::MissingArguments { .. } => "missing_arguments",
|
||||
ParseError::InvalidValue { .. } => "invalid_value",
|
||||
ParseError::InvalidSessionName { .. } => "invalid_session_name",
|
||||
};
|
||||
println!(
|
||||
r#"{{"success":false,"error":"{}","type":"{}"}}"#,
|
||||
@@ -131,35 +248,504 @@ fn main() {
|
||||
error_type
|
||||
);
|
||||
} else {
|
||||
eprintln!("\x1b[31m{}\x1b[0m", e.format());
|
||||
eprintln!("{}", color::red(&e.format()));
|
||||
}
|
||||
exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = ensure_daemon(&flags.session, flags.headed) {
|
||||
// Validate session name before starting daemon
|
||||
if let Some(ref name) = flags.session_name {
|
||||
if !validation::is_valid_session_name(name) {
|
||||
let msg = validation::session_name_error(name);
|
||||
if flags.json {
|
||||
println!(
|
||||
r#"{{"success":false,"error":"{}","type":"invalid_session_name"}}"#,
|
||||
msg.replace('"', "\\\"")
|
||||
);
|
||||
} else {
|
||||
eprintln!("{} {}", color::error_indicator(), msg);
|
||||
}
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
let daemon_result = match ensure_daemon(
|
||||
&flags.session,
|
||||
flags.headed,
|
||||
flags.executable_path.as_deref(),
|
||||
&flags.extensions,
|
||||
flags.args.as_deref(),
|
||||
flags.user_agent.as_deref(),
|
||||
flags.proxy.as_deref(),
|
||||
flags.proxy_bypass.as_deref(),
|
||||
flags.ignore_https_errors,
|
||||
flags.allow_file_access,
|
||||
flags.state.as_deref(),
|
||||
flags.provider.as_deref(),
|
||||
flags.device.as_deref(),
|
||||
flags.session_name.as_deref(),
|
||||
flags.debug,
|
||||
flags.download_path.as_deref(),
|
||||
) {
|
||||
Ok(result) => result,
|
||||
Err(e) => {
|
||||
if flags.json {
|
||||
println!(r#"{{"success":false,"error":"{}"}}"#, e);
|
||||
} else {
|
||||
eprintln!("{} {}", color::error_indicator(), e);
|
||||
}
|
||||
exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
// Warn if launch-time options were explicitly passed via CLI but daemon was already running
|
||||
// Only warn about flags that were passed on the command line, not those set via environment
|
||||
// variables (since the daemon already uses the env vars when it starts).
|
||||
if daemon_result.already_running {
|
||||
let ignored_flags: Vec<&str> = [
|
||||
if flags.cli_executable_path {
|
||||
Some("--executable-path")
|
||||
} else {
|
||||
None
|
||||
},
|
||||
if flags.cli_extensions {
|
||||
Some("--extension")
|
||||
} else {
|
||||
None
|
||||
},
|
||||
if flags.cli_state {
|
||||
Some("--state")
|
||||
} else {
|
||||
None
|
||||
},
|
||||
if flags.cli_args { Some("--args") } else { None },
|
||||
if flags.cli_user_agent {
|
||||
Some("--user-agent")
|
||||
} else {
|
||||
None
|
||||
},
|
||||
if flags.cli_proxy {
|
||||
Some("--proxy")
|
||||
} else {
|
||||
None
|
||||
},
|
||||
if flags.cli_proxy_bypass {
|
||||
Some("--proxy-bypass")
|
||||
} else {
|
||||
None
|
||||
},
|
||||
flags.ignore_https_errors.then_some("--ignore-https-errors"),
|
||||
flags.cli_allow_file_access.then_some("--allow-file-access"),
|
||||
flags.cli_download_path.then_some("--download-path"),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect();
|
||||
|
||||
if !ignored_flags.is_empty() && !flags.json {
|
||||
eprintln!(
|
||||
"{} {} ignored: daemon already running. Use 'agent-browser close' first to restart with new options.",
|
||||
color::warning_indicator(),
|
||||
ignored_flags.join(", ")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate mutually exclusive options
|
||||
if flags.cdp.is_some() && flags.provider.is_some() {
|
||||
let msg = "Cannot use --cdp and -p/--provider together";
|
||||
if flags.json {
|
||||
println!(r#"{{"success":false,"error":"{}"}}"#, e);
|
||||
println!(r#"{{"success":false,"error":"{}"}}"#, msg);
|
||||
} else {
|
||||
eprintln!("\x1b[31m✗\x1b[0m {}", e);
|
||||
eprintln!("{} {}", color::error_indicator(), msg);
|
||||
}
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// If --headed flag is set, send launch command first to switch to headed mode
|
||||
if flags.headed {
|
||||
let launch_cmd = json!({ "id": gen_id(), "action": "launch", "headless": false });
|
||||
if let Err(e) = send_command(launch_cmd, &flags.session) {
|
||||
if !flags.json {
|
||||
eprintln!("\x1b[33m⚠\x1b[0m Could not switch to headed mode: {}", e);
|
||||
if flags.auto_connect && flags.cdp.is_some() {
|
||||
let msg = "Cannot use --auto-connect and --cdp together";
|
||||
if flags.json {
|
||||
println!(r#"{{"success":false,"error":"{}"}}"#, msg);
|
||||
} else {
|
||||
eprintln!("{} {}", color::error_indicator(), msg);
|
||||
}
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if flags.auto_connect && flags.provider.is_some() {
|
||||
let msg = "Cannot use --auto-connect and -p/--provider together";
|
||||
if flags.json {
|
||||
println!(r#"{{"success":false,"error":"{}"}}"#, msg);
|
||||
} else {
|
||||
eprintln!("{} {}", color::error_indicator(), msg);
|
||||
}
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if flags.provider.is_some() && !flags.extensions.is_empty() {
|
||||
let msg = "Cannot use --extension with -p/--provider (extensions require local browser)";
|
||||
if flags.json {
|
||||
println!(r#"{{"success":false,"error":"{}"}}"#, msg);
|
||||
} else {
|
||||
eprintln!("{} {}", color::error_indicator(), msg);
|
||||
}
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if flags.cdp.is_some() && !flags.extensions.is_empty() {
|
||||
let msg = "Cannot use --extension with --cdp (extensions require local browser)";
|
||||
if flags.json {
|
||||
println!(r#"{{"success":false,"error":"{}"}}"#, msg);
|
||||
} else {
|
||||
eprintln!("{} {}", color::error_indicator(), msg);
|
||||
}
|
||||
exit(1);
|
||||
}
|
||||
|
||||
let mut attached_to_existing_browser = false;
|
||||
|
||||
// Auto-connect to existing browser
|
||||
if flags.auto_connect {
|
||||
let mut launch_cmd = json!({
|
||||
"id": gen_id(),
|
||||
"action": "launch",
|
||||
"autoConnect": true
|
||||
});
|
||||
|
||||
if flags.ignore_https_errors {
|
||||
launch_cmd["ignoreHTTPSErrors"] = json!(true);
|
||||
}
|
||||
|
||||
if let Some(ref cs) = flags.color_scheme {
|
||||
launch_cmd["colorScheme"] = json!(cs);
|
||||
}
|
||||
|
||||
if let Some(ref dp) = flags.download_path {
|
||||
launch_cmd["downloadPath"] = json!(dp);
|
||||
}
|
||||
|
||||
let err = match send_command(launch_cmd, &flags.session) {
|
||||
Ok(resp) if resp.success => None,
|
||||
Ok(resp) => Some(
|
||||
resp.error
|
||||
.unwrap_or_else(|| "Auto-connect failed".to_string()),
|
||||
),
|
||||
Err(e) => Some(e.to_string()),
|
||||
};
|
||||
|
||||
if let Some(msg) = err {
|
||||
if flags.json {
|
||||
println!(r#"{{"success":false,"error":"{}"}}"#, msg);
|
||||
} else {
|
||||
eprintln!("{} {}", color::error_indicator(), msg);
|
||||
}
|
||||
exit(1);
|
||||
}
|
||||
|
||||
attached_to_existing_browser = true;
|
||||
}
|
||||
|
||||
// Connect via CDP if --cdp flag is set
|
||||
// Accepts either a port number (e.g., "9222") or a full URL (e.g., "ws://..." or "wss://...")
|
||||
if let Some(ref cdp_value) = flags.cdp {
|
||||
let mut launch_cmd = if cdp_value.starts_with("ws://")
|
||||
|| cdp_value.starts_with("wss://")
|
||||
|| cdp_value.starts_with("http://")
|
||||
|| cdp_value.starts_with("https://")
|
||||
{
|
||||
// It's a URL - use cdpUrl field
|
||||
json!({
|
||||
"id": gen_id(),
|
||||
"action": "launch",
|
||||
"cdpUrl": cdp_value
|
||||
})
|
||||
} else {
|
||||
// It's a port number - validate and use cdpPort field
|
||||
let cdp_port: u16 = match cdp_value.parse::<u32>() {
|
||||
Ok(0) => {
|
||||
let msg = "Invalid CDP port: port must be greater than 0".to_string();
|
||||
if flags.json {
|
||||
println!(r#"{{"success":false,"error":"{}"}}"#, msg);
|
||||
} else {
|
||||
eprintln!("{} {}", color::error_indicator(), msg);
|
||||
}
|
||||
exit(1);
|
||||
}
|
||||
Ok(p) if p > 65535 => {
|
||||
let msg = format!(
|
||||
"Invalid CDP port: {} is out of range (valid range: 1-65535)",
|
||||
p
|
||||
);
|
||||
if flags.json {
|
||||
println!(r#"{{"success":false,"error":"{}"}}"#, msg);
|
||||
} else {
|
||||
eprintln!("{} {}", color::error_indicator(), msg);
|
||||
}
|
||||
exit(1);
|
||||
}
|
||||
Ok(p) => p as u16,
|
||||
Err(_) => {
|
||||
let msg = format!(
|
||||
"Invalid CDP value: '{}' is not a valid port number or URL",
|
||||
cdp_value
|
||||
);
|
||||
if flags.json {
|
||||
println!(r#"{{"success":false,"error":"{}"}}"#, msg);
|
||||
} else {
|
||||
eprintln!("{} {}", color::error_indicator(), msg);
|
||||
}
|
||||
exit(1);
|
||||
}
|
||||
};
|
||||
json!({
|
||||
"id": gen_id(),
|
||||
"action": "launch",
|
||||
"cdpPort": cdp_port
|
||||
})
|
||||
};
|
||||
|
||||
if flags.ignore_https_errors {
|
||||
launch_cmd["ignoreHTTPSErrors"] = json!(true);
|
||||
}
|
||||
|
||||
if let Some(ref cs) = flags.color_scheme {
|
||||
launch_cmd["colorScheme"] = json!(cs);
|
||||
}
|
||||
|
||||
if let Some(ref dp) = flags.download_path {
|
||||
launch_cmd["downloadPath"] = json!(dp);
|
||||
}
|
||||
|
||||
let err = match send_command(launch_cmd, &flags.session) {
|
||||
Ok(resp) if resp.success => None,
|
||||
Ok(resp) => Some(
|
||||
resp.error
|
||||
.unwrap_or_else(|| "CDP connection failed".to_string()),
|
||||
),
|
||||
Err(e) => Some(e.to_string()),
|
||||
};
|
||||
|
||||
if let Some(msg) = err {
|
||||
if flags.json {
|
||||
println!(r#"{{"success":false,"error":"{}"}}"#, msg);
|
||||
} else {
|
||||
eprintln!("{} {}", color::error_indicator(), msg);
|
||||
}
|
||||
exit(1);
|
||||
}
|
||||
|
||||
attached_to_existing_browser = true;
|
||||
}
|
||||
|
||||
// Launch with cloud provider if -p flag is set
|
||||
if let Some(ref provider) = flags.provider {
|
||||
let mut launch_cmd = json!({
|
||||
"id": gen_id(),
|
||||
"action": "launch",
|
||||
"provider": provider
|
||||
});
|
||||
|
||||
if let Some(ref cs) = flags.color_scheme {
|
||||
launch_cmd["colorScheme"] = json!(cs);
|
||||
}
|
||||
|
||||
match send_command(launch_cmd, &flags.session) {
|
||||
Ok(resp) => {
|
||||
if !resp.success {
|
||||
let msg = resp
|
||||
.error
|
||||
.unwrap_or_else(|| "Provider connection failed".to_string());
|
||||
if flags.json {
|
||||
println!(r#"{{"success":false,"error":"{}"}}"#, msg);
|
||||
} else {
|
||||
eprintln!("{} {}", color::error_indicator(), msg);
|
||||
}
|
||||
exit(1);
|
||||
}
|
||||
|
||||
}
|
||||
Err(e) => {
|
||||
if flags.json {
|
||||
println!(r#"{{"success":false,"error":"{}"}}"#, e);
|
||||
} else {
|
||||
eprintln!("{} {}", color::error_indicator(), e);
|
||||
}
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match send_command(cmd, &flags.session) {
|
||||
// Project policy: when no explicit connection mode is provided,
|
||||
// commands should attach to an existing browser.
|
||||
// Try CDP :9333 first, then fall back to auto-connect discovery.
|
||||
let can_try_default_cdp = flags.cdp.is_none()
|
||||
&& !flags.auto_connect
|
||||
&& flags.provider.is_none()
|
||||
&& flags.executable_path.is_none()
|
||||
&& flags.state.is_none()
|
||||
&& flags.proxy.is_none()
|
||||
&& flags.args.is_none()
|
||||
&& flags.user_agent.is_none()
|
||||
&& !flags.ignore_https_errors
|
||||
&& !flags.allow_file_access
|
||||
&& flags.extensions.is_empty();
|
||||
|
||||
if can_try_default_cdp {
|
||||
let mut launch_cmd = json!({
|
||||
"id": gen_id(),
|
||||
"action": "launch",
|
||||
"cdpPort": 9333
|
||||
});
|
||||
|
||||
if let Some(ref cs) = flags.color_scheme {
|
||||
launch_cmd["colorScheme"] = json!(cs);
|
||||
}
|
||||
|
||||
if let Ok(resp) = send_command(launch_cmd, &flags.session) {
|
||||
attached_to_existing_browser = resp.success;
|
||||
}
|
||||
|
||||
if !attached_to_existing_browser {
|
||||
let mut auto_connect_cmd = json!({
|
||||
"id": gen_id(),
|
||||
"action": "launch",
|
||||
"autoConnect": true
|
||||
});
|
||||
|
||||
if let Some(ref cs) = flags.color_scheme {
|
||||
auto_connect_cmd["colorScheme"] = json!(cs);
|
||||
}
|
||||
|
||||
if let Ok(resp) = send_command(auto_connect_cmd, &flags.session) {
|
||||
attached_to_existing_browser = resp.success;
|
||||
}
|
||||
}
|
||||
}
|
||||
if can_try_default_cdp && !attached_to_existing_browser {
|
||||
let msg = "Project policy requires using your existing browser. Could not connect to CDP at localhost:9333 and auto-discovery also failed. Start Chrome with remote debugging (for example, --remote-debugging-port=9333), or pass --cdp <port|url>.";
|
||||
if flags.json {
|
||||
println!(r#"{{"success":false,"error":"{}"}}"#, msg);
|
||||
} else {
|
||||
eprintln!("{} {}", color::error_indicator(), msg);
|
||||
}
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Launch headed browser or configure browser options (without CDP or provider)
|
||||
if (flags.headed
|
||||
|| flags.executable_path.is_some()
|
||||
|| flags.state.is_some()
|
||||
|| flags.proxy.is_some()
|
||||
|| flags.args.is_some()
|
||||
|| flags.user_agent.is_some()
|
||||
|| flags.ignore_https_errors
|
||||
|| flags.allow_file_access
|
||||
|| flags.debug
|
||||
|| flags.color_scheme.is_some()
|
||||
|| flags.download_path.is_some())
|
||||
&& flags.cdp.is_none()
|
||||
&& flags.provider.is_none()
|
||||
&& !attached_to_existing_browser
|
||||
{
|
||||
let mut launch_cmd = json!({
|
||||
"id": gen_id(),
|
||||
"action": "launch",
|
||||
"headless": !flags.headed
|
||||
});
|
||||
|
||||
let cmd_obj = launch_cmd
|
||||
.as_object_mut()
|
||||
.expect("json! macro guarantees object type");
|
||||
|
||||
// Add executable path if specified
|
||||
if let Some(ref exec_path) = flags.executable_path {
|
||||
cmd_obj.insert("executablePath".to_string(), json!(exec_path));
|
||||
}
|
||||
|
||||
// Add state path if specified
|
||||
if let Some(ref state_path) = flags.state {
|
||||
cmd_obj.insert("storageState".to_string(), json!(state_path));
|
||||
}
|
||||
|
||||
if let Some(ref proxy_str) = flags.proxy {
|
||||
let mut proxy_obj = parse_proxy(proxy_str);
|
||||
// Add bypass if specified
|
||||
if let Some(ref bypass) = flags.proxy_bypass {
|
||||
if let Some(obj) = proxy_obj.as_object_mut() {
|
||||
obj.insert("bypass".to_string(), json!(bypass));
|
||||
}
|
||||
}
|
||||
cmd_obj.insert("proxy".to_string(), proxy_obj);
|
||||
}
|
||||
|
||||
if let Some(ref ua) = flags.user_agent {
|
||||
cmd_obj.insert("userAgent".to_string(), json!(ua));
|
||||
}
|
||||
|
||||
if let Some(ref a) = flags.args {
|
||||
// Parse args (comma or newline separated)
|
||||
let args_vec: Vec<String> = a
|
||||
.split(&[',', '\n'][..])
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
cmd_obj.insert("args".to_string(), json!(args_vec));
|
||||
}
|
||||
|
||||
if flags.ignore_https_errors {
|
||||
launch_cmd["ignoreHTTPSErrors"] = json!(true);
|
||||
}
|
||||
|
||||
if flags.allow_file_access {
|
||||
launch_cmd["allowFileAccess"] = json!(true);
|
||||
}
|
||||
|
||||
if let Some(ref cs) = flags.color_scheme {
|
||||
launch_cmd["colorScheme"] = json!(cs);
|
||||
}
|
||||
|
||||
if let Some(ref dp) = flags.download_path {
|
||||
launch_cmd["downloadPath"] = json!(dp);
|
||||
}
|
||||
|
||||
match send_command(launch_cmd, &flags.session) {
|
||||
Ok(resp) => {
|
||||
if !resp.success {
|
||||
// Launch command failed (e.g., invalid state file)
|
||||
let error_msg = resp
|
||||
.error
|
||||
.unwrap_or_else(|| "Browser launch failed".to_string());
|
||||
if flags.json {
|
||||
println!(r#"{{"success":false,"error":"{}"}}"#, error_msg);
|
||||
} else {
|
||||
eprintln!("{} {}", color::error_indicator(), error_msg);
|
||||
}
|
||||
exit(1);
|
||||
}
|
||||
|
||||
}
|
||||
Err(e) => {
|
||||
if flags.json {
|
||||
println!(r#"{{"success":false,"error":"{}"}}"#, e);
|
||||
} else {
|
||||
eprintln!(
|
||||
"{} Could not configure browser: {}",
|
||||
color::error_indicator(),
|
||||
e
|
||||
);
|
||||
}
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match send_command(cmd.clone(), &flags.session) {
|
||||
Ok(resp) => {
|
||||
let success = resp.success;
|
||||
print_response(&resp, flags.json);
|
||||
// Extract action for context-specific output handling
|
||||
let action = cmd.get("action").and_then(|v| v.as_str());
|
||||
print_response(&resp, flags.json, action);
|
||||
if !success {
|
||||
exit(1);
|
||||
}
|
||||
@@ -168,9 +754,68 @@ fn main() {
|
||||
if flags.json {
|
||||
println!(r#"{{"success":false,"error":"{}"}}"#, e);
|
||||
} else {
|
||||
eprintln!("\x1b[31m✗\x1b[0m {}", e);
|
||||
eprintln!("{} {}", color::error_indicator(), e);
|
||||
}
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_proxy_simple() {
|
||||
let result = parse_proxy("http://proxy.com:8080");
|
||||
assert_eq!(result["server"], "http://proxy.com:8080");
|
||||
assert!(result.get("username").is_none());
|
||||
assert!(result.get("password").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_proxy_with_auth() {
|
||||
let result = parse_proxy("http://user:pass@proxy.com:8080");
|
||||
assert_eq!(result["server"], "http://proxy.com:8080");
|
||||
assert_eq!(result["username"], "user");
|
||||
assert_eq!(result["password"], "pass");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_proxy_username_only() {
|
||||
let result = parse_proxy("http://user@proxy.com:8080");
|
||||
assert_eq!(result["server"], "http://proxy.com:8080");
|
||||
assert_eq!(result["username"], "user");
|
||||
assert_eq!(result["password"], "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_proxy_no_protocol() {
|
||||
let result = parse_proxy("proxy.com:8080");
|
||||
assert_eq!(result["server"], "proxy.com:8080");
|
||||
assert!(result.get("username").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_proxy_socks5() {
|
||||
let result = parse_proxy("socks5://proxy.com:1080");
|
||||
assert_eq!(result["server"], "socks5://proxy.com:1080");
|
||||
assert!(result.get("username").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_proxy_socks5_with_auth() {
|
||||
let result = parse_proxy("socks5://admin:secret@proxy.com:1080");
|
||||
assert_eq!(result["server"], "socks5://proxy.com:1080");
|
||||
assert_eq!(result["username"], "admin");
|
||||
assert_eq!(result["password"], "secret");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_proxy_complex_password() {
|
||||
let result = parse_proxy("http://user:p@ss:w0rd@proxy.com:8080");
|
||||
assert_eq!(result["server"], "http://proxy.com:8080");
|
||||
assert_eq!(result["username"], "user");
|
||||
assert_eq!(result["password"], "p@ss:w0rd");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
include!("main.rs");
|
||||
+2429
-37
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,15 @@
|
||||
/// Check if a session name is valid (alphanumeric, hyphens, and underscores only)
|
||||
pub fn is_valid_session_name(name: &str) -> bool {
|
||||
!name.is_empty()
|
||||
&& name
|
||||
.chars()
|
||||
.all(|c| c.is_alphanumeric() || c == '-' || c == '_')
|
||||
}
|
||||
|
||||
/// Generate error message for invalid session name
|
||||
pub fn session_name_error(name: &str) -> String {
|
||||
format!(
|
||||
"Invalid session name '{}'. Only alphanumeric characters, hyphens, and underscores are allowed.",
|
||||
name
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
# 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
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"$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": {}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
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;
|
||||
@@ -0,0 +1,83 @@
|
||||
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,
|
||||
h2: ({ children }: { children?: React.ReactNode }) => {
|
||||
const id = slugify(extractText(children));
|
||||
return <h2 id={id}>{children}</h2>;
|
||||
},
|
||||
h3: ({ children }: { children?: React.ReactNode }) => {
|
||||
const id = slugify(extractText(children));
|
||||
return <h3 id={id}>{children}</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}
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
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);
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"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
@@ -0,0 +1,7 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,119 @@
|
||||
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-haiku-4.5";
|
||||
|
||||
const SYSTEM_PROMPT = `You are a helpful documentation assistant for agent-browser, a headless browser automation CLI designed for AI agents.
|
||||
|
||||
GitHub repository: https://github.com/leeguooooo/agent-browser
|
||||
Documentation: https://agent-browser.dev
|
||||
npm package: agent-browser-stealth
|
||||
|
||||
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({
|
||||
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();
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
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 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
import { pageMetadata } from '@/lib/page-metadata';
|
||||
|
||||
export const metadata = pageMetadata('cdp-mode');
|
||||
|
||||
# CDP Mode
|
||||
|
||||
Connect to an existing browser via Chrome DevTools Protocol:
|
||||
|
||||
Default behavior in this fork: when `--cdp` is omitted, agent-browser auto-attaches to an existing browser by trying `localhost:9333` first, then auto-discovery. If both fail, the command exits (no managed local-launch fallback).
|
||||
|
||||
Project policy:
|
||||
|
||||
- `--profile` / `AGENT_BROWSER_PROFILE` are forbidden
|
||||
- `--channel` / `AGENT_BROWSER_CHANNEL` are forbidden
|
||||
|
||||
```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, 9333)
|
||||
|
||||
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
|
||||
|
||||
Playwright overrides the browser's color scheme to `light` by default when connecting via CDP. Use `--color-scheme` to set a persistent preference:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
## Stealth behavior
|
||||
|
||||
`--stealth` is enabled by default across connection modes, but capabilities depend on how you connect:
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Connection type</th>
|
||||
<th>Stealth capabilities</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Local launch</td>
|
||||
<td>Chromium launch args + context init scripts</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>CDP / auto-connect</td>
|
||||
<td>Context init scripts</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Cloud providers</td>
|
||||
<td>Context init scripts (Kernel may also apply provider-managed stealth)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
Use `--debug` to print the active connection type and applied stealth capabilities.
|
||||
|
||||
## 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>-p <provider></code>
|
||||
</td>
|
||||
<td>
|
||||
Cloud browser provider (<code>browserbase</code>, <code>browseruse</code>,{' '}
|
||||
<code>kernel</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>--full, -f</code>
|
||||
</td>
|
||||
<td>Full page screenshot</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 cloud browser infrastructure when local browsers aren't available:
|
||||
|
||||
```bash
|
||||
# Browserbase
|
||||
export BROWSERBASE_API_KEY="your-api-key"
|
||||
export BROWSERBASE_PROJECT_ID="your-project-id"
|
||||
agent-browser -p browserbase open https://example.com
|
||||
|
||||
# Browser Use
|
||||
export BROWSER_USE_API_KEY="your-api-key"
|
||||
agent-browser -p browseruse open https://example.com
|
||||
|
||||
# Kernel
|
||||
export KERNEL_API_KEY="your-api-key"
|
||||
agent-browser -p kernel open https://example.com
|
||||
|
||||
# Or via environment variable
|
||||
export AGENT_BROWSER_PROVIDER=browserbase
|
||||
agent-browser open https://example.com
|
||||
```
|
||||
|
||||
The `-p` flag takes precedence over `AGENT_BROWSER_PROVIDER`.
|
||||
@@ -0,0 +1,516 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
|
||||
export const metadata = pageMetadata("changelog")
|
||||
|
||||
# Changelog
|
||||
|
||||
## v0.15.0
|
||||
|
||||
<p className="text-[#888] text-sm">February 2026</p>
|
||||
|
||||
### New Features
|
||||
|
||||
- **Authentication vault** -- Store credentials locally (always AES-256-GCM encrypted) and reference them by name. The LLM never sees passwords. Commands: `auth save`, `auth login`, `auth list`, `auth show`, `auth delete`. Passwords can be piped via stdin (`--password-stdin`) to avoid shell history exposure.
|
||||
- **Content boundary markers** -- `--content-boundaries` wraps page-sourced output in structural delimiters with a per-process CSPRNG nonce, so LLMs can distinguish trusted tool output from untrusted page content. In `--json` mode, a `_boundary` object is injected with `nonce` and `origin` fields.
|
||||
- **Domain allowlist** -- `--allowed-domains` restricts navigation, sub-resource requests, WebSocket connections, and EventSource streams to trusted domains. Supports exact match and wildcard prefix patterns (e.g., `*.example.com`).
|
||||
- **Action policy** -- `--action-policy` gates actions using a static JSON policy file with `allow`/`deny` lists across 13 action categories. Auth vault operations bypass policy enforcement.
|
||||
- **Action confirmation** -- `--confirm-actions` requires explicit approval for sensitive action categories. New `confirm` and `deny` commands for orchestrator use. `--confirm-interactive` enables human-in-the-loop terminal prompts (auto-denies if stdin is not a TTY). Pending confirmations auto-deny after 60 seconds.
|
||||
- **Output length limits** -- `--max-output` truncates large page outputs to prevent LLM context flooding.
|
||||
- **`--download-path` option** -- Set a default download directory via flag, `AGENT_BROWSER_DOWNLOAD_PATH` env var, or `downloadPath` config key. Without it, downloads go to a temporary directory deleted when the browser closes.
|
||||
- **`--selector` flag for scroll** -- Scroll within a specific container element instead of the page: `agent-browser scroll down 500 --selector "div.scroll-container"`
|
||||
|
||||
```bash
|
||||
# Auth vault
|
||||
echo "pass" | agent-browser auth save github --url https://github.com/login --username user --password-stdin
|
||||
agent-browser auth login github
|
||||
|
||||
# Security flags
|
||||
agent-browser --content-boundaries --allowed-domains "example.com,*.example.com" --max-output 50000 open https://example.com
|
||||
|
||||
# Download path
|
||||
agent-browser --download-path ./downloads open https://example.com
|
||||
|
||||
# Scroll within container
|
||||
agent-browser scroll down 500 --selector "div.content"
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Six new environment variables for security configuration: `AGENT_BROWSER_CONTENT_BOUNDARIES`, `AGENT_BROWSER_MAX_OUTPUT`, `AGENT_BROWSER_ALLOWED_DOMAINS`, `AGENT_BROWSER_ACTION_POLICY`, `AGENT_BROWSER_CONFIRM_ACTIONS`, `AGENT_BROWSER_CONFIRM_INTERACTIVE`.
|
||||
|
||||
---
|
||||
|
||||
## v0.14.0
|
||||
|
||||
<p className="text-[#888] text-sm">February 2026</p>
|
||||
|
||||
### New Features
|
||||
|
||||
- **`keyboard` command** -- Type with real keystrokes, insert text, and press shortcuts at the currently focused element without needing a selector (`keyboard type`, `keyboard inserttext`).
|
||||
- **`--color-scheme` flag** -- Persistent dark/light mode preference across browser sessions via flag or `AGENT_BROWSER_COLOR_SCHEME` env var.
|
||||
|
||||
```bash
|
||||
agent-browser keyboard type "Hello world"
|
||||
agent-browser keyboard inserttext "pasted text"
|
||||
agent-browser --color-scheme dark open https://example.com
|
||||
```
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed IPC EAGAIN errors (os error 35/11) with backpressure-aware socket writes, command serialization, and lowered 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.
|
||||
|
||||
---
|
||||
|
||||
## v0.13.0
|
||||
|
||||
<p className="text-[#888] text-sm">February 2026</p>
|
||||
|
||||
### New Features
|
||||
|
||||
- **Diff commands** -- Compare snapshots, screenshots, and URLs between page states. 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.
|
||||
|
||||
```bash
|
||||
agent-browser diff snapshot
|
||||
agent-browser diff screenshot --baseline before.png
|
||||
agent-browser diff url https://staging.example.com https://prod.example.com
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## v0.12.0
|
||||
|
||||
<p className="text-[#888] text-sm">February 2026</p>
|
||||
|
||||
### New Features
|
||||
|
||||
- **Annotated screenshots** -- `--annotate` flag overlays numbered labels on interactive elements and prints a legend mapping each label to its element ref. Enables multimodal AI models to reason about visual layout while using the same `@eN` refs for subsequent interactions. Also settable via `AGENT_BROWSER_ANNOTATE` env var.
|
||||
|
||||
```bash
|
||||
agent-browser screenshot --annotate
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## v0.11.1
|
||||
|
||||
<p className="text-[#888] text-sm">February 2026</p>
|
||||
|
||||
### Documentation
|
||||
|
||||
- Added documentation for command chaining with `&&` across README, CLI help output, docs, and skill files.
|
||||
|
||||
---
|
||||
|
||||
## v0.11.0
|
||||
|
||||
<p className="text-[#888] text-sm">February 2026</p>
|
||||
|
||||
### New Features
|
||||
|
||||
- **Configuration file support** -- Automatic loading from user (`~/.agent-browser/config.json`) and project (`./agent-browser.json`) directories with priority-based merging.
|
||||
- **Profiler commands** -- Chrome DevTools profiling with `profiler start` and `profiler stop`.
|
||||
- **Browser extension loading** -- `--extension` flag to load browser extensions.
|
||||
- **Storage state management** -- `state save` and `state load` commands for auth state persistence.
|
||||
- **iOS device emulation** -- `--device` flag for device emulation.
|
||||
- **Enhanced click** -- `--new-tab` option for click commands.
|
||||
- **Enhanced find** -- Additional actions and filtering options.
|
||||
- **CDP WebSocket URLs** -- `--cdp` now accepts WebSocket URLs in addition to ports.
|
||||
|
||||
---
|
||||
|
||||
## v0.10.0
|
||||
|
||||
<p className="text-[#888] text-sm">February 2026</p>
|
||||
|
||||
### New Features
|
||||
|
||||
- **Session persistence** - Automatic save/restore of cookies and localStorage across browser restarts using `--session-name` flag
|
||||
- **Encrypted state** - Optional AES-256-GCM encryption for saved session state data
|
||||
- **State management commands** - New commands for listing, showing, renaming, clearing, and cleaning up session state files
|
||||
- **New tab on click** - Added `--new-tab` option for click commands to open links in new tabs
|
||||
|
||||
```bash
|
||||
# Persist session state
|
||||
agent-browser --session-name myapp open https://example.com
|
||||
|
||||
# Manage saved states
|
||||
agent-browser state list
|
||||
agent-browser state show myapp
|
||||
agent-browser state clear myapp
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## v0.9.4
|
||||
|
||||
<p className="text-[#888] text-sm">February 2026</p>
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed all Clippy lint warnings in the Rust CLI
|
||||
|
||||
---
|
||||
|
||||
## v0.9.3
|
||||
|
||||
<p className="text-[#888] text-sm">February 2026</p>
|
||||
|
||||
### Improvements
|
||||
|
||||
- Added support for custom executable path in CLI browser launch options
|
||||
- Documentation site UI improvements including a new chat component with sheet-based interface
|
||||
|
||||
---
|
||||
|
||||
## v0.9.2
|
||||
|
||||
<p className="text-[#888] text-sm">February 2026</p>
|
||||
|
||||
### Improvements
|
||||
|
||||
- Migrated documentation site to MDX for improved content authoring
|
||||
- Added AI-powered docs chat feature
|
||||
- Updated README with Homebrew installation instructions for macOS users
|
||||
|
||||
---
|
||||
|
||||
## v0.9.1
|
||||
|
||||
<p className="text-[#888] text-sm">February 2026</p>
|
||||
|
||||
### New Features
|
||||
|
||||
- **`--allow-file-access` flag** - Enable opening and interacting with local `file://` URLs (PDFs, HTML files) by passing Chromium flags that allow JavaScript access to local files
|
||||
- **`-C`/`--cursor` flag for snapshots** - Include cursor-interactive elements like divs with onclick handlers or `cursor:pointer` styles
|
||||
|
||||
```bash
|
||||
agent-browser --allow-file-access open file:///path/to/document.pdf
|
||||
agent-browser snapshot -C
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## v0.9.0
|
||||
|
||||
<p className="text-[#888] text-sm">February 2026</p>
|
||||
|
||||
### New Features
|
||||
|
||||
- **iOS Simulator support** - Mobile Safari testing via Appium with real device and simulator support
|
||||
|
||||
```bash
|
||||
# List available iOS simulators
|
||||
agent-browser device list
|
||||
|
||||
# Launch on iOS device
|
||||
agent-browser -p ios --device "iPhone 16 Pro" open https://example.com
|
||||
|
||||
# Touch interactions
|
||||
agent-browser tap @e1
|
||||
agent-browser swipe up
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## v0.8.10
|
||||
|
||||
<p className="text-[#888] text-sm">January 2026</p>
|
||||
|
||||
### Improvements
|
||||
|
||||
- Added `--stdin` flag for eval command to read JavaScript from stdin, enabling heredoc usage for multiline scripts
|
||||
- Fixed binary permission issues on macOS/Linux when postinstall scripts don't run
|
||||
|
||||
---
|
||||
|
||||
## v0.8.9
|
||||
|
||||
<p className="text-[#888] text-sm">January 2026</p>
|
||||
|
||||
### Improvements
|
||||
|
||||
- Added `--stdin` flag for eval command to read JavaScript from stdin
|
||||
|
||||
---
|
||||
|
||||
## v0.8.8
|
||||
|
||||
<p className="text-[#888] text-sm">January 2026</p>
|
||||
|
||||
### Improvements
|
||||
|
||||
- Added base64 encoding support for the eval command with `-b`/`--base64` flag to avoid shell escaping issues
|
||||
- Updated documentation with AI agent setup instructions
|
||||
|
||||
---
|
||||
|
||||
## v0.8.7
|
||||
|
||||
<p className="text-[#888] text-sm">January 2026</p>
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed browser launch options not being passed correctly when using persistent profiles
|
||||
- Added pre-flight checks for socket path length limits and directory write permissions
|
||||
- Improved error handling to properly exit with failure status when browser launch fails
|
||||
|
||||
---
|
||||
|
||||
## v0.8.6
|
||||
|
||||
<p className="text-[#888] text-sm">January 2026</p>
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Improved daemon connection reliability with automatic retry logic for transient errors
|
||||
- CLI now cleans up stale socket and PID files before starting a new daemon
|
||||
|
||||
---
|
||||
|
||||
## v0.8.5
|
||||
|
||||
<p className="text-[#888] text-sm">January 2026</p>
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed version synchronization to automatically update Cargo.lock alongside Cargo.toml during releases
|
||||
- Made the CLI binary executable in the npm package
|
||||
|
||||
---
|
||||
|
||||
## v0.8.4
|
||||
|
||||
<p className="text-[#888] text-sm">January 2026</p>
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed "Daemon not found" error when running through AI agents by resolving symlinks in the executable path
|
||||
|
||||
---
|
||||
|
||||
## v0.8.3
|
||||
|
||||
<p className="text-[#888] text-sm">January 2026</p>
|
||||
|
||||
### Improvements
|
||||
|
||||
- Replaced shell-based CLI wrappers with a cross-platform Node.js wrapper to enable npx support on Windows
|
||||
- Added postinstall logic to patch npm bin entry on global installs for zero-overhead native binary invocation
|
||||
- Added CI tests to verify global installation across all platforms
|
||||
|
||||
---
|
||||
|
||||
## v0.8.2
|
||||
|
||||
<p className="text-[#888] text-sm">January 2026</p>
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed the Windows CMD wrapper to use the native binary directly instead of routing through Node.js
|
||||
- Added retry logic to CI install command for transient browser installation failures
|
||||
|
||||
---
|
||||
|
||||
## v0.8.1
|
||||
|
||||
<p className="text-[#888] text-sm">January 2026</p>
|
||||
|
||||
### Improvements
|
||||
|
||||
- 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
|
||||
|
||||
---
|
||||
|
||||
## v0.8.0
|
||||
|
||||
<p className="text-[#888] text-sm">January 2026</p>
|
||||
|
||||
### New Features
|
||||
|
||||
- **Kernel cloud browser provider** - Connect to Kernel (kernel.sh) for remote browser infrastructure with stealth mode and persistent profiles
|
||||
|
||||
```bash
|
||||
# Via -p flag
|
||||
agent-browser -p kernel open https://example.com
|
||||
|
||||
# Via environment variable
|
||||
export AGENT_BROWSER_PROVIDER=kernel
|
||||
export KERNEL_API_KEY=your-api-key
|
||||
agent-browser open https://example.com
|
||||
|
||||
# With persistent profile
|
||||
export KERNEL_PROFILE_NAME=my-profile
|
||||
agent-browser open https://example.com
|
||||
```
|
||||
|
||||
- **Ignore HTTPS certificate errors** - New flag for working with self-signed certificates and development environments
|
||||
|
||||
```bash
|
||||
agent-browser --ignore-https-errors open https://localhost:3000
|
||||
```
|
||||
|
||||
- **Enhanced cookie management** - Extended `cookies set` command with additional flags for setting cookies before page load
|
||||
|
||||
```bash
|
||||
agent-browser cookies set session_id "abc123" --url https://app.example.com --httpOnly --secure
|
||||
agent-browser cookies set token "xyz" --domain .example.com --path /api --expires 1735689600
|
||||
```
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed tab list command not recognizing new pages opened via clicks or `target="_blank"` links
|
||||
- Fixed `check` command hanging indefinitely
|
||||
- Fixed `set device` not applying deviceScaleFactor - HiDPI screenshots now work correctly
|
||||
- Fixed state load and profile persistence not working in v0.7.6
|
||||
- Screenshots now save to temp directory when no path is provided
|
||||
|
||||
### Security
|
||||
|
||||
- Daemon and stream server now reject cross-origin connections
|
||||
|
||||
---
|
||||
|
||||
## v0.7.1
|
||||
|
||||
<p className="text-[#888] text-sm">January 2026</p>
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **Fix native binary distribution** - 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.
|
||||
|
||||
---
|
||||
|
||||
## v0.7.0
|
||||
|
||||
<p className="text-[#888] text-sm">January 2026</p>
|
||||
|
||||
### New Features
|
||||
|
||||
- **Cloud browser providers** - Connect to Browserbase or Browser Use for remote browser infrastructure
|
||||
|
||||
```bash
|
||||
# Via -p flag (recommended)
|
||||
agent-browser -p browserbase open https://example.com
|
||||
agent-browser -p browseruse open https://example.com
|
||||
|
||||
# Via environment variable
|
||||
export AGENT_BROWSER_PROVIDER=browserbase
|
||||
agent-browser open https://example.com
|
||||
```
|
||||
|
||||
- **Persistent browser profiles** - Store cookies, localStorage, and login sessions across browser restarts
|
||||
|
||||
```bash
|
||||
agent-browser --profile ~/.myapp-profile open myapp.com
|
||||
# Login persists across restarts
|
||||
```
|
||||
|
||||
- **Remote CDP WebSocket URLs** - Connect to remote browser services via WebSocket
|
||||
|
||||
```bash
|
||||
agent-browser --cdp "wss://browser-service.com/cdp?token=..." snapshot
|
||||
```
|
||||
|
||||
- **`download` command** - Trigger downloads and wait for completion
|
||||
|
||||
```bash
|
||||
agent-browser download @e1 ./file.pdf
|
||||
agent-browser wait --download ./output.zip --timeout 30000
|
||||
```
|
||||
|
||||
- **Browser launch configuration** - Fine-grained control over browser startup
|
||||
|
||||
```bash
|
||||
agent-browser --args "--disable-gpu,--no-sandbox" open example.com
|
||||
agent-browser --user-agent "Custom UA" open example.com
|
||||
agent-browser --proxy-bypass "localhost,*.internal" open example.com
|
||||
```
|
||||
|
||||
- **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
|
||||
|
||||
---
|
||||
|
||||
## v0.6.0
|
||||
|
||||
<p className="text-[#888] text-sm">January 2026</p>
|
||||
|
||||
### New Features
|
||||
|
||||
- **Video recording** - Record browser sessions to WebM using Playwright's native recording
|
||||
|
||||
```bash
|
||||
agent-browser record start ./demo.webm
|
||||
agent-browser click @e1
|
||||
agent-browser record stop
|
||||
```
|
||||
|
||||
- **`connect` command** - Connect to a browser via CDP and persist the connection for subsequent commands
|
||||
|
||||
```bash
|
||||
agent-browser connect 9222
|
||||
agent-browser snapshot # No --cdp needed after connect
|
||||
```
|
||||
|
||||
- **`--proxy` flag** - Configure browser proxy with optional authentication
|
||||
|
||||
```bash
|
||||
agent-browser --proxy http://user:pass@proxy.com:8080 open example.com
|
||||
```
|
||||
|
||||
- **`get styles` command** - Extract computed styles from elements
|
||||
|
||||
```bash
|
||||
agent-browser get styles "button"
|
||||
```
|
||||
|
||||
- **Claude marketplace plugin** - Added `.claude-plugin/marketplace.json` for Claude Code integration
|
||||
- **Enhanced network output** - `network requests` now shows method, URL, and resource type
|
||||
- **`--version` flag** - Display CLI version
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix Windows daemon startup and port calculation
|
||||
- Support `libasound2t64` on newer Ubuntu versions (24.04+)
|
||||
- Prevent CDP timeout on empty URL tabs
|
||||
- Output screenshot as base64 when no path provided
|
||||
- Resolve refs in `get value` command
|
||||
- Support URL parameter in `tab new` command
|
||||
- Allow `about:`, `data:`, and `file:` URL schemes
|
||||
- Detect stale unix socket by attempting connection
|
||||
- Respect `AGENT_BROWSER_HEADED` environment variable
|
||||
- Handle SIGPIPE to prevent panic when piping to `head`/`tail`
|
||||
- Fix null path validation in screenshot command
|
||||
|
||||
### Protocol Alignment
|
||||
|
||||
These changes align the CLI with the daemon protocol for consistency:
|
||||
|
||||
- `select` command now uses `values` field (supports multiple selections)
|
||||
- `frame main` uses `mainframe` action
|
||||
- `mouse wheel` uses `wheel` action
|
||||
- `set media` uses `emulatemedia` action
|
||||
- Console output uses `messages` field
|
||||
|
||||
### Documentation
|
||||
|
||||
- Expanded SKILL.md with comprehensive command reference
|
||||
- Updated README with new commands and options
|
||||
- Updated CDP mode documentation with `connect` workflow
|
||||
@@ -0,0 +1,308 @@
|
||||
import { pageMetadata } from '@/lib/page-metadata';
|
||||
|
||||
export const metadata = pageMetadata('commands');
|
||||
|
||||
# Commands
|
||||
|
||||
## Core
|
||||
|
||||
```bash
|
||||
agent-browser open <url> # Navigate (aliases: goto, navigate)
|
||||
agent-browser --risk-mode block open <url> # Block when verification/captcha interstitial is detected
|
||||
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> [--delay <ms>] # Type into element
|
||||
agent-browser press <key> # Press key (Enter, Tab, Control+a) (alias: key)
|
||||
agent-browser keyboard type <text> [--delay <ms>] # 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 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 --version # Show CLI version
|
||||
agent-browser close # Close browser (aliases: quit, exit)
|
||||
```
|
||||
|
||||
Fork builds print dual-version metadata with `--version`:
|
||||
|
||||
```bash
|
||||
agent-browser 0.14.0-fork.1 (upstream 0.14.0, fork 1)
|
||||
```
|
||||
|
||||
## 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 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 2000-5000 # Random wait between 2-5 seconds
|
||||
agent-browser wait --text "Welcome" # Wait for text
|
||||
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
|
||||
```
|
||||
|
||||
## Risk Mode
|
||||
|
||||
Control how `open`/`navigate` handles verification or captcha interstitials:
|
||||
|
||||
```bash
|
||||
agent-browser --risk-mode warn open https://example.com # default: retry and warn with riskSignals
|
||||
agent-browser --risk-mode block open https://example.com # fail fast on detection
|
||||
agent-browser --risk-mode off open https://example.com # disable detection/retry
|
||||
```
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
## Settings
|
||||
|
||||
```bash
|
||||
agent-browser set viewport <w> <h> # Set viewport size
|
||||
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
|
||||
```
|
||||
|
||||
For `cookies set`, use one of these patterns:
|
||||
|
||||
- `--url <url>`
|
||||
- `--domain <domain> --path <path>`
|
||||
- omit all three to scope from the current page URL
|
||||
|
||||
When `--url` is omitted, `--domain` and `--path` must be provided together.
|
||||
|
||||
## 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 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
|
||||
```
|
||||
|
||||
## Tabs & frames
|
||||
|
||||
```bash
|
||||
agent-browser tab # List tabs
|
||||
agent-browser tab new [url] # New tab
|
||||
agent-browser tab <n> # Switch to tab
|
||||
agent-browser tab close [n] # Close tab
|
||||
agent-browser window new # Open new browser window
|
||||
agent-browser frame <sel> # Switch to iframe
|
||||
agent-browser frame main # Back to main frame
|
||||
```
|
||||
|
||||
## Dialogs
|
||||
|
||||
```bash
|
||||
agent-browser dialog accept [text] # Accept dialog (with optional prompt text)
|
||||
agent-browser dialog dismiss # Dismiss dialog
|
||||
```
|
||||
|
||||
## 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 --clear # Clear console log
|
||||
agent-browser errors # View page errors
|
||||
agent-browser errors --clear # Clear error log
|
||||
agent-browser highlight <sel> # Highlight element
|
||||
```
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
## Navigation
|
||||
|
||||
```bash
|
||||
agent-browser back # Go back
|
||||
agent-browser forward # Go forward
|
||||
agent-browser reload # Reload page
|
||||
```
|
||||
|
||||
## Global options
|
||||
|
||||
```bash
|
||||
--session <name> # Isolated browser session
|
||||
--session-name <name> # Auto-save/restore session state (cookies, localStorage)
|
||||
--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)
|
||||
--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)
|
||||
--stealth # Stealth mode (always on by default)
|
||||
-p, --provider <name> # Browser provider (ios, browserbase, kernel, browseruse)
|
||||
--device <name> # iOS device name (e.g., "iPhone 15 Pro")
|
||||
--json # JSON output (for scripts)
|
||||
--full, -f # Full page screenshot
|
||||
--annotate # Annotated screenshot with numbered element labels
|
||||
--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
|
||||
--debug # Debug output (includes stealth connection type + capabilities)
|
||||
```
|
||||
|
||||
## 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.
|
||||
@@ -0,0 +1,499 @@
|
||||
import { pageMetadata } from '@/lib/page-metadata';
|
||||
|
||||
export const metadata = pageMetadata('configuration');
|
||||
|
||||
# Configuration
|
||||
|
||||
Create an `agent-browser.json` file to set persistent defaults instead of repeating flags on every command.
|
||||
|
||||
In this fork, default launch behavior auto-attaches to an existing browser by trying `localhost:9333` (CDP) first, then auto-discovery. If both fail, commands exit instead of launching a managed browser.
|
||||
|
||||
## 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",
|
||||
"userAgent": "my-agent/1.0",
|
||||
"ignoreHttpsErrors": 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>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>riskMode</code>
|
||||
</td>
|
||||
<td>
|
||||
<code>--risk-mode</code>
|
||||
</td>
|
||||
<td>
|
||||
string (<code>off</code>, <code>warn</code>, <code>block</code>)
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>headers</code>
|
||||
</td>
|
||||
<td>
|
||||
<code>--headers</code>
|
||||
</td>
|
||||
<td>string (JSON)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
`riskMode` defaults to `warn` when unset.
|
||||
|
||||
## Common Configurations
|
||||
|
||||
### Local Development
|
||||
|
||||
```json
|
||||
{
|
||||
"headed": true,
|
||||
"sessionName": "local-dev"
|
||||
}
|
||||
```
|
||||
|
||||
### 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"
|
||||
}
|
||||
```
|
||||
|
||||
## 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`.
|
||||
|
||||
## 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_RISK_MODE</code>
|
||||
</td>
|
||||
<td>
|
||||
Verification/captcha handling mode (<code>off</code>, <code>warn</code>, <code>block</code>
|
||||
).
|
||||
</td>
|
||||
<td>
|
||||
<code>warn</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>AGENT_BROWSER_DEFAULT_TIMEOUT</code>
|
||||
</td>
|
||||
<td>Default Playwright 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_STREAM_PORT</code>
|
||||
</td>
|
||||
<td>
|
||||
Enable WebSocket streaming on the specified port (e.g., <code>9223</code>).
|
||||
</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>
|
||||
</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`.
|
||||
@@ -0,0 +1,179 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
|
||||
export const metadata = pageMetadata("diffing")
|
||||
|
||||
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
|
||||
```
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,333 @@
|
||||
@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);
|
||||
}
|
||||
|
||||
:root {
|
||||
--background: #fff;
|
||||
--foreground: #171717;
|
||||
--border: #e5e5e5;
|
||||
--muted: #f5f5f5;
|
||||
--muted-foreground: #737373;
|
||||
--primary: #171717;
|
||||
--primary-foreground: #fff;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #f5f5f5;
|
||||
--border: #262626;
|
||||
--muted: #262626;
|
||||
--muted-foreground: #a3a3a3;
|
||||
--primary: #f5f5f5;
|
||||
--primary-foreground: #0a0a0a;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
::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;
|
||||
}
|
||||
|
||||
/* 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 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;
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
|
||||
export const metadata = pageMetadata("installation")
|
||||
|
||||
# Installation
|
||||
|
||||
## Global installation (recommended)
|
||||
|
||||
Installs the native Rust binary for maximum performance:
|
||||
|
||||
```bash
|
||||
npm install -g agent-browser-stealth
|
||||
agent-browser install # Download Chromium
|
||||
```
|
||||
|
||||
This is the fastest option -- commands run through the native Rust CLI directly with sub-millisecond parsing overhead.
|
||||
|
||||
## Quick start (no install)
|
||||
|
||||
Run directly with `npx` if you want to try it without installing globally:
|
||||
|
||||
```bash
|
||||
npx agent-browser-stealth install # Download Chromium (first time only)
|
||||
npx agent-browser-stealth open example.com
|
||||
```
|
||||
|
||||
> **Note:** `npx` routes through Node.js before reaching the Rust CLI, so it is noticeably slower than a global install. For regular use, install globally.
|
||||
|
||||
## Project installation (local dependency)
|
||||
|
||||
For projects that want to pin the version in `package.json`:
|
||||
|
||||
```bash
|
||||
npm install agent-browser-stealth
|
||||
npx agent-browser-stealth install
|
||||
```
|
||||
|
||||
Then use via `npx` or `package.json` scripts:
|
||||
|
||||
```bash
|
||||
npx agent-browser-stealth open example.com
|
||||
```
|
||||
|
||||
## Homebrew (macOS)
|
||||
|
||||
```bash
|
||||
brew install agent-browser
|
||||
agent-browser install # Download Chromium
|
||||
```
|
||||
|
||||
## From source
|
||||
|
||||
```bash
|
||||
git clone https://github.com/leeguooooo/agent-browser
|
||||
cd agent-browser
|
||||
pnpm install
|
||||
pnpm build
|
||||
pnpm build:native
|
||||
./bin/agent-browser install
|
||||
pnpm link --global
|
||||
```
|
||||
|
||||
## Fork versioning
|
||||
|
||||
Fork releases use a dual-version format:
|
||||
|
||||
- `<upstream>-fork.<fork>`
|
||||
- Example: `0.14.0-fork.1`
|
||||
|
||||
`agent-browser --version` prints the full version and also shows upstream and fork parts for fork builds.
|
||||
|
||||
## Linux dependencies
|
||||
|
||||
On Linux, install system dependencies:
|
||||
|
||||
```bash
|
||||
agent-browser install --with-deps
|
||||
# or manually: npx playwright install-deps chromium
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
```typescript
|
||||
import chromium from '@sparticuz/chromium';
|
||||
import { BrowserManager } from 'agent-browser-stealth';
|
||||
|
||||
export async function handler() {
|
||||
const browser = new BrowserManager();
|
||||
await browser.launch({
|
||||
executablePath: await chromium.executablePath(),
|
||||
headless: true,
|
||||
});
|
||||
// ... use browser
|
||||
}
|
||||
```
|
||||
|
||||
## 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 leeguooooo/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
|
||||
```
|
||||
@@ -0,0 +1,211 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
|
||||
export const metadata = pageMetadata("ios")
|
||||
|
||||
# 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>Chromium/Firefox/WebKit</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.
|
||||
@@ -0,0 +1,93 @@
|
||||
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 | Headless Browser Automation for AI",
|
||||
template: "%s | agent-browser",
|
||||
},
|
||||
description: "Headless browser automation CLI for AI agents",
|
||||
openGraph: {
|
||||
type: "website",
|
||||
locale: "en_US",
|
||||
url: "https://agent-browser.dev",
|
||||
siteName: "agent-browser",
|
||||
title: "agent-browser | Headless Browser Automation for AI",
|
||||
description: "Headless browser automation CLI for AI agents",
|
||||
images: [{ url: "/og", width: 1200, height: 630, alt: "agent-browser" }],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: "agent-browser | Headless Browser Automation for AI",
|
||||
description: "Headless 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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
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,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { getPageTitle, renderOgImage } from "./og-image";
|
||||
|
||||
export async function GET() {
|
||||
const title = getPageTitle("")!;
|
||||
return renderOgImage(title);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
|
||||
export const metadata = pageMetadata("")
|
||||
|
||||
# agent-browser
|
||||
|
||||
Browser automation CLI designed for AI agents. Compact text output minimizes context usage. Fast Rust CLI with Node.js fallback.
|
||||
|
||||
```bash
|
||||
npm install -g agent-browser-stealth # all platforms (fastest, native Rust CLI)
|
||||
brew install agent-browser # macOS
|
||||
|
||||
# or try without installing
|
||||
npx agent-browser-stealth 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
|
||||
- **Auto region detection** - Locale, timezone, and Accept-Language automatically match the target site's TLD
|
||||
- **Captcha auto-retry** - Detects captcha/verification pages and retries with randomized backoff
|
||||
|
||||
## 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. **Node.js Daemon** - Manages Playwright browser instance
|
||||
|
||||
Daemon starts automatically and persists between commands.
|
||||
|
||||
## Platforms
|
||||
|
||||
Native Rust binaries for macOS (ARM64, x64), Linux (ARM64, x64), and Windows (x64).
|
||||
@@ -0,0 +1,114 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
|
||||
export const metadata = pageMetadata("profiler")
|
||||
|
||||
# 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.
|
||||
@@ -0,0 +1,94 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
|
||||
export const metadata = pageMetadata("quick-start")
|
||||
|
||||
# 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.
|
||||
@@ -0,0 +1,243 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
export const metadata = pageMetadata("security")
|
||||
|
||||
# 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
|
||||
```
|
||||
|
||||
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</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"
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,58 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
|
||||
export const metadata = pageMetadata("selectors")
|
||||
|
||||
# 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
|
||||
```
|
||||
@@ -0,0 +1,173 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
|
||||
export const metadata = pageMetadata("sessions")
|
||||
|
||||
# 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
|
||||
|
||||
## 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>
|
||||
@@ -0,0 +1,60 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
|
||||
export const metadata = pageMetadata("skills")
|
||||
|
||||
# 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.
|
||||
|
||||
## Available Skills
|
||||
|
||||
- **agent-browser** — General browser automation: navigation, snapshots, forms, screenshots, data extraction, sessions, authentication, diffing, and the full command reference.
|
||||
- **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. This is how agent-browser drives native desktop apps like the Slack macOS app.
|
||||
- **slack** — Browser-based Slack automation. Check unreads, navigate channels, search conversations, send messages, and extract data — no API tokens needed.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npx skills add vercel-labs/agent-browser --skill agent-browser
|
||||
npx skills add vercel-labs/agent-browser --skill dogfood
|
||||
npx skills add vercel-labs/agent-browser --skill electron
|
||||
npx skills add vercel-labs/agent-browser --skill slack
|
||||
```
|
||||
|
||||
After installing, your AI agent will automatically activate the right skill when it encounters a matching request.
|
||||
|
||||
## agent-browser
|
||||
|
||||
The core skill. Teaches agents the full agent-browser API: the navigate-snapshot-interact-re-snapshot workflow, all commands, command chaining, authentication (auth vault and state persistence), sessions, diffing, JavaScript evaluation, annotated screenshots, semantic locators, and configuration.
|
||||
|
||||
Example agent interactions:
|
||||
|
||||
- "Open example.com and fill out the contact form"
|
||||
- "Take a screenshot of the dashboard after logging in"
|
||||
- "Compare staging and production versions of the homepage"
|
||||
|
||||
## dogfood
|
||||
|
||||
A structured workflow for exploratory testing. The agent opens a target URL, systematically explores the app (navigating pages, testing forms, clicking buttons, checking console errors), and documents every issue it finds with:
|
||||
|
||||
- Numbered repro steps
|
||||
- Step-by-step screenshots
|
||||
- Repro videos for interactive bugs
|
||||
- Severity classification
|
||||
|
||||
The output is a markdown report in an output directory, ready to hand to the responsible team. Run it with a single prompt like "dogfood vercel.com" or "QA http://localhost:3000 — focus on the billing page".
|
||||
|
||||
## electron
|
||||
|
||||
Electron apps (VS Code, Slack, Discord, Figma, Notion, Spotify, etc.) are built on Chromium and expose a Chrome DevTools Protocol (CDP) port that agent-browser can connect to. This skill teaches agents how to launch or connect to any Electron app, then use the standard snapshot-interact workflow to automate it.
|
||||
|
||||
Electron apps are built on Chromium, so they expose a Chrome DevTools Protocol (CDP) port that agent-browser can connect to. Launch the app with `--remote-debugging-port`, connect, and use the standard snapshot-interact workflow. This is the foundation that the **slack** skill builds on.
|
||||
|
||||
## slack
|
||||
|
||||
Browser-based Slack automation. Connects to an existing Slack session (via `agent-browser connect 9222`) or opens Slack in a new browser, then uses snapshots and element refs to navigate the UI. Covers checking unreads, navigating channels and DMs, searching conversations, extracting message data, and taking screenshots — all without needing Slack API tokens or bot setup.
|
||||
|
||||
## Source
|
||||
|
||||
All skill files are in the [`skills/`](https://github.com/vercel-labs/agent-browser/tree/main/skills) directory of the repository.
|
||||
@@ -0,0 +1,120 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
|
||||
export const metadata = pageMetadata("snapshots")
|
||||
|
||||
# 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 -i -C # Include cursor-interactive elements
|
||||
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>-C, --cursor</code></td><td>Include cursor-interactive elements (cursor:pointer, onclick, tabindex)</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>
|
||||
|
||||
## Cursor-interactive elements
|
||||
|
||||
Many modern web apps use custom clickable elements (divs, spans) instead of standard buttons or links.
|
||||
The `-C` flag detects these by looking for:
|
||||
|
||||
- `cursor: pointer` CSS style
|
||||
- `onclick` attribute or handler
|
||||
- `tabindex` attribute (keyboard focusable)
|
||||
|
||||
```bash
|
||||
agent-browser snapshot -i -C
|
||||
# Output includes:
|
||||
# @e1 [button] "Submit"
|
||||
# @e2 [link] "Learn more"
|
||||
# Cursor-interactive elements:
|
||||
# @e3 [clickable] "Menu Item" [cursor:pointer, onclick]
|
||||
# @e4 [clickable] "Card" [cursor:pointer]
|
||||
```
|
||||
|
||||
## 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`:
|
||||
|
||||
```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.
|
||||
|
||||
## 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.
|
||||
@@ -0,0 +1,232 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
|
||||
export const metadata = pageMetadata("streaming")
|
||||
|
||||
# Streaming
|
||||
|
||||
Stream the browser viewport via WebSocket for live preview or "pair browsing"
|
||||
where a human can watch and interact alongside an AI agent.
|
||||
|
||||
## Enable streaming
|
||||
|
||||
Set the `AGENT_BROWSER_STREAM_PORT` environment variable to start
|
||||
a WebSocket server:
|
||||
|
||||
```bash
|
||||
AGENT_BROWSER_STREAM_PORT=9223 agent-browser open example.com
|
||||
```
|
||||
|
||||
The server streams viewport frames and accepts input events (mouse, keyboard, touch).
|
||||
|
||||
## 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-stealth';
|
||||
|
||||
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
|
||||
@@ -0,0 +1,25 @@
|
||||
import { codeToHtml } from "shiki";
|
||||
import { CopyButton } from "./copy-button";
|
||||
|
||||
interface CodeBlockProps {
|
||||
code: string;
|
||||
lang?: string;
|
||||
}
|
||||
|
||||
export async function CodeBlock({ code, lang = "bash" }: CodeBlockProps) {
|
||||
const trimmedCode = code.trim();
|
||||
const html = await codeToHtml(trimmedCode, {
|
||||
lang,
|
||||
themes: {
|
||||
light: "github-light-default",
|
||||
dark: "github-dark-default",
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="code-block relative group">
|
||||
<CopyButton code={trimmedCode} />
|
||||
<div dangerouslySetInnerHTML={{ __html: html }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,538 @@
|
||||
"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 === "k" && (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 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 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>K
|
||||
</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 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! p-0 flex flex-col"
|
||||
style={{ backgroundColor: "var(--background)", opacity: 1 }}
|
||||
>
|
||||
<SheetTitle className="sr-only">AI Chat</SheetTitle>
|
||||
{chatPanel}
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { ThemeToggle } from "./theme-toggle";
|
||||
|
||||
export function Header() {
|
||||
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">
|
||||
<a
|
||||
href="https://github.com/leeguooooo/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>
|
||||
<span>16k</span>
|
||||
</a>
|
||||
<a
|
||||
href="https://www.npmjs.com/package/agent-browser-stealth"
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
"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 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,
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
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: "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" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: null,
|
||||
items: [{ name: "Changelog", href: "/changelog" }],
|
||||
},
|
||||
];
|
||||
|
||||
export const allDocsPages: NavItem[] = navigation.flatMap(
|
||||
(section) => section.items
|
||||
);
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { Metadata } from "next";
|
||||
import { PAGE_TITLES } from "./page-titles";
|
||||
|
||||
const DESCRIPTION =
|
||||
"Headless 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],
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
export const PAGE_TITLES: Record<string, string> = {
|
||||
"": "Headless Browser\nAutomation for AI",
|
||||
installation: "Installation",
|
||||
"quick-start": "Quick Start",
|
||||
skills: "Skills",
|
||||
commands: "Commands",
|
||||
configuration: "Configuration",
|
||||
selectors: "Selectors",
|
||||
snapshots: "Snapshots",
|
||||
sessions: "Sessions",
|
||||
diffing: "Diffing",
|
||||
"cdp-mode": "CDP Mode",
|
||||
streaming: "Streaming",
|
||||
profiler: "Profiler",
|
||||
ios: "iOS Simulator",
|
||||
security: "Security",
|
||||
changelog: "Changelog",
|
||||
};
|
||||
|
||||
export function getPageTitle(slug: string): string | null {
|
||||
return slug in PAGE_TITLES ? PAGE_TITLES[slug]! : null;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Ratelimit } from "@upstash/ratelimit";
|
||||
import { Redis } from "@upstash/redis";
|
||||
|
||||
// Lazy initialization to avoid errors when Redis env vars are not configured
|
||||
let _minuteRateLimit: Ratelimit | null = null;
|
||||
let _dailyRateLimit: Ratelimit | null = null;
|
||||
|
||||
function getRedis(): Redis | null {
|
||||
const url = process.env.KV_REST_API_URL;
|
||||
const token = process.env.KV_REST_API_TOKEN;
|
||||
|
||||
if (!url || !token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Redis({ url, token });
|
||||
}
|
||||
|
||||
// No-op rate limiter for when Redis is not configured
|
||||
const noopRateLimiter = {
|
||||
limit: async () => ({ success: true, limit: 0, remaining: 0, reset: 0 }),
|
||||
};
|
||||
|
||||
const MINUTE_LIMIT = Number(process.env.RATE_LIMIT_PER_MINUTE) || 10;
|
||||
const DAILY_LIMIT = Number(process.env.RATE_LIMIT_PER_DAY) || 100;
|
||||
|
||||
// Requests per minute (sliding window)
|
||||
export const minuteRateLimit = {
|
||||
limit: async (identifier: string) => {
|
||||
if (!_minuteRateLimit) {
|
||||
const redis = getRedis();
|
||||
if (!redis) return noopRateLimiter.limit();
|
||||
_minuteRateLimit = new Ratelimit({
|
||||
redis,
|
||||
limiter: Ratelimit.slidingWindow(MINUTE_LIMIT, "1 m"),
|
||||
prefix: "ratelimit:minute",
|
||||
});
|
||||
}
|
||||
return _minuteRateLimit.limit(identifier);
|
||||
},
|
||||
};
|
||||
|
||||
// Requests per day (fixed window)
|
||||
export const dailyRateLimit = {
|
||||
limit: async (identifier: string) => {
|
||||
if (!_dailyRateLimit) {
|
||||
const redis = getRedis();
|
||||
if (!redis) return noopRateLimiter.limit();
|
||||
_dailyRateLimit = new Ratelimit({
|
||||
redis,
|
||||
limiter: Ratelimit.fixedWindow(DAILY_LIMIT, "1 d"),
|
||||
prefix: "ratelimit:daily",
|
||||
});
|
||||
}
|
||||
return _dailyRateLimit.limit(identifier);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts",
|
||||
"**/*.mts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
+26
-8
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "agent-browser",
|
||||
"version": "0.4.2",
|
||||
"description": "Headless browser automation CLI for AI agents",
|
||||
"name": "agent-browser-stealth",
|
||||
"version": "0.15.2-fork.0",
|
||||
"description": "Stealth browser automation CLI for AI agents with anti-bot evasions",
|
||||
"type": "module",
|
||||
"main": "dist/daemon.js",
|
||||
"files": [
|
||||
@@ -11,7 +11,8 @@
|
||||
"skills"
|
||||
],
|
||||
"bin": {
|
||||
"agent-browser": "./bin/agent-browser"
|
||||
"agent-browser-stealth": "./bin/agent-browser.js",
|
||||
"agent-browser": "./bin/agent-browser.js"
|
||||
},
|
||||
"scripts": {
|
||||
"prepare": "husky",
|
||||
@@ -32,12 +33,23 @@
|
||||
"format:check": "prettier --check 'src/**/*.ts'",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"postinstall": "node scripts/postinstall.js"
|
||||
"test:e2e:dogfood": "vitest run test/e2e/dogfood.eval.ts",
|
||||
"postinstall": "node scripts/postinstall.js",
|
||||
"verify:native-version": "node scripts/verify-native-version.js",
|
||||
"clawhub:sync": "bash scripts/clawhub-sync.sh",
|
||||
"sync:upstream": "bash scripts/sync-upstream.sh",
|
||||
"sync:upstream:push": "bash scripts/sync-upstream.sh --push",
|
||||
"changeset": "changeset",
|
||||
"ci:version": "changeset version && pnpm run version:sync && pnpm install --no-frozen-lockfile",
|
||||
"ci:publish": "pnpm run version:sync && pnpm run build && pnpm run build:native && pnpm run verify:native-version && changeset publish"
|
||||
},
|
||||
"keywords": [
|
||||
"browser",
|
||||
"automation",
|
||||
"headless",
|
||||
"stealth",
|
||||
"anti-bot",
|
||||
"anti-detection",
|
||||
"playwright",
|
||||
"cli",
|
||||
"agent"
|
||||
@@ -45,18 +57,24 @@
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/vercel-labs/agent-browser.git"
|
||||
"url": "git+https://github.com/leeguooooo/agent-browser.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/vercel-labs/agent-browser/issues"
|
||||
"url": "https://github.com/leeguooooo/agent-browser/issues"
|
||||
},
|
||||
"homepage": "https://github.com/vercel-labs/agent-browser#readme",
|
||||
"homepage": "https://github.com/leeguooooo/agent-browser#readme",
|
||||
"dependencies": {
|
||||
"node-simctl": "^7.4.0",
|
||||
"playwright-core": "^1.57.0",
|
||||
"webdriverio": "^9.15.0",
|
||||
"ws": "^8.19.0",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.2.52",
|
||||
"@changesets/cli": "^2.29.8",
|
||||
"@types/node": "^20.10.0",
|
||||
"@types/ws": "^8.18.1",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^15.2.11",
|
||||
"playwright": "^1.57.0",
|
||||
|
||||
Generated
+2639
-7
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* End-to-end check for CreepJS headless/stealth indicators.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/check-creepjs-headless.js
|
||||
* node scripts/check-creepjs-headless.js --compare-stealth
|
||||
* node scripts/check-creepjs-headless.js --binary ./cli/target/release/agent-browser
|
||||
*/
|
||||
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const rootDir = join(__dirname, '..');
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const getArgValue = (name, fallback) => {
|
||||
const index = args.indexOf(name);
|
||||
if (index === -1 || index + 1 >= args.length) return fallback;
|
||||
return args[index + 1];
|
||||
};
|
||||
|
||||
const binary = getArgValue('--binary', join(rootDir, 'cli', 'target', 'release', 'agent-browser'));
|
||||
const sessionPrefix = getArgValue('--session-prefix', 'creepjs-e2e');
|
||||
const compareStealth = args.includes('--compare-stealth');
|
||||
const targetUrl = getArgValue('--url', 'https://abrahamjuliot.github.io/creepjs/');
|
||||
|
||||
const extractionScript = `(() => {
|
||||
const headless = globalThis.Fingerprint?.headless ?? null;
|
||||
const toNumber = (value) => (typeof value === 'number' ? value : null);
|
||||
return {
|
||||
found: !!headless,
|
||||
metrics: headless ? {
|
||||
chromium: !!headless.chromium,
|
||||
likeHeadless: toNumber(headless.likeHeadlessRating),
|
||||
headless: toNumber(headless.headlessRating),
|
||||
stealth: toNumber(headless.stealthRating),
|
||||
raw: headless,
|
||||
} : null,
|
||||
navigator: {
|
||||
userAgent: navigator.userAgent,
|
||||
userAgentData: navigator.userAgentData ? navigator.userAgentData.toJSON?.() ?? null : null,
|
||||
language: navigator.language,
|
||||
languages: navigator.languages,
|
||||
platform: navigator.platform,
|
||||
webdriver: navigator.webdriver,
|
||||
webdriverInNavigator: ('webdriver' in navigator),
|
||||
},
|
||||
window: {
|
||||
innerWidth: window.innerWidth,
|
||||
innerHeight: window.innerHeight,
|
||||
outerWidth: window.outerWidth,
|
||||
outerHeight: window.outerHeight,
|
||||
screenX: window.screenX,
|
||||
screenY: window.screenY,
|
||||
},
|
||||
intl: {
|
||||
locale: Intl.DateTimeFormat().resolvedOptions().locale,
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
},
|
||||
};
|
||||
})()`;
|
||||
|
||||
function runCommand(commandArgs, options = {}) {
|
||||
const result = spawnSync(binary, commandArgs, { encoding: 'utf8' });
|
||||
if (result.status !== 0 && !options.allowFailure) {
|
||||
const stderr = (result.stderr || '').trim();
|
||||
const stdout = (result.stdout || '').trim();
|
||||
throw new Error(
|
||||
`Command failed: ${binary} ${commandArgs.join(' ')}\n` +
|
||||
`${stderr || stdout || `exit code ${result.status}`}`
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function withSessionArgs(session, stealth) {
|
||||
const base = ['--session', session];
|
||||
if (stealth === false) {
|
||||
base.push('--stealth', 'false');
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
function runSingleCheck({ stealth, runId }) {
|
||||
const session = `${sessionPrefix}-${runId}-${stealth ? 'stealth-on' : 'stealth-off'}`;
|
||||
|
||||
runCommand([...withSessionArgs(session, stealth), 'close'], { allowFailure: true });
|
||||
|
||||
try {
|
||||
runCommand([...withSessionArgs(session, stealth), 'open', targetUrl]);
|
||||
runCommand([
|
||||
...withSessionArgs(session, stealth),
|
||||
'wait',
|
||||
'--fn',
|
||||
'!!(window.Fingerprint && window.Fingerprint.headless)',
|
||||
]);
|
||||
runCommand([...withSessionArgs(session, stealth), 'wait', '2000']);
|
||||
|
||||
const evalResult = runCommand([
|
||||
...withSessionArgs(session, stealth),
|
||||
'eval',
|
||||
'--json',
|
||||
extractionScript,
|
||||
]);
|
||||
|
||||
const payload = JSON.parse(evalResult.stdout);
|
||||
return {
|
||||
session,
|
||||
stealth,
|
||||
url: targetUrl,
|
||||
extracted: payload?.data?.result ?? null,
|
||||
};
|
||||
} finally {
|
||||
runCommand([...withSessionArgs(session, stealth), 'close'], { allowFailure: true });
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
const runId = Date.now();
|
||||
const checks = compareStealth ? [true, false] : [true];
|
||||
const results = checks.map((stealth) => runSingleCheck({ stealth, runId }));
|
||||
|
||||
const output = {
|
||||
binary,
|
||||
compareStealth,
|
||||
timestamp: new Date().toISOString(),
|
||||
results,
|
||||
};
|
||||
|
||||
console.log(JSON.stringify(output, null, 2));
|
||||
}
|
||||
|
||||
main();
|
||||
Executable
+112
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* End-to-end check for bot.sannysoft.com WebDriver (New) result.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/check-sannysoft-webdriver.js
|
||||
* node scripts/check-sannysoft-webdriver.js --compare-stealth
|
||||
* node scripts/check-sannysoft-webdriver.js --binary ./cli/target/release/agent-browser
|
||||
*/
|
||||
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const rootDir = join(__dirname, '..');
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const getArgValue = (name, fallback) => {
|
||||
const index = args.indexOf(name);
|
||||
if (index === -1 || index + 1 >= args.length) return fallback;
|
||||
return args[index + 1];
|
||||
};
|
||||
|
||||
const binary = getArgValue('--binary', join(rootDir, 'cli', 'target', 'release', 'agent-browser'));
|
||||
const sessionPrefix = getArgValue('--session-prefix', 'botcheck-e2e');
|
||||
const compareStealth = args.includes('--compare-stealth');
|
||||
const targetUrl = getArgValue('--url', 'https://bot.sannysoft.com');
|
||||
|
||||
const extractionScript = `(() => {
|
||||
const normalize = (s) => (s || '').replace(/\\s+/g, ' ').trim();
|
||||
const rows = Array.from(document.querySelectorAll('tr'));
|
||||
const exact = rows.find((tr) => normalize(tr.cells?.[0]?.textContent).toLowerCase() === 'webdriver (new)');
|
||||
const fallback = exact || rows.find((tr) => normalize(tr.cells?.[0]?.textContent).toLowerCase().includes('webdriver'));
|
||||
return {
|
||||
found: !!fallback,
|
||||
label: fallback ? normalize(fallback.cells?.[0]?.textContent) : null,
|
||||
valueText: fallback ? normalize(fallback.cells?.[1]?.textContent) : null,
|
||||
statusText: fallback ? normalize(fallback.textContent) : null,
|
||||
navigatorWebdriver: navigator.webdriver,
|
||||
webdriverInNavigator: ('webdriver' in navigator),
|
||||
};
|
||||
})()`;
|
||||
|
||||
function runCommand(commandArgs, options = {}) {
|
||||
const result = spawnSync(binary, commandArgs, { encoding: 'utf8' });
|
||||
if (result.status !== 0 && !options.allowFailure) {
|
||||
const stderr = (result.stderr || '').trim();
|
||||
const stdout = (result.stdout || '').trim();
|
||||
throw new Error(
|
||||
`Command failed: ${binary} ${commandArgs.join(' ')}\n` +
|
||||
`${stderr || stdout || `exit code ${result.status}`}`
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function withSessionArgs(session, stealth) {
|
||||
const base = ['--session', session];
|
||||
if (stealth === false) {
|
||||
base.push('--stealth', 'false');
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
function runSingleCheck({ stealth, runId }) {
|
||||
const session = `${sessionPrefix}-${runId}-${stealth ? 'stealth-on' : 'stealth-off'}`;
|
||||
|
||||
// Best-effort cleanup in case previous run left state behind.
|
||||
runCommand([...withSessionArgs(session, stealth), 'close'], { allowFailure: true });
|
||||
|
||||
try {
|
||||
runCommand([...withSessionArgs(session, stealth), 'open', targetUrl]);
|
||||
runCommand([...withSessionArgs(session, stealth), 'wait', '--load', 'networkidle']);
|
||||
runCommand([...withSessionArgs(session, stealth), 'wait', '5000']);
|
||||
|
||||
const evalResult = runCommand([
|
||||
...withSessionArgs(session, stealth),
|
||||
'eval',
|
||||
'--json',
|
||||
extractionScript,
|
||||
]);
|
||||
|
||||
const payload = JSON.parse(evalResult.stdout);
|
||||
return {
|
||||
session,
|
||||
stealth,
|
||||
url: targetUrl,
|
||||
extracted: payload?.data?.result ?? null,
|
||||
};
|
||||
} finally {
|
||||
runCommand([...withSessionArgs(session, stealth), 'close'], { allowFailure: true });
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
const runId = Date.now();
|
||||
const checks = compareStealth ? [true, false] : [true];
|
||||
const results = checks.map((stealth) => runSingleCheck({ stealth, runId }));
|
||||
|
||||
const output = {
|
||||
binary,
|
||||
compareStealth,
|
||||
timestamp: new Date().toISOString(),
|
||||
results,
|
||||
};
|
||||
|
||||
console.log(JSON.stringify(output, null, 2));
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Verifies that package.json and cli/Cargo.toml have the same version.
|
||||
* Used in CI to catch version drift.
|
||||
*/
|
||||
|
||||
import { readFileSync } from 'fs';
|
||||
import { dirname, join } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const rootDir = join(__dirname, '..');
|
||||
|
||||
// Read package.json version
|
||||
const packageJson = JSON.parse(readFileSync(join(rootDir, 'package.json'), 'utf-8'));
|
||||
const packageVersion = packageJson.version;
|
||||
|
||||
// Read Cargo.toml version
|
||||
const cargoToml = readFileSync(join(rootDir, 'cli/Cargo.toml'), 'utf-8');
|
||||
const cargoVersionMatch = cargoToml.match(/^version\s*=\s*"([^"]*)"/m);
|
||||
|
||||
if (!cargoVersionMatch) {
|
||||
console.error('Could not find version in cli/Cargo.toml');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const cargoVersion = cargoVersionMatch[1];
|
||||
|
||||
if (packageVersion !== cargoVersion) {
|
||||
console.error('Version mismatch detected!');
|
||||
console.error(` package.json: ${packageVersion}`);
|
||||
console.error(` cli/Cargo.toml: ${cargoVersion}`);
|
||||
console.error('');
|
||||
console.error("Run 'pnpm run version:sync' to fix this.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`Versions are in sync: ${packageVersion}`);
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$ROOT_DIR"
|
||||
SKILL_NAME="agent-browser-stealth"
|
||||
|
||||
if ! command -v pnpm >/dev/null 2>&1; then
|
||||
echo "pnpm is required for ClawHub sync"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "skills/${SKILL_NAME}/SKILL.md" ]; then
|
||||
echo "Missing skill file: skills/${SKILL_NAME}/SKILL.md"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Sync only this fork-owned skill to avoid permission errors on other skills.
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMP_DIR"' EXIT
|
||||
mkdir -p "$TMP_DIR/skills"
|
||||
cp -R "skills/${SKILL_NAME}" "$TMP_DIR/skills/${SKILL_NAME}"
|
||||
|
||||
echo "Syncing local skill '${SKILL_NAME}' to ClawHub..."
|
||||
cd "$TMP_DIR"
|
||||
pnpm dlx clawhub@latest sync --all --root ./skills
|
||||
echo "ClawHub sync completed."
|
||||
@@ -12,7 +12,8 @@ import { platform, arch } from 'os';
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const projectRoot = join(__dirname, '..');
|
||||
|
||||
const sourcePath = join(projectRoot, 'cli/target/release/agent-browser');
|
||||
const sourceExt = platform() === 'win32' ? '.exe' : '';
|
||||
const sourcePath = join(projectRoot, `cli/target/release/agent-browser${sourceExt}`);
|
||||
const binDir = join(projectRoot, 'bin');
|
||||
|
||||
// Determine platform suffix
|
||||
|
||||
+168
-8
@@ -4,9 +4,12 @@
|
||||
* Postinstall script for agent-browser
|
||||
*
|
||||
* Downloads the platform-specific native binary if not present.
|
||||
* On global installs, patches npm's bin entry to use the native binary directly:
|
||||
* - Windows: Overwrites .cmd/.ps1 shims
|
||||
* - Mac/Linux: Replaces symlink to point to native binary
|
||||
*/
|
||||
|
||||
import { existsSync, mkdirSync, chmodSync, createWriteStream, unlinkSync } from 'fs';
|
||||
import { existsSync, mkdirSync, chmodSync, createWriteStream, unlinkSync, writeFileSync, symlinkSync, lstatSync, readFileSync } from 'fs';
|
||||
import { dirname, join } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { platform, arch } from 'os';
|
||||
@@ -24,15 +27,41 @@ const binaryName = `agent-browser-${platformKey}${ext}`;
|
||||
const binaryPath = join(binDir, binaryName);
|
||||
|
||||
// Package info
|
||||
const packageJson = JSON.parse(
|
||||
(await import('fs')).readFileSync(join(projectRoot, 'package.json'), 'utf8')
|
||||
);
|
||||
const packageJson = JSON.parse(readFileSync(join(projectRoot, 'package.json'), 'utf8'));
|
||||
const version = packageJson.version;
|
||||
const packageName = packageJson.name;
|
||||
const binCommands = getBinCommands(packageJson);
|
||||
|
||||
// GitHub release URL
|
||||
const GITHUB_REPO = 'anthropics/agent-browser'; // Update this to your actual repo
|
||||
const GITHUB_REPO = getGitHubRepoFromPackage(packageJson);
|
||||
const DOWNLOAD_URL = `https://github.com/${GITHUB_REPO}/releases/download/v${version}/${binaryName}`;
|
||||
|
||||
function getGitHubRepoFromPackage(pkg) {
|
||||
const repo = pkg?.repository;
|
||||
const repoUrl = typeof repo === 'string' ? repo : repo?.url;
|
||||
|
||||
if (typeof repoUrl === 'string') {
|
||||
const match = repoUrl.match(/github\.com[:/]([^/]+\/[^/.]+)(?:\.git)?$/i);
|
||||
if (match?.[1]) {
|
||||
return match[1];
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback for legacy package metadata
|
||||
return 'vercel-labs/agent-browser';
|
||||
}
|
||||
|
||||
function getBinCommands(pkg) {
|
||||
const bin = pkg?.bin;
|
||||
if (typeof bin === 'string') {
|
||||
return [pkg.name.replace(/^@[^/]+\//, '')];
|
||||
}
|
||||
if (bin && typeof bin === 'object') {
|
||||
return Object.keys(bin);
|
||||
}
|
||||
return ['agent-browser'];
|
||||
}
|
||||
|
||||
async function downloadFile(url, dest) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const file = createWriteStream(dest);
|
||||
@@ -68,7 +97,16 @@ async function downloadFile(url, dest) {
|
||||
async function main() {
|
||||
// Check if binary already exists
|
||||
if (existsSync(binaryPath)) {
|
||||
console.log(`✓ Native binary already exists: ${binaryName}`);
|
||||
// Ensure binary is executable (npm doesn't preserve execute bit)
|
||||
if (platform() !== 'win32') {
|
||||
chmodSync(binaryPath, 0o755);
|
||||
}
|
||||
console.log(`✓ Native binary ready: ${binaryName}`);
|
||||
|
||||
// On global installs, fix npm's bin entry to use native binary directly
|
||||
await fixGlobalInstallBin();
|
||||
|
||||
showPlaywrightReminder();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -95,10 +133,17 @@ async function main() {
|
||||
console.log('');
|
||||
console.log('To build the native binary locally:');
|
||||
console.log(' 1. Install Rust: https://rustup.rs');
|
||||
console.log(' 2. Run: npm run build:native');
|
||||
console.log(' 2. Run: pnpm run build:native');
|
||||
}
|
||||
|
||||
// Reminder about Playwright browsers
|
||||
// On global installs, fix npm's bin entry to use native binary directly
|
||||
// This avoids the /bin/sh error on Windows and provides zero-overhead execution
|
||||
await fixGlobalInstallBin();
|
||||
|
||||
showPlaywrightReminder();
|
||||
}
|
||||
|
||||
function showPlaywrightReminder() {
|
||||
console.log('');
|
||||
console.log('╔═══════════════════════════════════════════════════════════════════════════╗');
|
||||
console.log('║ To download browser binaries, run: ║');
|
||||
@@ -112,4 +157,119 @@ async function main() {
|
||||
console.log('╚═══════════════════════════════════════════════════════════════════════════╝');
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix npm's bin entry on global installs to use the native binary directly.
|
||||
* This provides zero-overhead CLI execution for global installs.
|
||||
*/
|
||||
async function fixGlobalInstallBin() {
|
||||
if (platform() === 'win32') {
|
||||
await fixWindowsShims();
|
||||
} else {
|
||||
await fixUnixSymlink();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix npm symlink on Mac/Linux global installs.
|
||||
* Replace the symlink to the JS wrapper with a symlink to the native binary.
|
||||
*/
|
||||
async function fixUnixSymlink() {
|
||||
// Get npm's global bin directory (npm prefix -g + /bin)
|
||||
let npmBinDir;
|
||||
try {
|
||||
const prefix = execSync('npm prefix -g', { encoding: 'utf8' }).trim();
|
||||
npmBinDir = join(prefix, 'bin');
|
||||
} catch {
|
||||
return; // npm not available
|
||||
}
|
||||
|
||||
let optimized = false;
|
||||
for (const commandName of binCommands) {
|
||||
const symlinkPath = join(npmBinDir, commandName);
|
||||
|
||||
// Check if symlink exists (indicates global install)
|
||||
try {
|
||||
const stat = lstatSync(symlinkPath);
|
||||
if (!stat.isSymbolicLink()) {
|
||||
continue; // Not a symlink, don't touch it
|
||||
}
|
||||
} catch {
|
||||
continue; // Symlink doesn't exist, not a global install
|
||||
}
|
||||
|
||||
// Replace symlink to point directly to native binary
|
||||
try {
|
||||
unlinkSync(symlinkPath);
|
||||
symlinkSync(binaryPath, symlinkPath);
|
||||
optimized = true;
|
||||
} catch (err) {
|
||||
// Permission error or other issue - not critical, JS wrapper still works
|
||||
console.log(`⚠ Could not optimize symlink (${commandName}): ${err.message}`);
|
||||
console.log(' CLI will work via Node.js wrapper (slightly slower startup)');
|
||||
}
|
||||
}
|
||||
|
||||
if (optimized) {
|
||||
console.log('✓ Optimized: symlink points to native binary (zero overhead)');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix npm-generated shims on Windows global installs.
|
||||
* npm generates shims that try to run /bin/sh, which doesn't exist on Windows.
|
||||
* We overwrite them to invoke the native .exe directly.
|
||||
*/
|
||||
async function fixWindowsShims() {
|
||||
// Check if this is a global install by looking for npm's global prefix
|
||||
let npmBinDir;
|
||||
try {
|
||||
npmBinDir = execSync('npm prefix -g', { encoding: 'utf8' }).trim();
|
||||
} catch {
|
||||
return; // Not a global install or npm not available
|
||||
}
|
||||
|
||||
// Path to native binary relative to npm prefix
|
||||
const packagePath = packageName.replace(/\//g, '\\');
|
||||
const relativeBinaryPath = `node_modules\\${packagePath}\\bin\\${binaryName}`;
|
||||
let optimized = false;
|
||||
|
||||
for (const commandName of binCommands) {
|
||||
// The shims are in the npm prefix directory (not prefix/bin on Windows)
|
||||
const cmdShim = join(npmBinDir, `${commandName}.cmd`);
|
||||
const ps1Shim = join(npmBinDir, `${commandName}.ps1`);
|
||||
|
||||
// Only fix if shims exist (indicates global install)
|
||||
if (!existsSync(cmdShim)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// Overwrite .cmd shim
|
||||
const cmdContent = `@ECHO off\r\n"%~dp0${relativeBinaryPath}" %*\r\n`;
|
||||
writeFileSync(cmdShim, cmdContent);
|
||||
|
||||
// Overwrite .ps1 shim
|
||||
const ps1Content = `#!/usr/bin/env pwsh
|
||||
$basedir = Split-Path $MyInvocation.MyCommand.Definition -Parent
|
||||
$exe = ""
|
||||
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
|
||||
$exe = ".exe"
|
||||
}
|
||||
& "$basedir/${relativeBinaryPath.replace(/\\/g, '/')}" $args
|
||||
exit $LASTEXITCODE
|
||||
`;
|
||||
writeFileSync(ps1Shim, ps1Content);
|
||||
optimized = true;
|
||||
} catch (err) {
|
||||
// Permission error or other issue - not critical, JS wrapper still works
|
||||
console.log(`⚠ Could not optimize shims (${commandName}): ${err.message}`);
|
||||
console.log(' CLI will work via Node.js wrapper (slightly slower startup)');
|
||||
}
|
||||
}
|
||||
|
||||
if (optimized) {
|
||||
console.log('✓ Optimized: shims point to native binary (zero overhead)');
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
|
||||
Executable
+142
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
UPSTREAM_REMOTE="upstream"
|
||||
UPSTREAM_BRANCH="main"
|
||||
BASE_BRANCH="main"
|
||||
TRACK_BRANCH="upstream-main"
|
||||
SYNC_BRANCH=""
|
||||
PUSH_BRANCH=false
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Synchronize upstream changes into a dedicated sync branch.
|
||||
|
||||
Usage:
|
||||
./scripts/sync-upstream.sh [options]
|
||||
|
||||
Options:
|
||||
--push Push the created sync branch to origin
|
||||
--upstream-remote <name> Upstream remote name (default: upstream)
|
||||
--upstream-branch <name> Upstream branch to sync from (default: main)
|
||||
--base-branch <name> Local base branch for sync branch (default: main)
|
||||
--track-branch <name> Local branch tracking upstream (default: upstream-main)
|
||||
--sync-branch <name> Explicit sync branch name (default: sync/YYYY-MM-DD)
|
||||
-h, --help Show this help message
|
||||
EOF
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--push)
|
||||
PUSH_BRANCH=true
|
||||
shift
|
||||
;;
|
||||
--upstream-remote)
|
||||
UPSTREAM_REMOTE="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--upstream-branch)
|
||||
UPSTREAM_BRANCH="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--base-branch)
|
||||
BASE_BRANCH="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--track-branch)
|
||||
TRACK_BRANCH="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--sync-branch)
|
||||
SYNC_BRANCH="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1" >&2
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
for var_name in UPSTREAM_REMOTE UPSTREAM_BRANCH BASE_BRANCH TRACK_BRANCH; do
|
||||
if [[ -z "${!var_name}" ]]; then
|
||||
echo "Error: ${var_name} cannot be empty." >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -n "$(git status --porcelain)" ]]; then
|
||||
echo "Error: working tree is not clean. Commit or stash changes first." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! git remote get-url "$UPSTREAM_REMOTE" >/dev/null 2>&1; then
|
||||
echo "Error: remote '$UPSTREAM_REMOTE' does not exist." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Fetching upstream branch: ${UPSTREAM_REMOTE}/${UPSTREAM_BRANCH}"
|
||||
git fetch "$UPSTREAM_REMOTE" "$UPSTREAM_BRANCH"
|
||||
|
||||
if git show-ref --verify --quiet "refs/heads/$TRACK_BRANCH"; then
|
||||
echo "Updating local track branch: $TRACK_BRANCH"
|
||||
git switch "$TRACK_BRANCH" >/dev/null
|
||||
git merge --ff-only "${UPSTREAM_REMOTE}/${UPSTREAM_BRANCH}"
|
||||
else
|
||||
echo "Creating local track branch: $TRACK_BRANCH"
|
||||
git branch "$TRACK_BRANCH" "${UPSTREAM_REMOTE}/${UPSTREAM_BRANCH}"
|
||||
fi
|
||||
|
||||
echo "Switching to base branch: $BASE_BRANCH"
|
||||
git switch "$BASE_BRANCH" >/dev/null
|
||||
|
||||
if git show-ref --verify --quiet "refs/remotes/origin/$BASE_BRANCH"; then
|
||||
echo "Fast-forwarding ${BASE_BRANCH} from origin/${BASE_BRANCH}"
|
||||
git fetch origin "$BASE_BRANCH"
|
||||
git merge --ff-only "origin/${BASE_BRANCH}"
|
||||
fi
|
||||
|
||||
if [[ -z "$SYNC_BRANCH" ]]; then
|
||||
SYNC_BRANCH="sync/$(date +%F)"
|
||||
fi
|
||||
|
||||
if git show-ref --verify --quiet "refs/heads/$SYNC_BRANCH"; then
|
||||
suffix=1
|
||||
while git show-ref --verify --quiet "refs/heads/${SYNC_BRANCH}-${suffix}"; do
|
||||
suffix=$((suffix + 1))
|
||||
done
|
||||
SYNC_BRANCH="${SYNC_BRANCH}-${suffix}"
|
||||
fi
|
||||
|
||||
echo "Creating sync branch: $SYNC_BRANCH"
|
||||
git switch -c "$SYNC_BRANCH" "$BASE_BRANCH" >/dev/null
|
||||
|
||||
merge_message="chore(sync): merge ${UPSTREAM_REMOTE}/${UPSTREAM_BRANCH} into ${BASE_BRANCH}"
|
||||
echo "Merging $TRACK_BRANCH into $SYNC_BRANCH"
|
||||
if ! git merge --no-ff "$TRACK_BRANCH" -m "$merge_message"; then
|
||||
echo ""
|
||||
echo "Merge conflict detected. Resolve conflicts, then run:"
|
||||
echo " git add <resolved-files>"
|
||||
echo " git commit"
|
||||
if [[ "$PUSH_BRANCH" == true ]]; then
|
||||
echo " git push -u origin $SYNC_BRANCH"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Upstream merge completed on branch: $SYNC_BRANCH"
|
||||
|
||||
if [[ "$PUSH_BRANCH" == true ]]; then
|
||||
echo "Pushing branch to origin: $SYNC_BRANCH"
|
||||
git push -u origin "$SYNC_BRANCH"
|
||||
echo "Done. Open a PR: ${SYNC_BRANCH} -> ${BASE_BRANCH}"
|
||||
else
|
||||
echo "Branch is local only. Push when ready:"
|
||||
echo " git push -u origin $SYNC_BRANCH"
|
||||
fi
|
||||
+46
-2
@@ -5,12 +5,14 @@
|
||||
* Run this script before building or releasing.
|
||||
*/
|
||||
|
||||
import { execSync } from "child_process";
|
||||
import { readFileSync, writeFileSync } from "fs";
|
||||
import { dirname, join } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const rootDir = join(__dirname, "..");
|
||||
const cliDir = join(rootDir, "cli");
|
||||
|
||||
// Read version from package.json (single source of truth)
|
||||
const packageJson = JSON.parse(
|
||||
@@ -18,20 +20,40 @@ const packageJson = JSON.parse(
|
||||
);
|
||||
const version = packageJson.version;
|
||||
|
||||
console.log(`Syncing version ${version} to all config files...`);
|
||||
function parseForkVersion(raw) {
|
||||
const match = raw.match(/^([0-9]+\.[0-9]+\.[0-9]+)-fork\.([A-Za-z0-9.-]+)$/);
|
||||
if (!match) return null;
|
||||
return {
|
||||
upstream: match[1],
|
||||
fork: match[2],
|
||||
};
|
||||
}
|
||||
|
||||
const forkVersion = parseForkVersion(version);
|
||||
if (forkVersion) {
|
||||
console.log(
|
||||
`Syncing version ${version} (upstream=${forkVersion.upstream}, fork=${forkVersion.fork}) to all config files...`
|
||||
);
|
||||
} else {
|
||||
console.log(`Syncing version ${version} to all config files...`);
|
||||
}
|
||||
|
||||
// Update Cargo.toml
|
||||
const cargoTomlPath = join(rootDir, "cli/Cargo.toml");
|
||||
const cargoTomlPath = join(cliDir, "Cargo.toml");
|
||||
let cargoToml = readFileSync(cargoTomlPath, "utf-8");
|
||||
const cargoVersionRegex = /^version\s*=\s*"[^"]*"/m;
|
||||
const newCargoVersion = `version = "${version}"`;
|
||||
const cargoNameMatch = cargoToml.match(/^name\s*=\s*"([^"]+)"/m);
|
||||
const cargoPackageName = cargoNameMatch?.[1] ?? "agent-browser-stealth";
|
||||
|
||||
let cargoTomlUpdated = false;
|
||||
if (cargoVersionRegex.test(cargoToml)) {
|
||||
const oldMatch = cargoToml.match(cargoVersionRegex)?.[0];
|
||||
if (oldMatch !== newCargoVersion) {
|
||||
cargoToml = cargoToml.replace(cargoVersionRegex, newCargoVersion);
|
||||
writeFileSync(cargoTomlPath, cargoToml);
|
||||
console.log(` Updated cli/Cargo.toml: ${oldMatch} -> ${newCargoVersion}`);
|
||||
cargoTomlUpdated = true;
|
||||
} else {
|
||||
console.log(` cli/Cargo.toml already up to date`);
|
||||
}
|
||||
@@ -40,4 +62,26 @@ if (cargoVersionRegex.test(cargoToml)) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Update Cargo.lock to match Cargo.toml
|
||||
if (cargoTomlUpdated) {
|
||||
try {
|
||||
execSync(`cargo update -p ${cargoPackageName} --offline`, {
|
||||
cwd: cliDir,
|
||||
stdio: "pipe",
|
||||
});
|
||||
console.log(` Updated cli/Cargo.lock`);
|
||||
} catch {
|
||||
// --offline may fail if package not in cache, try without it
|
||||
try {
|
||||
execSync(`cargo update -p ${cargoPackageName}`, {
|
||||
cwd: cliDir,
|
||||
stdio: "pipe",
|
||||
});
|
||||
console.log(` Updated cli/Cargo.lock`);
|
||||
} catch (e) {
|
||||
console.error(` Warning: Could not update Cargo.lock: ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Version sync complete.");
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Verifies that the bundled native binary version matches package.json version.
|
||||
* This prevents publishing npm tarballs where package version and native binary
|
||||
* version drift (e.g. package is fork.8 but binary still reports fork.7).
|
||||
*/
|
||||
|
||||
import { existsSync, readFileSync } from 'fs';
|
||||
import { dirname, join } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { arch, platform } from 'os';
|
||||
import { execFileSync } from 'child_process';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const projectRoot = join(__dirname, '..');
|
||||
|
||||
const pkg = JSON.parse(readFileSync(join(projectRoot, 'package.json'), 'utf8'));
|
||||
const expectedVersion = pkg.version;
|
||||
|
||||
const ext = platform() === 'win32' ? '.exe' : '';
|
||||
const platformBinary = join(projectRoot, 'bin', `agent-browser-${platform()}-${arch()}${ext}`);
|
||||
|
||||
if (!existsSync(platformBinary)) {
|
||||
console.error(`Error: native binary not found for current platform: ${platformBinary}`);
|
||||
console.error('Run `pnpm run build:native` before publishing.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let versionOutput = '';
|
||||
try {
|
||||
versionOutput = execFileSync(platformBinary, ['--version'], {
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
}).trim();
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.error(`Error: failed to execute native binary --version: ${message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!versionOutput.includes(expectedVersion)) {
|
||||
console.error(`Version mismatch: package.json=${expectedVersion}, native='${versionOutput}'.`);
|
||||
console.error('Run `pnpm run build:native` and retry publishing.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`✓ Native binary version matches package.json (${expectedVersion})`);
|
||||
@@ -0,0 +1,127 @@
|
||||
---
|
||||
name: agent-browser-stealth
|
||||
description: Stealth-first browser automation for OpenClaw using agent-browser-stealth. Use when tasks involve bot-protected websites, anti-fingerprint evasion, captcha-prone flows, login persistence, region-sensitive targets (e.g., Shopee/TikTok/e-commerce), or any request to automate web actions with lower detection risk.
|
||||
homepage: https://github.com/leeguooooo/agent-browser
|
||||
---
|
||||
|
||||
# agent-browser-stealth for OpenClaw
|
||||
|
||||
Use this skill when the task needs web automation and anti-bot stability.
|
||||
|
||||
## What this skill prioritizes
|
||||
|
||||
- Use `agent-browser` CLI from `agent-browser-stealth` package
|
||||
- Prefer stealth-safe interaction patterns over brittle one-shot scripts
|
||||
- Keep command flow deterministic: `open -> snapshot -> act -> re-snapshot`
|
||||
- Minimize bot signals with humanized pacing and stable session reuse
|
||||
|
||||
## Install and baseline
|
||||
|
||||
```bash
|
||||
pnpm add -g agent-browser-stealth
|
||||
agent-browser install
|
||||
agent-browser --version
|
||||
```
|
||||
|
||||
If default CDP mode is used in your environment, the CLI first tries `localhost:9333` and then auto-discovery. You can still pass `--cdp` / `--auto-connect` explicitly when needed.
|
||||
|
||||
## Standard execution workflow
|
||||
|
||||
```bash
|
||||
agent-browser open <url>
|
||||
agent-browser wait --load networkidle
|
||||
agent-browser snapshot -i
|
||||
# choose refs (@e1, @e2, ...)
|
||||
agent-browser click @eN
|
||||
agent-browser fill @eM "..."
|
||||
agent-browser snapshot -i
|
||||
```
|
||||
|
||||
Use refs (`@e1`) from snapshot output whenever possible.
|
||||
|
||||
## Anti-bot operating rules
|
||||
|
||||
1. Prefer headed mode for sensitive targets:
|
||||
|
||||
```bash
|
||||
agent-browser --headed --session-name shop open https://example.com
|
||||
```
|
||||
|
||||
2. Reuse session state to avoid repeated cold-start fingerprints:
|
||||
|
||||
```bash
|
||||
agent-browser --session-name shop open https://example.com
|
||||
```
|
||||
|
||||
3. Keep interactions human-like:
|
||||
|
||||
```bash
|
||||
agent-browser type @e2 "query" --delay 120
|
||||
agent-browser wait 1200-2600
|
||||
```
|
||||
|
||||
4. For contenteditable editors, use keyboard mode:
|
||||
|
||||
```bash
|
||||
agent-browser click "[contenteditable='true']"
|
||||
agent-browser keyboard type "Hello world" --delay 90
|
||||
```
|
||||
|
||||
5. If text must literally include `--delay`, stop arg parsing with `--`:
|
||||
|
||||
```bash
|
||||
agent-browser type @e2 -- "--delay 120"
|
||||
agent-browser keyboard type -- "--delay 120"
|
||||
```
|
||||
|
||||
## Region-sensitive websites
|
||||
|
||||
For region-bound sites, open target domain directly and let locale/timezone alignment apply.
|
||||
|
||||
```bash
|
||||
agent-browser open https://shopee.tw
|
||||
```
|
||||
|
||||
Only override locale/timezone when explicitly required by the task.
|
||||
|
||||
## Recovery patterns
|
||||
|
||||
If blocked or unstable:
|
||||
|
||||
1. Retry with `--headed`.
|
||||
2. Reuse `--session-name`.
|
||||
3. Slow down action cadence (`wait`, `type --delay`).
|
||||
4. Re-open page and regenerate refs with `snapshot -i`.
|
||||
|
||||
## Minimal recipes
|
||||
|
||||
Login flow:
|
||||
|
||||
```bash
|
||||
agent-browser --session-name account open https://example.com/login
|
||||
agent-browser snapshot -i
|
||||
agent-browser fill @e1 "$USERNAME"
|
||||
agent-browser fill @e2 "$PASSWORD"
|
||||
agent-browser click @e3
|
||||
agent-browser wait --url "**/dashboard"
|
||||
```
|
||||
|
||||
Search and capture:
|
||||
|
||||
```bash
|
||||
agent-browser open https://example.com
|
||||
agent-browser snapshot -i
|
||||
agent-browser type @e2 "iphone" --delay 120
|
||||
agent-browser press Enter
|
||||
agent-browser wait --load networkidle
|
||||
agent-browser screenshot result.png
|
||||
```
|
||||
|
||||
## Output expectations for OpenClaw
|
||||
|
||||
When using this skill, return:
|
||||
|
||||
- Exact commands executed
|
||||
- Key page state changes (URL/title/important element text)
|
||||
- Any anti-bot signal encountered and mitigation used
|
||||
- Next safe action
|
||||
+533
-97
@@ -1,98 +1,26 @@
|
||||
---
|
||||
name: agent-browser
|
||||
description: Automates browser interactions for web testing, form filling, screenshots, and data extraction. Use when the user needs to navigate websites, interact with web pages, fill forms, take screenshots, test web applications, or extract information from web pages.
|
||||
description: Browser automation CLI for AI agents. Use when the user needs to interact with websites, including navigating pages, filling forms, clicking buttons, taking screenshots, extracting data, testing web apps, or automating any browser task. Triggers include requests to "open a website", "fill out a form", "click a button", "take a screenshot", "scrape data from a page", "test this web app", "login to a site", "automate browser actions", or any task requiring programmatic web interaction.
|
||||
allowed-tools: Bash(npx agent-browser-stealth:*), Bash(npx agent-browser:*), Bash(agent-browser:*)
|
||||
---
|
||||
|
||||
# Browser Automation with agent-browser
|
||||
|
||||
## Quick start
|
||||
Install package: `pnpm add -g agent-browser-stealth` (CLI command remains `agent-browser` for compatibility). If global install is unavailable in your environment, use `pnpm dlx agent-browser-stealth <command>` for one-off runs.
|
||||
|
||||
```bash
|
||||
agent-browser open <url> # Navigate to page
|
||||
agent-browser snapshot -i # Get interactive elements with refs
|
||||
agent-browser click @e1 # Click element by ref
|
||||
agent-browser fill @e2 "text" # Fill input by ref
|
||||
agent-browser close # Close browser
|
||||
```
|
||||
## Core Workflow
|
||||
|
||||
## Core workflow
|
||||
Every browser automation follows this pattern:
|
||||
|
||||
1. Navigate: `agent-browser open <url>`
|
||||
2. Snapshot: `agent-browser snapshot -i` (returns elements with refs like `@e1`, `@e2`)
|
||||
3. Interact using refs from the snapshot
|
||||
4. Re-snapshot after navigation or significant DOM changes
|
||||
|
||||
## Commands
|
||||
|
||||
### Navigation
|
||||
```bash
|
||||
agent-browser open <url> # Navigate to URL
|
||||
agent-browser back # Go back
|
||||
agent-browser forward # Go forward
|
||||
agent-browser reload # Reload page
|
||||
agent-browser close # Close browser
|
||||
```
|
||||
|
||||
### Snapshot (page analysis)
|
||||
```bash
|
||||
agent-browser snapshot # Full accessibility tree
|
||||
agent-browser snapshot -i # Interactive elements only (recommended)
|
||||
agent-browser snapshot -c # Compact output
|
||||
agent-browser snapshot -d 3 # Limit depth to 3
|
||||
```
|
||||
|
||||
### Interactions (use @refs from snapshot)
|
||||
```bash
|
||||
agent-browser click @e1 # Click
|
||||
agent-browser dblclick @e1 # Double-click
|
||||
agent-browser fill @e2 "text" # Clear and type
|
||||
agent-browser type @e2 "text" # Type without clearing
|
||||
agent-browser press Enter # Press key
|
||||
agent-browser press Control+a # Key combination
|
||||
agent-browser hover @e1 # Hover
|
||||
agent-browser check @e1 # Check checkbox
|
||||
agent-browser uncheck @e1 # Uncheck checkbox
|
||||
agent-browser select @e1 "value" # Select dropdown
|
||||
agent-browser scroll down 500 # Scroll page
|
||||
agent-browser scrollintoview @e1 # Scroll element into view
|
||||
```
|
||||
|
||||
### Get information
|
||||
```bash
|
||||
agent-browser get text @e1 # Get element text
|
||||
agent-browser get value @e1 # Get input value
|
||||
agent-browser get title # Get page title
|
||||
agent-browser get url # Get current URL
|
||||
```
|
||||
|
||||
### Screenshots
|
||||
```bash
|
||||
agent-browser screenshot # Screenshot to stdout
|
||||
agent-browser screenshot path.png # Save to file
|
||||
agent-browser screenshot --full # Full page
|
||||
```
|
||||
|
||||
### Wait
|
||||
```bash
|
||||
agent-browser wait @e1 # Wait for element
|
||||
agent-browser wait 2000 # Wait milliseconds
|
||||
agent-browser wait --text "Success" # Wait for text
|
||||
agent-browser wait --load networkidle # Wait for network idle
|
||||
```
|
||||
|
||||
### Semantic locators (alternative to refs)
|
||||
```bash
|
||||
agent-browser find role button click --name "Submit"
|
||||
agent-browser find text "Sign In" click
|
||||
agent-browser find label "Email" fill "user@test.com"
|
||||
```
|
||||
|
||||
## Example: Form submission
|
||||
1. **Navigate**: `agent-browser open <url>`
|
||||
2. **Snapshot**: `agent-browser snapshot -i` (get element refs like `@e1`, `@e2`)
|
||||
3. **Interact**: Use refs to click, fill, select
|
||||
4. **Re-snapshot**: After navigation or DOM changes, get fresh refs
|
||||
|
||||
```bash
|
||||
agent-browser open https://example.com/form
|
||||
agent-browser snapshot -i
|
||||
# Output shows: textbox "Email" [ref=e1], textbox "Password" [ref=e2], button "Submit" [ref=e3]
|
||||
# Output: @e1 [input type="email"], @e2 [input type="password"], @e3 [button] "Submit"
|
||||
|
||||
agent-browser fill @e1 "user@example.com"
|
||||
agent-browser fill @e2 "password123"
|
||||
@@ -101,43 +29,551 @@ agent-browser wait --load networkidle
|
||||
agent-browser snapshot -i # Check result
|
||||
```
|
||||
|
||||
## Example: Authentication with saved state
|
||||
## Command Chaining
|
||||
|
||||
Commands can be chained with `&&` in a single shell invocation. The browser persists between commands via a background daemon, so chaining is safe and more efficient than separate calls.
|
||||
|
||||
```bash
|
||||
# Login once
|
||||
# Chain open + wait + snapshot in one call
|
||||
agent-browser open https://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 "password123" && agent-browser click @e3
|
||||
|
||||
# Navigate and capture
|
||||
agent-browser open https://example.com && agent-browser wait --load networkidle && agent-browser screenshot page.png
|
||||
```
|
||||
|
||||
**When to chain:** Use `&&` when you don't need to read the output of an intermediate command before proceeding (e.g., open + wait + screenshot). Run commands separately when you need to parse the output first (e.g., snapshot to discover refs, then interact using those refs).
|
||||
|
||||
## Essential Commands
|
||||
|
||||
```bash
|
||||
# Navigation
|
||||
agent-browser open <url> # Navigate (aliases: goto, navigate)
|
||||
agent-browser --risk-mode block open <url> # Block if verification/captcha interstitial is detected
|
||||
agent-browser close # Close browser
|
||||
agent-browser --version # Show CLI version (fork builds include upstream/fork)
|
||||
|
||||
# Snapshot
|
||||
agent-browser snapshot -i # Interactive elements with refs (recommended)
|
||||
agent-browser snapshot -i -C # Include cursor-interactive elements (divs with onclick, cursor:pointer)
|
||||
agent-browser snapshot -s "#selector" # Scope to CSS selector
|
||||
|
||||
# Interaction (use @refs from snapshot)
|
||||
agent-browser click @e1 # Click element
|
||||
agent-browser click @e1 --new-tab # Click and open in new tab
|
||||
agent-browser fill @e2 "text" # Clear and type text
|
||||
agent-browser type @e2 "text" --delay 120 # Type without clearing (human-like pacing)
|
||||
agent-browser select @e1 "option" # Select dropdown option
|
||||
agent-browser check @e1 # Check checkbox
|
||||
agent-browser press Enter # Press key
|
||||
agent-browser keyboard type "text" --delay 90 # Type at current focus (no selector)
|
||||
agent-browser keyboard inserttext "text" # Insert without key events
|
||||
agent-browser scroll down 500 # Scroll page
|
||||
agent-browser scroll down 500 --selector "div.content" # Scroll within a specific container
|
||||
|
||||
# Get information
|
||||
agent-browser get text @e1 # Get element text
|
||||
agent-browser get url # Get current URL
|
||||
agent-browser get title # Get page title
|
||||
|
||||
# Wait
|
||||
agent-browser wait @e1 # Wait for element
|
||||
agent-browser wait --load networkidle # Wait for network idle
|
||||
agent-browser wait --url "**/page" # Wait for URL pattern
|
||||
agent-browser wait 2000 # Wait milliseconds
|
||||
agent-browser wait 2000-5000 # Random wait between 2-5 seconds
|
||||
|
||||
# Downloads
|
||||
agent-browser download @e1 ./file.pdf # Click element to trigger download
|
||||
agent-browser wait --download ./output.zip # Wait for any download to complete
|
||||
agent-browser --download-path ./downloads open <url> # Set default download directory
|
||||
|
||||
# Capture
|
||||
agent-browser screenshot # Screenshot to temp dir
|
||||
agent-browser screenshot --full # Full page screenshot
|
||||
agent-browser screenshot --annotate # Annotated screenshot with numbered element labels
|
||||
agent-browser pdf output.pdf # Save as PDF
|
||||
|
||||
# Diff (compare page states)
|
||||
agent-browser diff snapshot # Compare current vs last snapshot
|
||||
agent-browser diff snapshot --baseline before.txt # Compare current vs saved file
|
||||
agent-browser diff screenshot --baseline before.png # Visual pixel diff
|
||||
agent-browser diff url <url1> <url2> # Compare two pages
|
||||
agent-browser diff url <url1> <url2> --wait-until networkidle # Custom wait strategy
|
||||
agent-browser diff url <url1> <url2> --selector "#main" # Scope to element
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Form Submission
|
||||
|
||||
```bash
|
||||
agent-browser open https://example.com/signup
|
||||
agent-browser snapshot -i
|
||||
agent-browser fill @e1 "Jane Doe"
|
||||
agent-browser fill @e2 "jane@example.com"
|
||||
agent-browser select @e3 "California"
|
||||
agent-browser check @e4
|
||||
agent-browser click @e5
|
||||
agent-browser wait --load networkidle
|
||||
```
|
||||
|
||||
### Authentication with Auth Vault (Recommended)
|
||||
|
||||
```bash
|
||||
# Save credentials once (encrypted with AGENT_BROWSER_ENCRYPTION_KEY)
|
||||
# Recommended: pipe password via stdin to avoid shell history exposure
|
||||
echo "pass" | agent-browser auth save github --url https://github.com/login --username user --password-stdin
|
||||
|
||||
# Login using saved profile (LLM never sees password)
|
||||
agent-browser auth login github
|
||||
|
||||
# List/show/delete profiles
|
||||
agent-browser auth list
|
||||
agent-browser auth show github
|
||||
agent-browser auth delete github
|
||||
```
|
||||
|
||||
### Authentication with State Persistence
|
||||
|
||||
```bash
|
||||
# Login once and save state
|
||||
agent-browser open https://app.example.com/login
|
||||
agent-browser snapshot -i
|
||||
agent-browser fill @e1 "username"
|
||||
agent-browser fill @e2 "password"
|
||||
agent-browser fill @e1 "$USERNAME"
|
||||
agent-browser fill @e2 "$PASSWORD"
|
||||
agent-browser click @e3
|
||||
agent-browser wait --url "**/dashboard"
|
||||
agent-browser state save auth.json
|
||||
|
||||
# Later sessions: load saved state
|
||||
# Reuse in future sessions
|
||||
agent-browser state load auth.json
|
||||
agent-browser open https://app.example.com/dashboard
|
||||
```
|
||||
|
||||
## Sessions (parallel browsers)
|
||||
### Cookie Injection for Auth Callbacks
|
||||
|
||||
```bash
|
||||
agent-browser --session test1 open site-a.com
|
||||
agent-browser --session test2 open site-b.com
|
||||
agent-browser session list
|
||||
# Before navigation: set by URL
|
||||
agent-browser cookies set session_id "abc123" --url https://app.example.com/api/auth/sso/callback
|
||||
|
||||
# Explicit domain/path pair (must be provided together)
|
||||
agent-browser cookies set auth_token "xyz789" --domain .example.com --path /api
|
||||
|
||||
# Or navigate first and rely on current URL
|
||||
agent-browser open https://app.example.com/api/auth/sso/callback
|
||||
agent-browser cookies set callback_token "token123"
|
||||
```
|
||||
|
||||
## JSON output (for parsing)
|
||||
### Session Persistence
|
||||
|
||||
Add `--json` for machine-readable output:
|
||||
```bash
|
||||
# Auto-save/restore cookies and localStorage across browser restarts
|
||||
agent-browser --session-name myapp open https://app.example.com/login
|
||||
# ... login flow ...
|
||||
agent-browser close # State auto-saved to ~/.agent-browser/sessions/
|
||||
|
||||
# Next time, state is auto-loaded
|
||||
agent-browser --session-name myapp open https://app.example.com/dashboard
|
||||
|
||||
# Encrypt state at rest
|
||||
export AGENT_BROWSER_ENCRYPTION_KEY=$(openssl rand -hex 32)
|
||||
agent-browser --session-name secure open https://app.example.com
|
||||
|
||||
# Manage saved states
|
||||
agent-browser state list
|
||||
agent-browser state show myapp-default.json
|
||||
agent-browser state clear myapp
|
||||
agent-browser state clean --older-than 7
|
||||
```
|
||||
|
||||
### Data Extraction
|
||||
|
||||
```bash
|
||||
agent-browser open https://example.com/products
|
||||
agent-browser snapshot -i
|
||||
agent-browser get text @e5 # Get specific element text
|
||||
agent-browser get text body > page.txt # Get all page text
|
||||
|
||||
# JSON output for parsing
|
||||
agent-browser snapshot -i --json
|
||||
agent-browser get text @e1 --json
|
||||
```
|
||||
|
||||
## Debugging
|
||||
### Parallel Sessions
|
||||
|
||||
```bash
|
||||
agent-browser open example.com --headed # Show browser window
|
||||
agent-browser console # View console messages
|
||||
agent-browser errors # View page errors
|
||||
agent-browser --session site1 open https://site-a.com
|
||||
agent-browser --session site2 open https://site-b.com
|
||||
|
||||
agent-browser --session site1 snapshot -i
|
||||
agent-browser --session site2 snapshot -i
|
||||
|
||||
agent-browser session list
|
||||
```
|
||||
|
||||
### Connect to Existing Chrome
|
||||
|
||||
By default in this fork, commands without `--cdp` auto-attach to your existing browser with this order:
|
||||
|
||||
1. Try CDP at `localhost:9333`
|
||||
2. If unavailable, fall back to `--auto-connect`-style discovery
|
||||
3. If both fail, exit with guidance (no automatic managed local browser launch on this path)
|
||||
|
||||
```bash
|
||||
# Auto-discover running Chrome with remote debugging enabled
|
||||
agent-browser --auto-connect open https://example.com
|
||||
agent-browser --auto-connect snapshot
|
||||
|
||||
# Or with explicit CDP port
|
||||
agent-browser --cdp 9222 snapshot
|
||||
|
||||
# Debug auto-attach behavior
|
||||
agent-browser --debug snapshot
|
||||
```
|
||||
|
||||
### Color Scheme (Dark Mode)
|
||||
|
||||
```bash
|
||||
# Persistent dark mode via flag (applies to all pages and new tabs)
|
||||
agent-browser --color-scheme dark open https://example.com
|
||||
|
||||
# Or via environment variable
|
||||
AGENT_BROWSER_COLOR_SCHEME=dark agent-browser open https://example.com
|
||||
|
||||
# Or set during session (persists for subsequent commands)
|
||||
agent-browser set media dark
|
||||
```
|
||||
|
||||
### Visual Browser (Debugging)
|
||||
|
||||
```bash
|
||||
agent-browser --headed open https://example.com
|
||||
agent-browser highlight @e1 # Highlight element
|
||||
agent-browser record start demo.webm # Record session
|
||||
agent-browser profiler start # Start Chrome DevTools profiling
|
||||
agent-browser profiler stop trace.json # Stop and save profile (path optional)
|
||||
```
|
||||
|
||||
### Local Files (PDFs, HTML)
|
||||
|
||||
```bash
|
||||
# Open local files with file:// URLs
|
||||
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
|
||||
```
|
||||
|
||||
### Project Policy
|
||||
|
||||
- `--profile` / `AGENT_BROWSER_PROFILE` are forbidden
|
||||
- `--channel` / `AGENT_BROWSER_CHANNEL` are forbidden
|
||||
- Use existing browser sessions (default attach path: CDP `localhost:9333` then auto-discovery) or pass `--cdp` explicitly
|
||||
|
||||
### Stealth Mode (Always On)
|
||||
|
||||
Stealth is always active -- no flags needed. All sessions automatically apply anti-detection patches (navigator.webdriver removal, UA override, plugin injection, WebGL masking, humanized interactions, etc.).
|
||||
|
||||
Chromium launches in managed mode use Chrome channel by default for a genuine browser binary fingerprint.
|
||||
|
||||
For best results against strong bot detection, use `--headed` and `--session-name`.
|
||||
|
||||
### Auto Region Detection
|
||||
|
||||
The browser automatically detects the target site's region from the URL TLD and sets matching locale, timezone, and Accept-Language headers. For example, navigating to `shopee.tw` sets locale `zh-TW` and timezone `Asia/Taipei`. This reduces server-side risk scoring from region-signal mismatches.
|
||||
|
||||
Override: `AGENT_BROWSER_LOCALE`, `AGENT_BROWSER_TIMEZONE` env vars.
|
||||
|
||||
### Captcha Detection & Auto-Retry
|
||||
|
||||
When a navigation lands on a captcha/verification page, behavior is controlled by `--risk-mode` (or `AGENT_BROWSER_RISK_MODE`):
|
||||
|
||||
- `warn` (default): retry up to 2 times with randomized backoff (3-7s), then return warning plus structured `riskSignals`
|
||||
- `block`: fail fast once a risk interstitial is detected
|
||||
- `off`: disable this detection/retry path
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
agent-browser --risk-mode warn open https://example.com
|
||||
agent-browser --risk-mode block open https://example.com
|
||||
AGENT_BROWSER_RISK_MODE=off agent-browser open https://example.com
|
||||
```
|
||||
|
||||
### iOS Simulator (Mobile Safari)
|
||||
|
||||
```bash
|
||||
# List available iOS simulators
|
||||
agent-browser device list
|
||||
|
||||
# Launch Safari on a specific device
|
||||
agent-browser -p ios --device "iPhone 16 Pro" open https://example.com
|
||||
|
||||
# Same workflow as desktop - snapshot, interact, re-snapshot
|
||||
agent-browser -p ios snapshot -i
|
||||
agent-browser -p ios tap @e1 # Tap (alias for click)
|
||||
agent-browser -p ios fill @e2 "text"
|
||||
agent-browser -p ios swipe up # Mobile-specific gesture
|
||||
|
||||
# Take screenshot
|
||||
agent-browser -p ios screenshot mobile.png
|
||||
|
||||
# Close session (shuts down simulator)
|
||||
agent-browser -p ios close
|
||||
```
|
||||
|
||||
**Requirements:** macOS with Xcode, Appium (`npm install -g appium && appium driver install xcuitest`)
|
||||
|
||||
**Real devices:** Works with physical iOS devices if pre-configured. Use `--device "<UDID>"` where UDID is from `xcrun xctrace list devices`.
|
||||
|
||||
## Security
|
||||
|
||||
All security features are opt-in. By default, agent-browser imposes no restrictions on navigation, actions, or output.
|
||||
|
||||
### Content Boundaries (Recommended for AI Agents)
|
||||
|
||||
Enable `--content-boundaries` to wrap page-sourced output in markers that help LLMs distinguish tool output from untrusted page content:
|
||||
|
||||
```bash
|
||||
export AGENT_BROWSER_CONTENT_BOUNDARIES=1
|
||||
agent-browser snapshot
|
||||
# Output:
|
||||
# --- AGENT_BROWSER_PAGE_CONTENT nonce=<hex> origin=https://example.com ---
|
||||
# [accessibility tree]
|
||||
# --- END_AGENT_BROWSER_PAGE_CONTENT nonce=<hex> ---
|
||||
```
|
||||
|
||||
### Domain Allowlist
|
||||
|
||||
Restrict navigation to trusted domains. Wildcards like `*.example.com` also match the bare domain `example.com`. Sub-resource requests, WebSocket, and EventSource connections to non-allowed domains are also blocked. Include CDN domains your target pages depend on:
|
||||
|
||||
```bash
|
||||
export AGENT_BROWSER_ALLOWED_DOMAINS="example.com,*.example.com"
|
||||
agent-browser open https://example.com # OK
|
||||
agent-browser open https://malicious.com # Blocked
|
||||
```
|
||||
|
||||
### Action Policy
|
||||
|
||||
Use a policy file to gate destructive actions:
|
||||
|
||||
```bash
|
||||
export AGENT_BROWSER_ACTION_POLICY=./policy.json
|
||||
```
|
||||
|
||||
Example `policy.json`:
|
||||
|
||||
```json
|
||||
{ "default": "deny", "allow": ["navigate", "snapshot", "click", "scroll", "wait", "get"] }
|
||||
```
|
||||
|
||||
Auth vault operations (`auth login`, etc.) bypass action policy but domain allowlist still applies.
|
||||
|
||||
### Output Limits
|
||||
|
||||
Prevent context flooding from large pages:
|
||||
|
||||
```bash
|
||||
export AGENT_BROWSER_MAX_OUTPUT=50000
|
||||
```
|
||||
|
||||
## Diffing (Verifying Changes)
|
||||
|
||||
Use `diff snapshot` after performing an action to verify it had the intended effect. This compares the current accessibility tree against the last snapshot taken in the session.
|
||||
|
||||
```bash
|
||||
# Typical workflow: snapshot -> action -> diff
|
||||
agent-browser snapshot -i # Take baseline snapshot
|
||||
agent-browser click @e2 # Perform action
|
||||
agent-browser diff snapshot # See what changed (auto-compares to last snapshot)
|
||||
```
|
||||
|
||||
For visual regression testing or monitoring:
|
||||
|
||||
```bash
|
||||
# Save a baseline screenshot, then compare later
|
||||
agent-browser screenshot baseline.png
|
||||
# ... time passes or changes are made ...
|
||||
agent-browser diff screenshot --baseline baseline.png
|
||||
|
||||
# Compare staging vs production
|
||||
agent-browser diff url https://staging.example.com https://prod.example.com --screenshot
|
||||
```
|
||||
|
||||
`diff snapshot` output uses `+` for additions and `-` for removals, similar to git diff. `diff screenshot` produces a diff image with changed pixels highlighted in red, plus a mismatch percentage.
|
||||
|
||||
## Timeouts and Slow Pages
|
||||
|
||||
The default Playwright timeout is 25 seconds for local browsers. This can be overridden with the `AGENT_BROWSER_DEFAULT_TIMEOUT` environment variable (value in milliseconds). For slow websites or large pages, use explicit waits instead of relying on the default timeout:
|
||||
|
||||
```bash
|
||||
# Wait for network activity to settle (best for slow pages)
|
||||
agent-browser wait --load networkidle
|
||||
|
||||
# Wait for a specific element to appear
|
||||
agent-browser wait "#content"
|
||||
agent-browser wait @e1
|
||||
|
||||
# Wait for a specific URL pattern (useful after redirects)
|
||||
agent-browser wait --url "**/dashboard"
|
||||
|
||||
# Wait for a JavaScript condition
|
||||
agent-browser wait --fn "document.readyState === 'complete'"
|
||||
|
||||
# Wait a fixed duration (milliseconds) as a last resort
|
||||
agent-browser wait 5000
|
||||
|
||||
# Random wait between 2-5 seconds (useful for anti-detection)
|
||||
agent-browser wait 2000-5000
|
||||
```
|
||||
|
||||
When dealing with consistently slow websites, use `wait --load networkidle` after `open` to ensure the page is fully loaded before taking a snapshot. If a specific element is slow to render, wait for it directly with `wait <selector>` or `wait @ref`.
|
||||
|
||||
### Humanized Interactions
|
||||
|
||||
agent-browser automatically humanizes interactions to avoid behavioral detection:
|
||||
|
||||
- **Randomized typing**: `type --delay` varies each keystroke delay by +-40%
|
||||
- **Random wait ranges**: `wait 2000-5000` pauses for a random duration in that range
|
||||
- **Bezier curve mouse**: Before every `click`, the mouse moves along a natural-looking curve
|
||||
|
||||
These behaviors are always active. For sensitive sites, combine with `--headed` and `--session-name` for best results.
|
||||
|
||||
## Session Management and Cleanup
|
||||
|
||||
When running multiple agents or automations concurrently, always use named sessions to avoid conflicts:
|
||||
|
||||
```bash
|
||||
# Each agent gets its own isolated session
|
||||
agent-browser --session agent1 open site-a.com
|
||||
agent-browser --session agent2 open site-b.com
|
||||
|
||||
# Check active sessions
|
||||
agent-browser session list
|
||||
```
|
||||
|
||||
Always close your browser session when done to avoid leaked processes:
|
||||
|
||||
```bash
|
||||
agent-browser close # Close default session
|
||||
agent-browser --session agent1 close # Close specific session
|
||||
```
|
||||
|
||||
If a previous session was not closed properly, the daemon may still be running. Use `agent-browser close` to clean it up before starting new work.
|
||||
|
||||
## Ref Lifecycle (Important)
|
||||
|
||||
Refs (`@e1`, `@e2`, etc.) are invalidated when the page changes. Always re-snapshot after:
|
||||
|
||||
- Clicking links or buttons that navigate
|
||||
- Form submissions
|
||||
- Dynamic content loading (dropdowns, modals)
|
||||
|
||||
```bash
|
||||
agent-browser click @e5 # Navigates to new page
|
||||
agent-browser snapshot -i # MUST re-snapshot
|
||||
agent-browser click @e1 # Use new refs
|
||||
```
|
||||
|
||||
## Annotated Screenshots (Vision Mode)
|
||||
|
||||
Use `--annotate` to take a screenshot with numbered labels overlaid on interactive elements. Each label `[N]` maps to ref `@eN`. This also caches refs, so you can interact with elements immediately without a separate snapshot.
|
||||
|
||||
```bash
|
||||
agent-browser screenshot --annotate
|
||||
# Output includes the image path and a legend:
|
||||
# [1] @e1 button "Submit"
|
||||
# [2] @e2 link "Home"
|
||||
# [3] @e3 textbox "Email"
|
||||
agent-browser click @e2 # Click using ref from annotated screenshot
|
||||
```
|
||||
|
||||
Use annotated screenshots when:
|
||||
|
||||
- The page has unlabeled icon buttons or visual-only elements
|
||||
- You need to verify visual layout or styling
|
||||
- Canvas or chart elements are present (invisible to text snapshots)
|
||||
- You need spatial reasoning about element positions
|
||||
|
||||
## Semantic Locators (Alternative to Refs)
|
||||
|
||||
When refs are unavailable or unreliable, use semantic locators:
|
||||
|
||||
```bash
|
||||
agent-browser find text "Sign In" click
|
||||
agent-browser find label "Email" fill "user@test.com"
|
||||
agent-browser find role button click --name "Submit"
|
||||
agent-browser find placeholder "Search" type "query"
|
||||
agent-browser find testid "submit-btn" click
|
||||
```
|
||||
|
||||
## JavaScript Evaluation (eval)
|
||||
|
||||
Use `eval` to run JavaScript in the browser context. **Shell quoting can corrupt complex expressions** -- use `--stdin` or `-b` to avoid issues.
|
||||
|
||||
```bash
|
||||
# Simple expressions work with regular quoting
|
||||
agent-browser eval 'document.title'
|
||||
agent-browser eval 'document.querySelectorAll("img").length'
|
||||
|
||||
# Complex JS: use --stdin with heredoc (RECOMMENDED)
|
||||
agent-browser eval --stdin <<'EVALEOF'
|
||||
JSON.stringify(
|
||||
Array.from(document.querySelectorAll("img"))
|
||||
.filter(i => !i.alt)
|
||||
.map(i => ({ src: i.src.split("/").pop(), width: i.width }))
|
||||
)
|
||||
EVALEOF
|
||||
|
||||
# Alternative: base64 encoding (avoids all shell escaping issues)
|
||||
agent-browser eval -b "$(echo -n 'Array.from(document.querySelectorAll("a")).map(a => a.href)' | base64)"
|
||||
```
|
||||
|
||||
**Why this matters:** When the shell processes your command, inner double quotes, `!` characters (history expansion), backticks, and `$()` can all corrupt the JavaScript before it reaches agent-browser. The `--stdin` and `-b` flags bypass shell interpretation entirely.
|
||||
|
||||
**Rules of thumb:**
|
||||
|
||||
- Single-line, no nested quotes -> regular `eval 'expression'` with single quotes is fine
|
||||
- Nested quotes, arrow functions, template literals, or multiline -> use `eval --stdin <<'EVALEOF'`
|
||||
- Programmatic/generated scripts -> use `eval -b` with base64
|
||||
|
||||
## Configuration File
|
||||
|
||||
Create `agent-browser.json` in the project root for persistent settings:
|
||||
|
||||
```json
|
||||
{
|
||||
"headed": true,
|
||||
"proxy": "http://localhost:8080"
|
||||
}
|
||||
```
|
||||
|
||||
Priority (lowest to highest): `~/.agent-browser/config.json` < `./agent-browser.json` < env vars < CLI flags. Use `--config <path>` or `AGENT_BROWSER_CONFIG` env var for a custom config file (exits with error if missing/invalid). All CLI options map to camelCase keys (e.g., `--executable-path` -> `"executablePath"`). Boolean flags accept `true`/`false` values (e.g., `--headed false` overrides config). Extensions from user and project configs are merged, not replaced.
|
||||
|
||||
## Deep-Dive Documentation
|
||||
|
||||
| Reference | When to Use |
|
||||
| -------------------------------------------------------------------- | --------------------------------------------------------- |
|
||||
| [references/commands.md](references/commands.md) | Full command reference with all options |
|
||||
| [references/snapshot-refs.md](references/snapshot-refs.md) | Ref lifecycle, invalidation rules, troubleshooting |
|
||||
| [references/session-management.md](references/session-management.md) | Parallel sessions, state persistence, concurrent scraping |
|
||||
| [references/authentication.md](references/authentication.md) | Login flows, OAuth, 2FA handling, state reuse |
|
||||
| [references/video-recording.md](references/video-recording.md) | Recording workflows for debugging and documentation |
|
||||
| [references/profiling.md](references/profiling.md) | Chrome DevTools profiling for performance analysis |
|
||||
| [references/proxy-support.md](references/proxy-support.md) | Proxy configuration, geo-testing, rotating proxies |
|
||||
|
||||
## Ready-to-Use Templates
|
||||
|
||||
| Template | Description |
|
||||
| ------------------------------------------------------------------------ | ----------------------------------- |
|
||||
| [templates/form-automation.sh](templates/form-automation.sh) | Form filling with validation |
|
||||
| [templates/authenticated-session.sh](templates/authenticated-session.sh) | Login once, reuse state |
|
||||
| [templates/capture-workflow.sh](templates/capture-workflow.sh) | Content extraction with screenshots |
|
||||
|
||||
```bash
|
||||
./templates/form-automation.sh https://example.com/form
|
||||
./templates/authenticated-session.sh https://app.example.com/login
|
||||
./templates/capture-workflow.sh https://example.com ./output
|
||||
```
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
# Authentication Patterns
|
||||
|
||||
Login flows, session persistence, OAuth, 2FA, and authenticated browsing.
|
||||
|
||||
**Related**: [session-management.md](session-management.md) for state persistence details, [SKILL.md](../SKILL.md) for quick start.
|
||||
|
||||
## Contents
|
||||
|
||||
- [Basic Login Flow](#basic-login-flow)
|
||||
- [Saving Authentication State](#saving-authentication-state)
|
||||
- [Restoring Authentication](#restoring-authentication)
|
||||
- [OAuth / SSO Flows](#oauth--sso-flows)
|
||||
- [Two-Factor Authentication](#two-factor-authentication)
|
||||
- [HTTP Basic Auth](#http-basic-auth)
|
||||
- [Cookie-Based Auth](#cookie-based-auth)
|
||||
- [Token Refresh Handling](#token-refresh-handling)
|
||||
- [Security Best Practices](#security-best-practices)
|
||||
|
||||
## Basic Login Flow
|
||||
|
||||
```bash
|
||||
# Navigate to login page
|
||||
agent-browser open https://app.example.com/login
|
||||
agent-browser wait --load networkidle
|
||||
|
||||
# Get form elements
|
||||
agent-browser snapshot -i
|
||||
# Output: @e1 [input type="email"], @e2 [input type="password"], @e3 [button] "Sign In"
|
||||
|
||||
# Fill credentials
|
||||
agent-browser fill @e1 "user@example.com"
|
||||
agent-browser fill @e2 "password123"
|
||||
|
||||
# Submit
|
||||
agent-browser click @e3
|
||||
agent-browser wait --load networkidle
|
||||
|
||||
# Verify login succeeded
|
||||
agent-browser get url # Should be dashboard, not login
|
||||
```
|
||||
|
||||
## Saving Authentication State
|
||||
|
||||
After logging in, save state for reuse:
|
||||
|
||||
```bash
|
||||
# Login first (see above)
|
||||
agent-browser open https://app.example.com/login
|
||||
agent-browser snapshot -i
|
||||
agent-browser fill @e1 "user@example.com"
|
||||
agent-browser fill @e2 "password123"
|
||||
agent-browser click @e3
|
||||
agent-browser wait --url "**/dashboard"
|
||||
|
||||
# Save authenticated state
|
||||
agent-browser state save ./auth-state.json
|
||||
```
|
||||
|
||||
## Restoring Authentication
|
||||
|
||||
Skip login by loading saved state:
|
||||
|
||||
```bash
|
||||
# Load saved auth state
|
||||
agent-browser state load ./auth-state.json
|
||||
|
||||
# Navigate directly to protected page
|
||||
agent-browser open https://app.example.com/dashboard
|
||||
|
||||
# Verify authenticated
|
||||
agent-browser snapshot -i
|
||||
```
|
||||
|
||||
## OAuth / SSO Flows
|
||||
|
||||
For OAuth redirects:
|
||||
|
||||
```bash
|
||||
# Start OAuth flow
|
||||
agent-browser open https://app.example.com/auth/google
|
||||
|
||||
# Handle redirects automatically
|
||||
agent-browser wait --url "**/accounts.google.com**"
|
||||
agent-browser snapshot -i
|
||||
|
||||
# Fill Google credentials
|
||||
agent-browser fill @e1 "user@gmail.com"
|
||||
agent-browser click @e2 # Next button
|
||||
agent-browser wait 2000
|
||||
agent-browser snapshot -i
|
||||
agent-browser fill @e3 "password"
|
||||
agent-browser click @e4 # Sign in
|
||||
|
||||
# Wait for redirect back
|
||||
agent-browser wait --url "**/app.example.com**"
|
||||
agent-browser state save ./oauth-state.json
|
||||
```
|
||||
|
||||
## Two-Factor Authentication
|
||||
|
||||
Handle 2FA with manual intervention:
|
||||
|
||||
```bash
|
||||
# Login with credentials
|
||||
agent-browser open https://app.example.com/login --headed # Show browser
|
||||
agent-browser snapshot -i
|
||||
agent-browser fill @e1 "user@example.com"
|
||||
agent-browser fill @e2 "password123"
|
||||
agent-browser click @e3
|
||||
|
||||
# Wait for user to complete 2FA manually
|
||||
echo "Complete 2FA in the browser window..."
|
||||
agent-browser wait --url "**/dashboard" --timeout 120000
|
||||
|
||||
# Save state after 2FA
|
||||
agent-browser state save ./2fa-state.json
|
||||
```
|
||||
|
||||
## HTTP Basic Auth
|
||||
|
||||
For sites using HTTP Basic Authentication:
|
||||
|
||||
```bash
|
||||
# Set credentials before navigation
|
||||
agent-browser set credentials username password
|
||||
|
||||
# Navigate to protected resource
|
||||
agent-browser open https://protected.example.com/api
|
||||
```
|
||||
|
||||
## Cookie-Based Auth
|
||||
|
||||
Manually set authentication cookies:
|
||||
|
||||
```bash
|
||||
# Set auth cookie
|
||||
agent-browser cookies set session_token "abc123xyz"
|
||||
|
||||
# Navigate to protected page
|
||||
agent-browser open https://app.example.com/dashboard
|
||||
```
|
||||
|
||||
## Token Refresh Handling
|
||||
|
||||
For sessions with expiring tokens:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Wrapper that handles token refresh
|
||||
|
||||
STATE_FILE="./auth-state.json"
|
||||
|
||||
# Try loading existing state
|
||||
if [[ -f "$STATE_FILE" ]]; then
|
||||
agent-browser state load "$STATE_FILE"
|
||||
agent-browser open https://app.example.com/dashboard
|
||||
|
||||
# Check if session is still valid
|
||||
URL=$(agent-browser get url)
|
||||
if [[ "$URL" == *"/login"* ]]; then
|
||||
echo "Session expired, re-authenticating..."
|
||||
# Perform fresh login
|
||||
agent-browser snapshot -i
|
||||
agent-browser fill @e1 "$USERNAME"
|
||||
agent-browser fill @e2 "$PASSWORD"
|
||||
agent-browser click @e3
|
||||
agent-browser wait --url "**/dashboard"
|
||||
agent-browser state save "$STATE_FILE"
|
||||
fi
|
||||
else
|
||||
# First-time login
|
||||
agent-browser open https://app.example.com/login
|
||||
# ... login flow ...
|
||||
fi
|
||||
```
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
1. **Never commit state files** - They contain session tokens
|
||||
```bash
|
||||
echo "*.auth-state.json" >> .gitignore
|
||||
```
|
||||
|
||||
2. **Use environment variables for credentials**
|
||||
```bash
|
||||
agent-browser fill @e1 "$APP_USERNAME"
|
||||
agent-browser fill @e2 "$APP_PASSWORD"
|
||||
```
|
||||
|
||||
3. **Clean up after automation**
|
||||
```bash
|
||||
agent-browser cookies clear
|
||||
rm -f ./auth-state.json
|
||||
```
|
||||
|
||||
4. **Use short-lived sessions for CI/CD**
|
||||
```bash
|
||||
# Don't persist state in CI
|
||||
agent-browser open https://app.example.com/login
|
||||
# ... login and perform actions ...
|
||||
agent-browser close # Session ends, nothing persisted
|
||||
```
|
||||
@@ -0,0 +1,263 @@
|
||||
# Command Reference
|
||||
|
||||
Complete reference for all agent-browser commands. For quick start and common patterns, see SKILL.md.
|
||||
|
||||
## Navigation
|
||||
|
||||
```bash
|
||||
agent-browser open <url> # Navigate to URL (aliases: goto, navigate)
|
||||
# Supports: https://, http://, file://, about:, data://
|
||||
# Auto-prepends https:// if no protocol given
|
||||
agent-browser back # Go back
|
||||
agent-browser forward # Go forward
|
||||
agent-browser reload # Reload page
|
||||
agent-browser close # Close browser (aliases: quit, exit)
|
||||
agent-browser connect 9222 # Connect to browser via CDP port
|
||||
```
|
||||
|
||||
## Snapshot (page analysis)
|
||||
|
||||
```bash
|
||||
agent-browser snapshot # Full accessibility tree
|
||||
agent-browser snapshot -i # Interactive elements only (recommended)
|
||||
agent-browser snapshot -c # Compact output
|
||||
agent-browser snapshot -d 3 # Limit depth to 3
|
||||
agent-browser snapshot -s "#main" # Scope to CSS selector
|
||||
```
|
||||
|
||||
## Interactions (use @refs from snapshot)
|
||||
|
||||
```bash
|
||||
agent-browser click @e1 # Click
|
||||
agent-browser click @e1 --new-tab # Click and open in new tab
|
||||
agent-browser dblclick @e1 # Double-click
|
||||
agent-browser focus @e1 # Focus element
|
||||
agent-browser fill @e2 "text" # Clear and type
|
||||
agent-browser type @e2 "text" # Type without clearing
|
||||
agent-browser press Enter # Press key (alias: key)
|
||||
agent-browser press Control+a # Key combination
|
||||
agent-browser keydown Shift # Hold key down
|
||||
agent-browser keyup Shift # Release key
|
||||
agent-browser hover @e1 # Hover
|
||||
agent-browser check @e1 # Check checkbox
|
||||
agent-browser uncheck @e1 # Uncheck checkbox
|
||||
agent-browser select @e1 "value" # Select dropdown option
|
||||
agent-browser select @e1 "a" "b" # Select multiple options
|
||||
agent-browser scroll down 500 # Scroll page (default: down 300px)
|
||||
agent-browser scrollintoview @e1 # Scroll element into view (alias: scrollinto)
|
||||
agent-browser drag @e1 @e2 # Drag and drop
|
||||
agent-browser upload @e1 file.pdf # Upload files
|
||||
```
|
||||
|
||||
## Get Information
|
||||
|
||||
```bash
|
||||
agent-browser get text @e1 # Get element text
|
||||
agent-browser get html @e1 # Get innerHTML
|
||||
agent-browser get value @e1 # Get input value
|
||||
agent-browser get attr @e1 href # Get attribute
|
||||
agent-browser get title # Get page title
|
||||
agent-browser get url # Get current URL
|
||||
agent-browser get count ".item" # Count matching elements
|
||||
agent-browser get box @e1 # Get bounding box
|
||||
agent-browser get styles @e1 # Get computed styles (font, color, bg, etc.)
|
||||
```
|
||||
|
||||
## Check State
|
||||
|
||||
```bash
|
||||
agent-browser is visible @e1 # Check if visible
|
||||
agent-browser is enabled @e1 # Check if enabled
|
||||
agent-browser is checked @e1 # Check if checked
|
||||
```
|
||||
|
||||
## Screenshots and PDF
|
||||
|
||||
```bash
|
||||
agent-browser screenshot # Save to temporary directory
|
||||
agent-browser screenshot path.png # Save to specific path
|
||||
agent-browser screenshot --full # Full page
|
||||
agent-browser pdf output.pdf # Save as PDF
|
||||
```
|
||||
|
||||
## Video Recording
|
||||
|
||||
```bash
|
||||
agent-browser record start ./demo.webm # Start recording
|
||||
agent-browser click @e1 # Perform actions
|
||||
agent-browser record stop # Stop and save video
|
||||
agent-browser record restart ./take2.webm # Stop current + start new
|
||||
```
|
||||
|
||||
## Wait
|
||||
|
||||
```bash
|
||||
agent-browser wait @e1 # Wait for element
|
||||
agent-browser wait 2000 # Wait milliseconds
|
||||
agent-browser wait --text "Success" # Wait for text (or -t)
|
||||
agent-browser wait --url "**/dashboard" # Wait for URL pattern (or -u)
|
||||
agent-browser wait --load networkidle # Wait for network idle (or -l)
|
||||
agent-browser wait --fn "window.ready" # Wait for JS condition (or -f)
|
||||
```
|
||||
|
||||
## Mouse Control
|
||||
|
||||
```bash
|
||||
agent-browser mouse move 100 200 # Move mouse
|
||||
agent-browser mouse down left # Press button
|
||||
agent-browser mouse up left # Release button
|
||||
agent-browser mouse wheel 100 # Scroll wheel
|
||||
```
|
||||
|
||||
## Semantic Locators (alternative to refs)
|
||||
|
||||
```bash
|
||||
agent-browser find role button click --name "Submit"
|
||||
agent-browser find text "Sign In" click
|
||||
agent-browser find text "Sign In" click --exact # Exact match only
|
||||
agent-browser find label "Email" fill "user@test.com"
|
||||
agent-browser find placeholder "Search" type "query"
|
||||
agent-browser find alt "Logo" click
|
||||
agent-browser find title "Close" click
|
||||
agent-browser find testid "submit-btn" click
|
||||
agent-browser find first ".item" click
|
||||
agent-browser find last ".item" click
|
||||
agent-browser find nth 2 "a" hover
|
||||
```
|
||||
|
||||
## Browser Settings
|
||||
|
||||
```bash
|
||||
agent-browser set viewport 1920 1080 # Set viewport size
|
||||
agent-browser set device "iPhone 14" # Emulate device
|
||||
agent-browser set geo 37.7749 -122.4194 # Set geolocation (alias: geolocation)
|
||||
agent-browser set offline on # Toggle offline mode
|
||||
agent-browser set headers '{"X-Key":"v"}' # Extra HTTP headers
|
||||
agent-browser set credentials user pass # HTTP basic auth (alias: auth)
|
||||
agent-browser set media dark # Emulate color scheme
|
||||
agent-browser set media light reduced-motion # Light mode + reduced motion
|
||||
```
|
||||
|
||||
## Cookies and Storage
|
||||
|
||||
```bash
|
||||
agent-browser cookies # Get all cookies
|
||||
agent-browser cookies set name value # 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
|
||||
```
|
||||
|
||||
## Network
|
||||
|
||||
```bash
|
||||
agent-browser network route <url> # Intercept requests
|
||||
agent-browser network route <url> --abort # Block requests
|
||||
agent-browser network route <url> --body '{}' # Mock response
|
||||
agent-browser network unroute [url] # Remove routes
|
||||
agent-browser network requests # View tracked requests
|
||||
agent-browser network requests --filter api # Filter requests
|
||||
```
|
||||
|
||||
## Tabs and Windows
|
||||
|
||||
```bash
|
||||
agent-browser tab # List tabs
|
||||
agent-browser tab new [url] # New tab
|
||||
agent-browser tab 2 # Switch to tab by index
|
||||
agent-browser tab close # Close current tab
|
||||
agent-browser tab close 2 # Close tab by index
|
||||
agent-browser window new # New window
|
||||
```
|
||||
|
||||
## Frames
|
||||
|
||||
```bash
|
||||
agent-browser frame "#iframe" # Switch to iframe
|
||||
agent-browser frame main # Back to main frame
|
||||
```
|
||||
|
||||
## Dialogs
|
||||
|
||||
```bash
|
||||
agent-browser dialog accept [text] # Accept dialog
|
||||
agent-browser dialog dismiss # Dismiss dialog
|
||||
```
|
||||
|
||||
## JavaScript
|
||||
|
||||
```bash
|
||||
agent-browser eval "document.title" # Simple expressions only
|
||||
agent-browser eval -b "<base64>" # Any JavaScript (base64 encoded)
|
||||
agent-browser eval --stdin # Read script from stdin
|
||||
```
|
||||
|
||||
Use `-b`/`--base64` or `--stdin` for reliable execution. Shell escaping with nested quotes and special characters is error-prone.
|
||||
|
||||
```bash
|
||||
# Base64 encode your script, then:
|
||||
agent-browser eval -b "ZG9jdW1lbnQucXVlcnlTZWxlY3RvcignW3NyYyo9Il9uZXh0Il0nKQ=="
|
||||
|
||||
# Or use stdin with heredoc for multiline scripts:
|
||||
cat <<'EOF' | agent-browser eval --stdin
|
||||
const links = document.querySelectorAll('a');
|
||||
Array.from(links).map(a => a.href);
|
||||
EOF
|
||||
```
|
||||
|
||||
## State Management
|
||||
|
||||
```bash
|
||||
agent-browser state save auth.json # Save cookies, storage, auth state
|
||||
agent-browser state load auth.json # Restore saved state
|
||||
```
|
||||
|
||||
## Global Options
|
||||
|
||||
```bash
|
||||
agent-browser --session <name> ... # Isolated browser session
|
||||
agent-browser --json ... # JSON output for parsing
|
||||
agent-browser --headed ... # Show browser window (not headless)
|
||||
agent-browser --full ... # Full page screenshot (-f)
|
||||
agent-browser --cdp <port> ... # Connect via Chrome DevTools Protocol
|
||||
agent-browser -p <provider> ... # Cloud browser provider (--provider)
|
||||
agent-browser --proxy <url> ... # Use proxy server
|
||||
agent-browser --proxy-bypass <hosts> # Hosts to bypass proxy
|
||||
agent-browser --headers <json> ... # HTTP headers scoped to URL's origin
|
||||
agent-browser --executable-path <p> # Custom browser executable
|
||||
agent-browser --extension <path> ... # Load browser extension (repeatable)
|
||||
agent-browser --ignore-https-errors # Ignore SSL certificate errors
|
||||
agent-browser --help # Show help (-h)
|
||||
agent-browser --version # Show version (-V)
|
||||
agent-browser <command> --help # Show detailed help for a command
|
||||
```
|
||||
|
||||
## Debugging
|
||||
|
||||
```bash
|
||||
agent-browser --headed open example.com # Show browser window
|
||||
agent-browser --cdp 9222 snapshot # Connect via CDP port
|
||||
agent-browser connect 9222 # Alternative: connect command
|
||||
agent-browser console # View console messages
|
||||
agent-browser console --clear # Clear console
|
||||
agent-browser errors # View page errors
|
||||
agent-browser errors --clear # Clear errors
|
||||
agent-browser highlight @e1 # Highlight element
|
||||
agent-browser trace start # Start recording trace
|
||||
agent-browser trace stop trace.zip # Stop and save trace
|
||||
agent-browser profiler start # Start Chrome DevTools profiling
|
||||
agent-browser profiler stop trace.json # Stop and save profile
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
```bash
|
||||
AGENT_BROWSER_SESSION="mysession" # Default session name
|
||||
AGENT_BROWSER_EXECUTABLE_PATH="/path/chrome" # Custom browser path
|
||||
AGENT_BROWSER_EXTENSIONS="/ext1,/ext2" # Comma-separated extension paths
|
||||
AGENT_BROWSER_PROVIDER="browserbase" # Cloud browser provider
|
||||
AGENT_BROWSER_STREAM_PORT="9223" # WebSocket streaming port
|
||||
AGENT_BROWSER_HOME="/path/to/agent-browser" # Custom install location
|
||||
```
|
||||
@@ -0,0 +1,120 @@
|
||||
# Profiling
|
||||
|
||||
Capture Chrome DevTools performance profiles during browser automation for performance analysis.
|
||||
|
||||
**Related**: [commands.md](commands.md) for full command reference, [SKILL.md](../SKILL.md) for quick start.
|
||||
|
||||
## Contents
|
||||
|
||||
- [Basic Profiling](#basic-profiling)
|
||||
- [Profiler Commands](#profiler-commands)
|
||||
- [Categories](#categories)
|
||||
- [Use Cases](#use-cases)
|
||||
- [Output Format](#output-format)
|
||||
- [Viewing Profiles](#viewing-profiles)
|
||||
- [Limitations](#limitations)
|
||||
|
||||
## Basic Profiling
|
||||
|
||||
```bash
|
||||
# Start profiling
|
||||
agent-browser profiler start
|
||||
|
||||
# Perform actions
|
||||
agent-browser navigate https://example.com
|
||||
agent-browser click "#button"
|
||||
agent-browser wait 1000
|
||||
|
||||
# Stop and save
|
||||
agent-browser profiler stop ./trace.json
|
||||
```
|
||||
|
||||
## Profiler Commands
|
||||
|
||||
```bash
|
||||
# Start profiling with default categories
|
||||
agent-browser profiler start
|
||||
|
||||
# Start with custom trace categories
|
||||
agent-browser profiler start --categories "devtools.timeline,v8.execute,blink.user_timing"
|
||||
|
||||
# Stop profiling and save to file
|
||||
agent-browser profiler stop ./trace.json
|
||||
```
|
||||
|
||||
## Categories
|
||||
|
||||
The `--categories` flag accepts a comma-separated list of Chrome trace categories. Default categories include:
|
||||
|
||||
- `devtools.timeline` -- standard DevTools performance traces
|
||||
- `v8.execute` -- time spent running JavaScript
|
||||
- `blink` -- renderer events
|
||||
- `blink.user_timing` -- `performance.mark()` / `performance.measure()` calls
|
||||
- `latencyInfo` -- input-to-latency tracking
|
||||
- `renderer.scheduler` -- task scheduling and execution
|
||||
- `toplevel` -- broad-spectrum basic events
|
||||
|
||||
Several `disabled-by-default-*` categories are also included for detailed timeline, call stack, and V8 CPU profiling data.
|
||||
|
||||
## Use Cases
|
||||
|
||||
### Diagnosing Slow Page Loads
|
||||
|
||||
```bash
|
||||
agent-browser profiler start
|
||||
agent-browser navigate https://app.example.com
|
||||
agent-browser wait --load networkidle
|
||||
agent-browser profiler stop ./page-load-profile.json
|
||||
```
|
||||
|
||||
### Profiling User Interactions
|
||||
|
||||
```bash
|
||||
agent-browser navigate https://app.example.com
|
||||
agent-browser profiler start
|
||||
agent-browser click "#submit"
|
||||
agent-browser wait 2000
|
||||
agent-browser profiler stop ./interaction-profile.json
|
||||
```
|
||||
|
||||
### CI Performance Regression Checks
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
agent-browser profiler start
|
||||
agent-browser navigate https://app.example.com
|
||||
agent-browser wait --load networkidle
|
||||
agent-browser profiler stop "./profiles/build-${BUILD_ID}.json"
|
||||
```
|
||||
|
||||
## 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, ... },
|
||||
...
|
||||
],
|
||||
"metadata": {
|
||||
"clock-domain": "LINUX_CLOCK_MONOTONIC"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `metadata.clock-domain` field is set based on the host platform (Linux or macOS). On Windows it is omitted.
|
||||
|
||||
## Viewing Profiles
|
||||
|
||||
Load the output JSON file in any of these tools:
|
||||
|
||||
- **Chrome DevTools**: Performance panel > Load profile (Ctrl+Shift+I > Performance)
|
||||
- **Perfetto UI**: https://ui.perfetto.dev/ -- drag and drop the JSON file
|
||||
- **Trace Viewer**: `chrome://tracing` in any Chromium browser
|
||||
|
||||
## 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.
|
||||
@@ -0,0 +1,194 @@
|
||||
# Proxy Support
|
||||
|
||||
Proxy configuration for geo-testing, rate limiting avoidance, and corporate environments.
|
||||
|
||||
**Related**: [commands.md](commands.md) for global options, [SKILL.md](../SKILL.md) for quick start.
|
||||
|
||||
## Contents
|
||||
|
||||
- [Basic Proxy Configuration](#basic-proxy-configuration)
|
||||
- [Authenticated Proxy](#authenticated-proxy)
|
||||
- [SOCKS Proxy](#socks-proxy)
|
||||
- [Proxy Bypass](#proxy-bypass)
|
||||
- [Common Use Cases](#common-use-cases)
|
||||
- [Verifying Proxy Connection](#verifying-proxy-connection)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Best Practices](#best-practices)
|
||||
|
||||
## Basic Proxy Configuration
|
||||
|
||||
Use the `--proxy` flag or set proxy via environment variable:
|
||||
|
||||
```bash
|
||||
# Via CLI flag
|
||||
agent-browser --proxy "http://proxy.example.com:8080" open https://example.com
|
||||
|
||||
# Via environment variable
|
||||
export HTTP_PROXY="http://proxy.example.com:8080"
|
||||
agent-browser open https://example.com
|
||||
|
||||
# HTTPS proxy
|
||||
export HTTPS_PROXY="https://proxy.example.com:8080"
|
||||
agent-browser open https://example.com
|
||||
|
||||
# Both
|
||||
export HTTP_PROXY="http://proxy.example.com:8080"
|
||||
export HTTPS_PROXY="http://proxy.example.com:8080"
|
||||
agent-browser open https://example.com
|
||||
```
|
||||
|
||||
## Authenticated Proxy
|
||||
|
||||
For proxies requiring authentication:
|
||||
|
||||
```bash
|
||||
# Include credentials in URL
|
||||
export HTTP_PROXY="http://username:password@proxy.example.com:8080"
|
||||
agent-browser open https://example.com
|
||||
```
|
||||
|
||||
## SOCKS Proxy
|
||||
|
||||
```bash
|
||||
# SOCKS5 proxy
|
||||
export ALL_PROXY="socks5://proxy.example.com:1080"
|
||||
agent-browser open https://example.com
|
||||
|
||||
# SOCKS5 with auth
|
||||
export ALL_PROXY="socks5://user:pass@proxy.example.com:1080"
|
||||
agent-browser open https://example.com
|
||||
```
|
||||
|
||||
## Proxy Bypass
|
||||
|
||||
Skip proxy for specific domains using `--proxy-bypass` or `NO_PROXY`:
|
||||
|
||||
```bash
|
||||
# Via CLI flag
|
||||
agent-browser --proxy "http://proxy.example.com:8080" --proxy-bypass "localhost,*.internal.com" open https://example.com
|
||||
|
||||
# Via environment variable
|
||||
export NO_PROXY="localhost,127.0.0.1,.internal.company.com"
|
||||
agent-browser open https://internal.company.com # Direct connection
|
||||
agent-browser open https://external.com # Via proxy
|
||||
```
|
||||
|
||||
## Common Use Cases
|
||||
|
||||
### Geo-Location Testing
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Test site from different regions using geo-located proxies
|
||||
|
||||
PROXIES=(
|
||||
"http://us-proxy.example.com:8080"
|
||||
"http://eu-proxy.example.com:8080"
|
||||
"http://asia-proxy.example.com:8080"
|
||||
)
|
||||
|
||||
for proxy in "${PROXIES[@]}"; do
|
||||
export HTTP_PROXY="$proxy"
|
||||
export HTTPS_PROXY="$proxy"
|
||||
|
||||
region=$(echo "$proxy" | grep -oP '^\w+-\w+')
|
||||
echo "Testing from: $region"
|
||||
|
||||
agent-browser --session "$region" open https://example.com
|
||||
agent-browser --session "$region" screenshot "./screenshots/$region.png"
|
||||
agent-browser --session "$region" close
|
||||
done
|
||||
```
|
||||
|
||||
### Rotating Proxies for Scraping
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Rotate through proxy list to avoid rate limiting
|
||||
|
||||
PROXY_LIST=(
|
||||
"http://proxy1.example.com:8080"
|
||||
"http://proxy2.example.com:8080"
|
||||
"http://proxy3.example.com:8080"
|
||||
)
|
||||
|
||||
URLS=(
|
||||
"https://site.com/page1"
|
||||
"https://site.com/page2"
|
||||
"https://site.com/page3"
|
||||
)
|
||||
|
||||
for i in "${!URLS[@]}"; do
|
||||
proxy_index=$((i % ${#PROXY_LIST[@]}))
|
||||
export HTTP_PROXY="${PROXY_LIST[$proxy_index]}"
|
||||
export HTTPS_PROXY="${PROXY_LIST[$proxy_index]}"
|
||||
|
||||
agent-browser open "${URLS[$i]}"
|
||||
agent-browser get text body > "output-$i.txt"
|
||||
agent-browser close
|
||||
|
||||
sleep 1 # Polite delay
|
||||
done
|
||||
```
|
||||
|
||||
### Corporate Network Access
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Access internal sites via corporate proxy
|
||||
|
||||
export HTTP_PROXY="http://corpproxy.company.com:8080"
|
||||
export HTTPS_PROXY="http://corpproxy.company.com:8080"
|
||||
export NO_PROXY="localhost,127.0.0.1,.company.com"
|
||||
|
||||
# External sites go through proxy
|
||||
agent-browser open https://external-vendor.com
|
||||
|
||||
# Internal sites bypass proxy
|
||||
agent-browser open https://intranet.company.com
|
||||
```
|
||||
|
||||
## Verifying Proxy Connection
|
||||
|
||||
```bash
|
||||
# Check your apparent IP
|
||||
agent-browser open https://httpbin.org/ip
|
||||
agent-browser get text body
|
||||
# Should show proxy's IP, not your real IP
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Proxy Connection Failed
|
||||
|
||||
```bash
|
||||
# Test proxy connectivity first
|
||||
curl -x http://proxy.example.com:8080 https://httpbin.org/ip
|
||||
|
||||
# Check if proxy requires auth
|
||||
export HTTP_PROXY="http://user:pass@proxy.example.com:8080"
|
||||
```
|
||||
|
||||
### SSL/TLS Errors Through Proxy
|
||||
|
||||
Some proxies perform SSL inspection. If you encounter certificate errors:
|
||||
|
||||
```bash
|
||||
# For testing only - not recommended for production
|
||||
agent-browser open https://example.com --ignore-https-errors
|
||||
```
|
||||
|
||||
### Slow Performance
|
||||
|
||||
```bash
|
||||
# Use proxy only when necessary
|
||||
export NO_PROXY="*.cdn.com,*.static.com" # Direct CDN access
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use environment variables** - Don't hardcode proxy credentials
|
||||
2. **Set NO_PROXY appropriately** - Avoid routing local traffic through proxy
|
||||
3. **Test proxy before automation** - Verify connectivity with simple requests
|
||||
4. **Handle proxy failures gracefully** - Implement retry logic for unstable proxies
|
||||
5. **Rotate proxies for large scraping jobs** - Distribute load and avoid bans
|
||||
@@ -0,0 +1,193 @@
|
||||
# Session Management
|
||||
|
||||
Multiple isolated browser sessions with state persistence and concurrent browsing.
|
||||
|
||||
**Related**: [authentication.md](authentication.md) for login patterns, [SKILL.md](../SKILL.md) for quick start.
|
||||
|
||||
## Contents
|
||||
|
||||
- [Named Sessions](#named-sessions)
|
||||
- [Session Isolation Properties](#session-isolation-properties)
|
||||
- [Session State Persistence](#session-state-persistence)
|
||||
- [Common Patterns](#common-patterns)
|
||||
- [Default Session](#default-session)
|
||||
- [Session Cleanup](#session-cleanup)
|
||||
- [Best Practices](#best-practices)
|
||||
|
||||
## Named Sessions
|
||||
|
||||
Use `--session` flag to isolate browser contexts:
|
||||
|
||||
```bash
|
||||
# Session 1: Authentication flow
|
||||
agent-browser --session auth open https://app.example.com/login
|
||||
|
||||
# Session 2: Public browsing (separate cookies, storage)
|
||||
agent-browser --session public open https://example.com
|
||||
|
||||
# Commands are isolated by session
|
||||
agent-browser --session auth fill @e1 "user@example.com"
|
||||
agent-browser --session public get text body
|
||||
```
|
||||
|
||||
## Session Isolation Properties
|
||||
|
||||
Each session has independent:
|
||||
- Cookies
|
||||
- LocalStorage / SessionStorage
|
||||
- IndexedDB
|
||||
- Cache
|
||||
- Browsing history
|
||||
- Open tabs
|
||||
|
||||
## Session State Persistence
|
||||
|
||||
### Save Session State
|
||||
|
||||
```bash
|
||||
# Save cookies, storage, and auth state
|
||||
agent-browser state save /path/to/auth-state.json
|
||||
```
|
||||
|
||||
### Load Session State
|
||||
|
||||
```bash
|
||||
# Restore saved state
|
||||
agent-browser state load /path/to/auth-state.json
|
||||
|
||||
# Continue with authenticated session
|
||||
agent-browser open https://app.example.com/dashboard
|
||||
```
|
||||
|
||||
### State File Contents
|
||||
|
||||
```json
|
||||
{
|
||||
"cookies": [...],
|
||||
"localStorage": {...},
|
||||
"sessionStorage": {...},
|
||||
"origins": [...]
|
||||
}
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Authenticated Session Reuse
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Save login state once, reuse many times
|
||||
|
||||
STATE_FILE="/tmp/auth-state.json"
|
||||
|
||||
# Check if we have saved state
|
||||
if [[ -f "$STATE_FILE" ]]; then
|
||||
agent-browser state load "$STATE_FILE"
|
||||
agent-browser open https://app.example.com/dashboard
|
||||
else
|
||||
# Perform login
|
||||
agent-browser open https://app.example.com/login
|
||||
agent-browser snapshot -i
|
||||
agent-browser fill @e1 "$USERNAME"
|
||||
agent-browser fill @e2 "$PASSWORD"
|
||||
agent-browser click @e3
|
||||
agent-browser wait --load networkidle
|
||||
|
||||
# Save for future use
|
||||
agent-browser state save "$STATE_FILE"
|
||||
fi
|
||||
```
|
||||
|
||||
### Concurrent Scraping
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Scrape multiple sites concurrently
|
||||
|
||||
# Start all sessions
|
||||
agent-browser --session site1 open https://site1.com &
|
||||
agent-browser --session site2 open https://site2.com &
|
||||
agent-browser --session site3 open https://site3.com &
|
||||
wait
|
||||
|
||||
# Extract from each
|
||||
agent-browser --session site1 get text body > site1.txt
|
||||
agent-browser --session site2 get text body > site2.txt
|
||||
agent-browser --session site3 get text body > site3.txt
|
||||
|
||||
# Cleanup
|
||||
agent-browser --session site1 close
|
||||
agent-browser --session site2 close
|
||||
agent-browser --session site3 close
|
||||
```
|
||||
|
||||
### A/B Testing Sessions
|
||||
|
||||
```bash
|
||||
# Test different user experiences
|
||||
agent-browser --session variant-a open "https://app.com?variant=a"
|
||||
agent-browser --session variant-b open "https://app.com?variant=b"
|
||||
|
||||
# Compare
|
||||
agent-browser --session variant-a screenshot /tmp/variant-a.png
|
||||
agent-browser --session variant-b screenshot /tmp/variant-b.png
|
||||
```
|
||||
|
||||
## Default Session
|
||||
|
||||
When `--session` is omitted, commands use the default session:
|
||||
|
||||
```bash
|
||||
# These use the same default session
|
||||
agent-browser open https://example.com
|
||||
agent-browser snapshot -i
|
||||
agent-browser close # Closes default session
|
||||
```
|
||||
|
||||
## Session Cleanup
|
||||
|
||||
```bash
|
||||
# Close specific session
|
||||
agent-browser --session auth close
|
||||
|
||||
# List active sessions
|
||||
agent-browser session list
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Name Sessions Semantically
|
||||
|
||||
```bash
|
||||
# GOOD: Clear purpose
|
||||
agent-browser --session github-auth open https://github.com
|
||||
agent-browser --session docs-scrape open https://docs.example.com
|
||||
|
||||
# AVOID: Generic names
|
||||
agent-browser --session s1 open https://github.com
|
||||
```
|
||||
|
||||
### 2. Always Clean Up
|
||||
|
||||
```bash
|
||||
# Close sessions when done
|
||||
agent-browser --session auth close
|
||||
agent-browser --session scrape close
|
||||
```
|
||||
|
||||
### 3. Handle State Files Securely
|
||||
|
||||
```bash
|
||||
# Don't commit state files (contain auth tokens!)
|
||||
echo "*.auth-state.json" >> .gitignore
|
||||
|
||||
# Delete after use
|
||||
rm /tmp/auth-state.json
|
||||
```
|
||||
|
||||
### 4. Timeout Long Sessions
|
||||
|
||||
```bash
|
||||
# Set timeout for automated scripts
|
||||
timeout 60 agent-browser --session long-task get text body
|
||||
```
|
||||
@@ -0,0 +1,194 @@
|
||||
# Snapshot and Refs
|
||||
|
||||
Compact element references that reduce context usage dramatically for AI agents.
|
||||
|
||||
**Related**: [commands.md](commands.md) for full command reference, [SKILL.md](../SKILL.md) for quick start.
|
||||
|
||||
## Contents
|
||||
|
||||
- [How Refs Work](#how-refs-work)
|
||||
- [Snapshot Command](#the-snapshot-command)
|
||||
- [Using Refs](#using-refs)
|
||||
- [Ref Lifecycle](#ref-lifecycle)
|
||||
- [Best Practices](#best-practices)
|
||||
- [Ref Notation Details](#ref-notation-details)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
|
||||
## How Refs Work
|
||||
|
||||
Traditional approach:
|
||||
```
|
||||
Full DOM/HTML → AI parses → CSS selector → Action (~3000-5000 tokens)
|
||||
```
|
||||
|
||||
agent-browser approach:
|
||||
```
|
||||
Compact snapshot → @refs assigned → Direct interaction (~200-400 tokens)
|
||||
```
|
||||
|
||||
## The Snapshot Command
|
||||
|
||||
```bash
|
||||
# Basic snapshot (shows page structure)
|
||||
agent-browser snapshot
|
||||
|
||||
# Interactive snapshot (-i flag) - RECOMMENDED
|
||||
agent-browser snapshot -i
|
||||
```
|
||||
|
||||
### Snapshot Output Format
|
||||
|
||||
```
|
||||
Page: Example Site - Home
|
||||
URL: https://example.com
|
||||
|
||||
@e1 [header]
|
||||
@e2 [nav]
|
||||
@e3 [a] "Home"
|
||||
@e4 [a] "Products"
|
||||
@e5 [a] "About"
|
||||
@e6 [button] "Sign In"
|
||||
|
||||
@e7 [main]
|
||||
@e8 [h1] "Welcome"
|
||||
@e9 [form]
|
||||
@e10 [input type="email"] placeholder="Email"
|
||||
@e11 [input type="password"] placeholder="Password"
|
||||
@e12 [button type="submit"] "Log In"
|
||||
|
||||
@e13 [footer]
|
||||
@e14 [a] "Privacy Policy"
|
||||
```
|
||||
|
||||
## Using Refs
|
||||
|
||||
Once you have refs, interact directly:
|
||||
|
||||
```bash
|
||||
# Click the "Sign In" button
|
||||
agent-browser click @e6
|
||||
|
||||
# Fill email input
|
||||
agent-browser fill @e10 "user@example.com"
|
||||
|
||||
# Fill password
|
||||
agent-browser fill @e11 "password123"
|
||||
|
||||
# Submit the form
|
||||
agent-browser click @e12
|
||||
```
|
||||
|
||||
## Ref Lifecycle
|
||||
|
||||
**IMPORTANT**: Refs are invalidated when the page changes!
|
||||
|
||||
```bash
|
||||
# Get initial snapshot
|
||||
agent-browser snapshot -i
|
||||
# @e1 [button] "Next"
|
||||
|
||||
# Click triggers page change
|
||||
agent-browser click @e1
|
||||
|
||||
# MUST re-snapshot to get new refs!
|
||||
agent-browser snapshot -i
|
||||
# @e1 [h1] "Page 2" ← Different element now!
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Always Snapshot Before Interacting
|
||||
|
||||
```bash
|
||||
# CORRECT
|
||||
agent-browser open https://example.com
|
||||
agent-browser snapshot -i # Get refs first
|
||||
agent-browser click @e1 # Use ref
|
||||
|
||||
# WRONG
|
||||
agent-browser open https://example.com
|
||||
agent-browser click @e1 # Ref doesn't exist yet!
|
||||
```
|
||||
|
||||
### 2. Re-Snapshot After Navigation
|
||||
|
||||
```bash
|
||||
agent-browser click @e5 # Navigates to new page
|
||||
agent-browser snapshot -i # Get new refs
|
||||
agent-browser click @e1 # Use new refs
|
||||
```
|
||||
|
||||
### 3. Re-Snapshot After Dynamic Changes
|
||||
|
||||
```bash
|
||||
agent-browser click @e1 # Opens dropdown
|
||||
agent-browser snapshot -i # See dropdown items
|
||||
agent-browser click @e7 # Select item
|
||||
```
|
||||
|
||||
### 4. Snapshot Specific Regions
|
||||
|
||||
For complex pages, snapshot specific areas:
|
||||
|
||||
```bash
|
||||
# Snapshot just the form
|
||||
agent-browser snapshot @e9
|
||||
```
|
||||
|
||||
## Ref Notation Details
|
||||
|
||||
```
|
||||
@e1 [tag type="value"] "text content" placeholder="hint"
|
||||
│ │ │ │ │
|
||||
│ │ │ │ └─ Additional attributes
|
||||
│ │ │ └─ Visible text
|
||||
│ │ └─ Key attributes shown
|
||||
│ └─ HTML tag name
|
||||
└─ Unique ref ID
|
||||
```
|
||||
|
||||
### Common Patterns
|
||||
|
||||
```
|
||||
@e1 [button] "Submit" # Button with text
|
||||
@e2 [input type="email"] # Email input
|
||||
@e3 [input type="password"] # Password input
|
||||
@e4 [a href="/page"] "Link Text" # Anchor link
|
||||
@e5 [select] # Dropdown
|
||||
@e6 [textarea] placeholder="Message" # Text area
|
||||
@e7 [div class="modal"] # Container (when relevant)
|
||||
@e8 [img alt="Logo"] # Image
|
||||
@e9 [checkbox] checked # Checked checkbox
|
||||
@e10 [radio] selected # Selected radio
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Ref not found" Error
|
||||
|
||||
```bash
|
||||
# Ref may have changed - re-snapshot
|
||||
agent-browser snapshot -i
|
||||
```
|
||||
|
||||
### Element Not Visible in Snapshot
|
||||
|
||||
```bash
|
||||
# Scroll down to reveal element
|
||||
agent-browser scroll down 1000
|
||||
agent-browser snapshot -i
|
||||
|
||||
# Or wait for dynamic content
|
||||
agent-browser wait 1000
|
||||
agent-browser snapshot -i
|
||||
```
|
||||
|
||||
### Too Many Elements
|
||||
|
||||
```bash
|
||||
# Snapshot specific container
|
||||
agent-browser snapshot @e5
|
||||
|
||||
# Or use get text for content-only extraction
|
||||
agent-browser get text @e5
|
||||
```
|
||||
@@ -0,0 +1,173 @@
|
||||
# Video Recording
|
||||
|
||||
Capture browser automation as video for debugging, documentation, or verification.
|
||||
|
||||
**Related**: [commands.md](commands.md) for full command reference, [SKILL.md](../SKILL.md) for quick start.
|
||||
|
||||
## Contents
|
||||
|
||||
- [Basic Recording](#basic-recording)
|
||||
- [Recording Commands](#recording-commands)
|
||||
- [Use Cases](#use-cases)
|
||||
- [Best Practices](#best-practices)
|
||||
- [Output Format](#output-format)
|
||||
- [Limitations](#limitations)
|
||||
|
||||
## Basic Recording
|
||||
|
||||
```bash
|
||||
# Start recording
|
||||
agent-browser record start ./demo.webm
|
||||
|
||||
# Perform actions
|
||||
agent-browser open https://example.com
|
||||
agent-browser snapshot -i
|
||||
agent-browser click @e1
|
||||
agent-browser fill @e2 "test input"
|
||||
|
||||
# Stop and save
|
||||
agent-browser record stop
|
||||
```
|
||||
|
||||
## Recording Commands
|
||||
|
||||
```bash
|
||||
# Start recording to file
|
||||
agent-browser record start ./output.webm
|
||||
|
||||
# Stop current recording
|
||||
agent-browser record stop
|
||||
|
||||
# Restart with new file (stops current + starts new)
|
||||
agent-browser record restart ./take2.webm
|
||||
```
|
||||
|
||||
## Use Cases
|
||||
|
||||
### Debugging Failed Automation
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Record automation for debugging
|
||||
|
||||
agent-browser record start ./debug-$(date +%Y%m%d-%H%M%S).webm
|
||||
|
||||
# Run your automation
|
||||
agent-browser open https://app.example.com
|
||||
agent-browser snapshot -i
|
||||
agent-browser click @e1 || {
|
||||
echo "Click failed - check recording"
|
||||
agent-browser record stop
|
||||
exit 1
|
||||
}
|
||||
|
||||
agent-browser record stop
|
||||
```
|
||||
|
||||
### Documentation Generation
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Record workflow for documentation
|
||||
|
||||
agent-browser record start ./docs/how-to-login.webm
|
||||
|
||||
agent-browser open https://app.example.com/login
|
||||
agent-browser wait 1000 # Pause for visibility
|
||||
|
||||
agent-browser snapshot -i
|
||||
agent-browser fill @e1 "demo@example.com"
|
||||
agent-browser wait 500
|
||||
|
||||
agent-browser fill @e2 "password"
|
||||
agent-browser wait 500
|
||||
|
||||
agent-browser click @e3
|
||||
agent-browser wait --load networkidle
|
||||
agent-browser wait 1000 # Show result
|
||||
|
||||
agent-browser record stop
|
||||
```
|
||||
|
||||
### CI/CD Test Evidence
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Record E2E test runs for CI artifacts
|
||||
|
||||
TEST_NAME="${1:-e2e-test}"
|
||||
RECORDING_DIR="./test-recordings"
|
||||
mkdir -p "$RECORDING_DIR"
|
||||
|
||||
agent-browser record start "$RECORDING_DIR/$TEST_NAME-$(date +%s).webm"
|
||||
|
||||
# Run test
|
||||
if run_e2e_test; then
|
||||
echo "Test passed"
|
||||
else
|
||||
echo "Test failed - recording saved"
|
||||
fi
|
||||
|
||||
agent-browser record stop
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Add Pauses for Clarity
|
||||
|
||||
```bash
|
||||
# Slow down for human viewing
|
||||
agent-browser click @e1
|
||||
agent-browser wait 500 # Let viewer see result
|
||||
```
|
||||
|
||||
### 2. Use Descriptive Filenames
|
||||
|
||||
```bash
|
||||
# Include context in filename
|
||||
agent-browser record start ./recordings/login-flow-2024-01-15.webm
|
||||
agent-browser record start ./recordings/checkout-test-run-42.webm
|
||||
```
|
||||
|
||||
### 3. Handle Recording in Error Cases
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
cleanup() {
|
||||
agent-browser record stop 2>/dev/null || true
|
||||
agent-browser close 2>/dev/null || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
agent-browser record start ./automation.webm
|
||||
# ... automation steps ...
|
||||
```
|
||||
|
||||
### 4. Combine with Screenshots
|
||||
|
||||
```bash
|
||||
# Record video AND capture key frames
|
||||
agent-browser record start ./flow.webm
|
||||
|
||||
agent-browser open https://example.com
|
||||
agent-browser screenshot ./screenshots/step1-homepage.png
|
||||
|
||||
agent-browser click @e1
|
||||
agent-browser screenshot ./screenshots/step2-after-click.png
|
||||
|
||||
agent-browser record stop
|
||||
```
|
||||
|
||||
## Output Format
|
||||
|
||||
- Default format: WebM (VP8/VP9 codec)
|
||||
- Compatible with all modern browsers and video players
|
||||
- Compressed but high quality
|
||||
|
||||
## Limitations
|
||||
|
||||
- Recording adds slight overhead to automation
|
||||
- Large recordings can consume significant disk space
|
||||
- Some headless environments may have codec limitations
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
#!/bin/bash
|
||||
# Template: Authenticated Session Workflow
|
||||
# Purpose: Login once, save state, reuse for subsequent runs
|
||||
# Usage: ./authenticated-session.sh <login-url> [state-file]
|
||||
#
|
||||
# RECOMMENDED: Use the auth vault instead of this template:
|
||||
# echo "<pass>" | agent-browser auth save myapp --url <login-url> --username <user> --password-stdin
|
||||
# agent-browser auth login myapp
|
||||
# The auth vault stores credentials securely and the LLM never sees passwords.
|
||||
#
|
||||
# Environment variables:
|
||||
# APP_USERNAME - Login username/email
|
||||
# APP_PASSWORD - Login password
|
||||
#
|
||||
# Two modes:
|
||||
# 1. Discovery mode (default): Shows form structure so you can identify refs
|
||||
# 2. Login mode: Performs actual login after you update the refs
|
||||
#
|
||||
# Setup steps:
|
||||
# 1. Run once to see form structure (discovery mode)
|
||||
# 2. Update refs in LOGIN FLOW section below
|
||||
# 3. Set APP_USERNAME and APP_PASSWORD
|
||||
# 4. Delete the DISCOVERY section
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
LOGIN_URL="${1:?Usage: $0 <login-url> [state-file]}"
|
||||
STATE_FILE="${2:-./auth-state.json}"
|
||||
|
||||
echo "Authentication workflow: $LOGIN_URL"
|
||||
|
||||
# ================================================================
|
||||
# SAVED STATE: Skip login if valid saved state exists
|
||||
# ================================================================
|
||||
if [[ -f "$STATE_FILE" ]]; then
|
||||
echo "Loading saved state from $STATE_FILE..."
|
||||
if agent-browser --state "$STATE_FILE" open "$LOGIN_URL" 2>/dev/null; then
|
||||
agent-browser wait --load networkidle
|
||||
|
||||
CURRENT_URL=$(agent-browser get url)
|
||||
if [[ "$CURRENT_URL" != *"login"* ]] && [[ "$CURRENT_URL" != *"signin"* ]]; then
|
||||
echo "Session restored successfully"
|
||||
agent-browser snapshot -i
|
||||
exit 0
|
||||
fi
|
||||
echo "Session expired, performing fresh login..."
|
||||
agent-browser close 2>/dev/null || true
|
||||
else
|
||||
echo "Failed to load state, re-authenticating..."
|
||||
fi
|
||||
rm -f "$STATE_FILE"
|
||||
fi
|
||||
|
||||
# ================================================================
|
||||
# DISCOVERY MODE: Shows form structure (delete after setup)
|
||||
# ================================================================
|
||||
echo "Opening login page..."
|
||||
agent-browser open "$LOGIN_URL"
|
||||
agent-browser wait --load networkidle
|
||||
|
||||
echo ""
|
||||
echo "Login form structure:"
|
||||
echo "---"
|
||||
agent-browser snapshot -i
|
||||
echo "---"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " 1. Note the refs: username=@e?, password=@e?, submit=@e?"
|
||||
echo " 2. Update the LOGIN FLOW section below with your refs"
|
||||
echo " 3. Set: export APP_USERNAME='...' APP_PASSWORD='...'"
|
||||
echo " 4. Delete this DISCOVERY MODE section"
|
||||
echo ""
|
||||
agent-browser close
|
||||
exit 0
|
||||
|
||||
# ================================================================
|
||||
# LOGIN FLOW: Uncomment and customize after discovery
|
||||
# ================================================================
|
||||
# : "${APP_USERNAME:?Set APP_USERNAME environment variable}"
|
||||
# : "${APP_PASSWORD:?Set APP_PASSWORD environment variable}"
|
||||
#
|
||||
# agent-browser open "$LOGIN_URL"
|
||||
# agent-browser wait --load networkidle
|
||||
# agent-browser snapshot -i
|
||||
#
|
||||
# # Fill credentials (update refs to match your form)
|
||||
# agent-browser fill @e1 "$APP_USERNAME"
|
||||
# agent-browser fill @e2 "$APP_PASSWORD"
|
||||
# agent-browser click @e3
|
||||
# agent-browser wait --load networkidle
|
||||
#
|
||||
# # Verify login succeeded
|
||||
# FINAL_URL=$(agent-browser get url)
|
||||
# if [[ "$FINAL_URL" == *"login"* ]] || [[ "$FINAL_URL" == *"signin"* ]]; then
|
||||
# echo "Login failed - still on login page"
|
||||
# agent-browser screenshot /tmp/login-failed.png
|
||||
# agent-browser close
|
||||
# exit 1
|
||||
# fi
|
||||
#
|
||||
# # Save state for future runs
|
||||
# echo "Saving state to $STATE_FILE"
|
||||
# agent-browser state save "$STATE_FILE"
|
||||
# echo "Login successful"
|
||||
# agent-browser snapshot -i
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
#!/bin/bash
|
||||
# Template: Content Capture Workflow
|
||||
# Purpose: Extract content from web pages (text, screenshots, PDF)
|
||||
# Usage: ./capture-workflow.sh <url> [output-dir]
|
||||
#
|
||||
# Outputs:
|
||||
# - page-full.png: Full page screenshot
|
||||
# - page-structure.txt: Page element structure with refs
|
||||
# - page-text.txt: All text content
|
||||
# - page.pdf: PDF version
|
||||
#
|
||||
# Optional: Load auth state for protected pages
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
TARGET_URL="${1:?Usage: $0 <url> [output-dir]}"
|
||||
OUTPUT_DIR="${2:-.}"
|
||||
|
||||
echo "Capturing: $TARGET_URL"
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
# Optional: Load authentication state
|
||||
# if [[ -f "./auth-state.json" ]]; then
|
||||
# echo "Loading authentication state..."
|
||||
# agent-browser state load "./auth-state.json"
|
||||
# fi
|
||||
|
||||
# Navigate to target
|
||||
agent-browser open "$TARGET_URL"
|
||||
agent-browser wait --load networkidle
|
||||
|
||||
# Get metadata
|
||||
TITLE=$(agent-browser get title)
|
||||
URL=$(agent-browser get url)
|
||||
echo "Title: $TITLE"
|
||||
echo "URL: $URL"
|
||||
|
||||
# Capture full page screenshot
|
||||
agent-browser screenshot --full "$OUTPUT_DIR/page-full.png"
|
||||
echo "Saved: $OUTPUT_DIR/page-full.png"
|
||||
|
||||
# Get page structure with refs
|
||||
agent-browser snapshot -i > "$OUTPUT_DIR/page-structure.txt"
|
||||
echo "Saved: $OUTPUT_DIR/page-structure.txt"
|
||||
|
||||
# Extract all text content
|
||||
agent-browser get text body > "$OUTPUT_DIR/page-text.txt"
|
||||
echo "Saved: $OUTPUT_DIR/page-text.txt"
|
||||
|
||||
# Save as PDF
|
||||
agent-browser pdf "$OUTPUT_DIR/page.pdf"
|
||||
echo "Saved: $OUTPUT_DIR/page.pdf"
|
||||
|
||||
# Optional: Extract specific elements using refs from structure
|
||||
# agent-browser get text @e5 > "$OUTPUT_DIR/main-content.txt"
|
||||
|
||||
# Optional: Handle infinite scroll pages
|
||||
# for i in {1..5}; do
|
||||
# agent-browser scroll down 1000
|
||||
# agent-browser wait 1000
|
||||
# done
|
||||
# agent-browser screenshot --full "$OUTPUT_DIR/page-scrolled.png"
|
||||
|
||||
# Cleanup
|
||||
agent-browser close
|
||||
|
||||
echo ""
|
||||
echo "Capture complete:"
|
||||
ls -la "$OUTPUT_DIR"
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
#!/bin/bash
|
||||
# Template: Form Automation Workflow
|
||||
# Purpose: Fill and submit web forms with validation
|
||||
# Usage: ./form-automation.sh <form-url>
|
||||
#
|
||||
# This template demonstrates the snapshot-interact-verify pattern:
|
||||
# 1. Navigate to form
|
||||
# 2. Snapshot to get element refs
|
||||
# 3. Fill fields using refs
|
||||
# 4. Submit and verify result
|
||||
#
|
||||
# Customize: Update the refs (@e1, @e2, etc.) based on your form's snapshot output
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
FORM_URL="${1:?Usage: $0 <form-url>}"
|
||||
|
||||
echo "Form automation: $FORM_URL"
|
||||
|
||||
# Step 1: Navigate to form
|
||||
agent-browser open "$FORM_URL"
|
||||
agent-browser wait --load networkidle
|
||||
|
||||
# Step 2: Snapshot to discover form elements
|
||||
echo ""
|
||||
echo "Form structure:"
|
||||
agent-browser snapshot -i
|
||||
|
||||
# Step 3: Fill form fields (customize these refs based on snapshot output)
|
||||
#
|
||||
# Common field types:
|
||||
# agent-browser fill @e1 "John Doe" # Text input
|
||||
# agent-browser fill @e2 "user@example.com" # Email input
|
||||
# agent-browser fill @e3 "SecureP@ss123" # Password input
|
||||
# agent-browser select @e4 "Option Value" # Dropdown
|
||||
# agent-browser check @e5 # Checkbox
|
||||
# agent-browser click @e6 # Radio button
|
||||
# agent-browser fill @e7 "Multi-line text" # Textarea
|
||||
# agent-browser upload @e8 /path/to/file.pdf # File upload
|
||||
#
|
||||
# Uncomment and modify:
|
||||
# agent-browser fill @e1 "Test User"
|
||||
# agent-browser fill @e2 "test@example.com"
|
||||
# agent-browser click @e3 # Submit button
|
||||
|
||||
# Step 4: Wait for submission
|
||||
# agent-browser wait --load networkidle
|
||||
# agent-browser wait --url "**/success" # Or wait for redirect
|
||||
|
||||
# Step 5: Verify result
|
||||
echo ""
|
||||
echo "Result:"
|
||||
agent-browser get url
|
||||
agent-browser snapshot -i
|
||||
|
||||
# Optional: Capture evidence
|
||||
agent-browser screenshot /tmp/form-result.png
|
||||
echo "Screenshot saved: /tmp/form-result.png"
|
||||
|
||||
# Cleanup
|
||||
agent-browser close
|
||||
echo "Done"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user