@naticha/bunwv
v0.1.2
Published
Headless browser automation CLI for Bun, powered by Bun.WebView
Maintainers
Readme
bunwv
Headless browser automation CLI for Bun, powered by Bun.WebView. Cross-platform: WebKit on macOS (default, zero dependencies), Chrome on macOS/Linux/Windows.
A persistent daemon keeps a browser instance alive so page state — DOM, modals, forms, auth, cookies — survives across commands. Designed agent-first: every action verb is silent on success, errors are JSON on stderr with stable exit codes, and event/console buffers are cursor-pulled. Built for AI coding assistants (Claude Code, Cursor, etc.) driving the browser through discrete tool calls.
Install
bun install -g @naticha/bunwvRequires Bun v1.3.14+. On macOS, uses the native WebKit engine by default (zero dependencies). On Linux and Windows, automatically uses Chrome/Chromium (must be installed).
AI Coding Assistant Skill
bunx skills add naticha/bunwv
# or
npx skills add naticha/bunwvOr install directly in Claude Code:
/plugin marketplace add naticha/bunwv
/plugin install bunwv@naticha/bunwvThis installs the skill file that teaches AI assistants how to use bunwv for browser testing.
Quick Start
bunwv start # start the daemon
bunwv navigate http://localhost:3000 # go to a page
bunwv screenshot # writes /tmp/bunwv-screenshot-<session>.jpg (JPEG @ q80), prints the path
bunwv screenshot --max-width 1024 # bound the longest side; aspect preserved
bunwv screenshot --metadata # {"width","height","format"} instead of pixels
bunwv click --selector "button.submit" # click an element (auto-waits)
bunwv type "hello world" # type into focused element
bunwv evaluate "document.title" # run JS in the page, JSON-literal result
bunwv close # stop the daemonAgent-first contract
- Successful action verbs print nothing on stdout and exit 0.
click,type,navigate,press,scroll,scroll-to,clear,submit,resize,back/forward/reload,close,exists,wait-for,wait-for-gone,cdp-subscribe,cdp-unsubscribeare all silent. Read verbs (status,evaluate,events,console,cdp,cdp-subscriptions,screenshot,sessions) print their result. - Stable exit codes:
0ok,1generic,2usage,3timeout,4element-not-found,5daemon-unreachable,6batch-partial. - Errors are JSON on stderr:
{ok:false, error, exitCode}. Branch on the exit code, not stderr text. - Error-level console auto-surfaces. If the page logs
console.error/console.warnwhile a verb runs,{"console":[…]}is written to stderr alongside the verb's response. --jsonglobal flag wraps any command's output as{ok, data?, error?, exitCode}.- Flexible flags:
--flag value,--flag=value, repeated flags (e.g.--mod Shift --mod Control), and flags before or after the command all work. BUNWV_SESSIONenv var replaces--session <name>when set.
Commands
Session
| Command | Description |
|---|---|
| start [--width N] [--height N] [--data-store PATH] [--idle-timeout ms] [--backend webkit\|chrome] [--chrome-path PATH] [--chrome-argv '[json]'] [--chrome-url <ws>] [--chrome-stdout inherit\|ignore] [--chrome-stderr inherit\|ignore] [--webkit-stdout inherit\|ignore] [--webkit-stderr inherit\|ignore] [--url <initial>] | Start the daemon (default 1920x1080, 30min idle timeout) |
| close [--all] | Stop this session, or every running session with --all |
| status | Terse: <url> \| <title> \| <idle\|loading> \| pending=<n>. --json for loading, pendingEvents, cursor, cdpSubscriptions |
| sessions | List all running sessions |
Navigation
| Command | Description |
|---|---|
| navigate <url> | Navigate to a URL (silent) |
| back / forward / reload | History + refresh (silent) |
Interaction
| Command | Description |
|---|---|
| click --selector <css> | Click an element by CSS selector (auto-waits for actionability, isTrusted: true) |
| click --text <text> | Click an element by visible text. --text-match exact\|contains\|regex (default: trimmed contains) |
| click --at <x,y> | Click at coordinates (no actionability wait) |
| click ... [--button left\|right\|middle] [--count 1\|2\|3] [--mod Shift] [--mod Control] [--mod Alt] [--mod Meta] [--timeout ms] | Modifiers, mouse button, click count, actionability timeout |
| exists <selector> | Silent probe. Exit 0 present, 4 missing |
| type <text> | Type text into the focused element |
| press <key> [--mod Shift] [--mod Control] ... | Press a key with optional modifiers (case-sensitive per Bun.WebView) |
| clear <selector> | Clear an input/textarea (React-compatible native setter) |
| submit [--form <sel>] [--button <text>] | Submit a form via requestSubmit() (React-compatible) |
| scroll <dx> <dy> | Scroll by wheel event |
| scroll-to <selector> [--block start\|center\|end\|nearest] [--timeout ms] | Scroll element into view |
Inspection
| Command | Description |
|---|---|
| screenshot [--format png\|jpeg\|webp\|avif\|heic] [--quality 0-100] [--max-width N] [--max-height N] [--placeholder \| --metadata] [--encoding blob\|buffer\|base64\|shmem] [--out <path>\|-] | Capture the viewport. Default: JPEG @ q80, writes /tmp/bunwv-screenshot-<session>.jpg and prints the path. --max-width/--max-height cap dimensions (aspect preserved, never upscales). --placeholder emits a blur-up data URL; --metadata emits {width,height,format} JSON. AVIF/HEIC encode is Apple-Silicon-only |
| image <input> [--out <path>\|-] [--format ...] [--quality N] [--resize WxH \| --max-width N \| --max-height N] [--rotate 90\|180\|270] [--flip] [--flop] [--metadata] [--placeholder] | Transform a local image via Bun.Image — no daemon required. Output format inferred from --out extension; defaults to jpeg. Default --out is the input with the new extension |
| evaluate <expr> | Evaluate JS in the page. Always prints the JSON-literal result (auto-wraps statements in an IIFE) |
| console [--clear] [--since <seq>] | Captured page console output. Terse: <seq> [<level>] <message>. \n/\r escaped. --json for raw messages + cursor |
| events [--since <seq>] | Navigation events + subscribed CDP events since the cursor. 1000 entries / 10 MB LRU cap |
| cdp <method> [--params '{}'] | Raw Chrome DevTools Protocol call (Chrome backend only) |
| cdp-subscribe <CDP.event> [<CDP.event> ...] | Subscribe one or more CDP events into the events buffer |
| cdp-unsubscribe <CDP.event> [<CDP.event> ...] | Unsubscribe |
| cdp-subscriptions | List active subscriptions |
| resize <w> <h> | Resize the viewport |
Waiting
| Command | Description |
|---|---|
| wait-for <selector> | Wait until element appears (default 10s) |
| wait-for --url <substring> / --title <substring> | Wait for URL or title to contain a substring |
| wait-for-gone <selector> \| --url <substr> \| --title <substr> | Symmetric removal wait |
| wait-for ... [--timeout ms] | Override the 10s default |
Batch
| Command | Description |
|---|---|
| batch [--file <path>] [--keep-going] | Read NDJSON from stdin (or a file), each line a JSON array of args. Runs all lines in one Bun process, emits one NDJSON envelope per command. Outer flags like --session inherit into each line |
All commands accept --json, --session <name> (or BUNWV_SESSION env var), and the flexible flag syntax.
Sessions
Sessions are named and isolated. Each runs its own daemon on a separate Unix socket. Sockets and PID files are chmod 0600, so other local users can't drive your session.
bunwv start # "default" session
bunwv start --session staging # separate "staging" session
BUNWV_SESSION=staging bunwv navigate http://staging:3000
bunwv sessions # list running sessions
bunwv close --session staging # stop one session
bunwv close --all # stop every running sessionAuto-shutdown — daemons exit after 30 minutes of inactivity. Override with --idle-timeout:
bunwv start --idle-timeout 3600000 # 1 hour
bunwv start --idle-timeout 0 # neverReuse detection — starting an existing session reports its current state and exits 0:
$ bunwv start
Reusing existing session "default" (PID: 12345)
URL: http://localhost:3000/dashboardPersistent auth — use --data-store to preserve cookies/localStorage across daemon restarts:
bunwv start --data-store ./bunwv-sessionWorking with React
Two commands are specifically designed for React apps:
clear — clears input fields using the native value setter and dispatches React-compatible events. Keyboard-based clearing (Cmd+A, Backspace) does not reliably update React state.
bunwv clear "input[name='email']"
bunwv click --selector "input[name='email']"
bunwv type "[email protected]"submit — submits forms via form.requestSubmit(), which properly triggers React form handlers. JS .click() produces isTrusted: false events that many React forms ignore.
bunwv submit --button "Save Changes"
bunwv wait-for-gone "[role='dialog']"Console Capture
Page console.log, console.error, etc. are captured into a cursor-based ring buffer (1000 entries). console.error/console.warn entries that fire during a verb are auto-surfaced to that verb's stderr as {"console":[…]} — the agent sees failures without a second call.
Pull the buffer explicitly:
bunwv console # terse: "<seq> [<level>] <message>"
bunwv console --clear # print, then clear
bunwv console --since 42 # only entries with seq > 42
bunwv --json console # {messages, cursor, truncated?, oldest?}Advance --since using the max seq you saw (first field of each line). Use --json when you need raw multi-line messages or the truncation signal.
Events & CDP
onNavigated, onNavigationFailed, and any subscribed CDP events land in a shared ring buffer (1000 entries / 10 MB LRU):
bunwv events --since 0 # full buffer
bunwv events --since 42 # new events onlyIf the buffer evicted older entries, the response includes "truncated":true,"oldest":<seq>.
Chrome backend & CDP
macOS defaults to WebKit; Linux/Windows auto-use Chrome. Override on any platform:
bunwv start --backend chrome
bunwv start --backend webkit # macOS only
bunwv start --chrome-path /path/to/chromium
bunwv start --chrome-argv '["--headless=new"]' # extra flags
bunwv start --chrome-url ws://127.0.0.1:9222/devtools/... # attach to a running ChromeRaw CDP calls and subscriptions (Chrome only):
bunwv cdp "Page.getLayoutMetrics"
bunwv cdp "Runtime.evaluate" --params '{"expression": "1+1"}'
bunwv cdp "Network.enable"
bunwv cdp-subscribe Network.responseReceived Network.requestWillBeSent
bunwv navigate https://example.com
bunwv events --since 0
bunwv cdp-unsubscribe Network.responseReceived Network.requestWillBeSentBatch mode
bunwv batch runs many commands in a single Bun process, eliminating per-command startup cost. Each stdin line is a JSON array of args; each response is an NDJSON envelope on stdout.
cat <<'EOF' | bunwv batch --session staging --keep-going
["navigate","http://localhost:3000/login"]
["click","--selector","input[name='email']"]
["type","[email protected]"]
["press","Tab"]
["type","hunter2"]
["submit","--button","Sign In"]
["wait-for","--url","/dashboard"]
["screenshot"]
EOF--keep-going runs the full list even if one line fails; the process exits 6 (batch-partial) on any failure, 0 on full success. Without --keep-going, batch stops at the first failure and returns that line's exit code.
How It Works
┌──────────┐ Unix Socket ┌───────────────┐ Bun.WebView ┌──────────────┐
│ bunwv │ ──── HTTP POST ────▶ │ daemon │ ─────── API ────▶ │ WebKit macOS │
│ CLI │ ◀─── JSON/bytes ──── │ (background) │ ◀──────────────── │ Chrome Linux │
└──────────┘ /tmp/bunwv-*.sock └───────────────┘ │ / Windows │
└──────────────┘- The daemon spawns on
bunwv startand listens on a Unix socket (owner-only,chmod 0600). - Each CLI command sends one HTTP request to the daemon and exits — no long-lived connections.
- The daemon owns a single
Bun.WebViewinstance. - All selector/coordinate input is dispatched as native events (
isTrusted: true); selector-based methods auto-wait for actionability (attached, visible, stable, unobscured). - Navigation and CDP events are buffered with monotonic
seqcursors so agents can poll for what's new since their last turn.
AI Assistant Integration
bunwv is designed for AI coding assistants that can't see a browser. The typical workflow:
- Navigate to a page
- Screenshot — the assistant Reads the PNG to "see" the page
- Decide what to do based on the screenshot
- Act —
click,type,submit - Wait —
wait-fora selector, URL, or title change - Screenshot again to verify
- Repeat
The Claude Code skill (skills/bunwv/SKILL.md) documents these patterns end-to-end, including batch mode, React form handling, error recovery via exit codes, and cursor-based event/console polling.
License
MIT
