Help us improve
Share bugs, ideas, or general feedback.
From bunwv
Headless browser testing via persistent Bun.WebView session. Automates UI testing: navigate, click, fill forms, take screenshots, verify page content.
npx claudepluginhub naticha/bunwv --plugin bunwvHow this skill is triggered — by the user, by Claude, or both
Slash command
/bunwv:bunwvThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Headless browser automation using a persistent WebView session. The daemon keeps a single WebView instance alive so page state (DOM, modals, forms, SPA routes, scroll position) persists across commands.
CLI for browser automation: navigate sites, snapshot elements for refs, fill forms, click buttons, screenshot, scrape data, test web apps. Chains commands, imports auth state.
Automates headless browser via agent-browser CLI: open/navigate sites, snapshot interactive elements for refs, click/fill forms, verify UI, scrape data, e2e test web apps.
Automates browser interactions via Chrome DevTools Protocol. Screenshots, clicks, types, navigates, reads page accessibility trees, extracts text, and executes JavaScript in web pages. Use when the user asks to interact with a website, test a web app, fill web forms, scrape web content, or automate browser tasks.
Share bugs, ideas, or general feedback.
Headless browser automation using a persistent WebView session. The daemon keeps a single WebView instance alive so page state (DOM, modals, forms, SPA routes, scroll position) persists across commands.
bunwv is designed for AI agents driving it via discrete tool calls. A few contracts to rely on:
click, type, navigate, press, scroll, scroll-to, clear, submit, resize, back/forward/reload, close, exists, wait-for, wait-for-gone, cdp-subscribe, cdp-unsubscribe all follow this. Read verbs (status, evaluate, events, console, cdp, cdp-subscriptions, screenshot, sessions) print their result.0 ok, 1 generic, 2 usage, 3 timeout, 4 element-not-found, 5 daemon-unreachable, 6 batch-partial (only in batch --keep-going).console.error/console.warn auto-surface during verbs. If the page logs an error while a verb runs, bunwv prints {"console":[…]} to stderr. You see the failure without a second call.events --since <seq> returns entries newer than the cursor plus a new cursor. Keep the cursor across turns; refetch after actions. If the buffer evicted older entries, the response includes "truncated":true,"oldest":<seq>.bunwv screenshot writes bytes to /tmp/bunwv-screenshot-<session>.jpg (JPEG @ q80) by default and prints the path on stdout. Use the Read tool on that path to see the image.--json for uniform envelopes. Any command with --json returns {ok, data?, error?, exitCode} as a single JSON line. Use it when you prefer one shape over terse output.--flag value, --flag=value, and repeated flags (e.g. --mod Shift --mod Control) all work. Flags may appear before or after the command: bunwv --json status and bunwv status --json are equivalent.BUNWV_SESSION env var — set it once and --session becomes optional.Run all commands with bunwv <command> (installed globally via bun install -g @naticha/bunwv).
bunwv start [--width N] [--height N] [--data-store PATH] [--idle-timeout ms]
[--backend webkit|chrome] [--chrome-path PATH] [--chrome-argv '[json]']
[--chrome-url <ws-url>] [--url <initial-url>]
bunwv navigate <url>
bunwv click --selector <css> | --text <text> | --at <x,y>
[--text-match exact|contains|regex] # default: contains (trimmed)
[--button left|right|middle] [--count 1|2|3]
[--mod Shift] [--mod Control] [--mod Alt] [--mod Meta]
[--timeout ms]
bunwv exists <selector> # silent; exit 0 if present, 4 if not
bunwv type <text>
bunwv press <key> [--mod Shift] [--mod Control] ...
bunwv clear <selector>
bunwv submit [--form <selector>] [--button <text>]
bunwv scroll <dx> <dy>
bunwv scroll-to <selector> [--block start|center|end|nearest] [--timeout ms]
bunwv 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>|-]
bunwv image <input> [--out <path>|-] [--format ...] [--quality N]
[--resize WxH | --max-width N | --max-height N]
[--rotate 90|180|270] [--flip] [--flop] [--metadata] [--placeholder]
bunwv evaluate <expression>
bunwv console [--clear] [--since <seq>] # terse "<seq> [<level>] <message>", cursor-based
bunwv events [--since <seq>]
bunwv cdp <method> [--params '{}']
bunwv cdp-subscribe <CDP.event> [<CDP.event> ...]
bunwv cdp-unsubscribe <CDP.event> [<CDP.event> ...]
bunwv cdp-subscriptions
bunwv wait-for <selector> | --url <substring> | --title <substring>
[--timeout ms]
bunwv wait-for-gone <selector> | --url <substring> | --title <substring>
[--timeout ms]
bunwv batch [--file <path>] [--keep-going] # stdin NDJSON of JSON arrays
bunwv status [--json]
bunwv resize <width> <height>
bunwv back / forward / reload
bunwv sessions
bunwv close [--all]
bunwv help
All commands accept --json (opt-in envelope), --session <name> (or BUNWV_SESSION env var), and the flexible flag syntax described above.
Default viewport is 1920x1080 for readable screenshots.
Sessions are named and isolated. Each session runs its own daemon on a separate Unix socket. The default session is named default; override with --session <name> or the BUNWV_SESSION env var.
bunwv start # starts "default" session
bunwv start --session cmais # separate "cmais" session
BUNWV_SESSION=cmais bunwv navigate http://localhost:3000
bunwv sessions # list all running sessions
bunwv close --session cmais # stop a specific session
bunwv close --all # stop every running session
Auto-shutdown: Daemons exit after 30 minutes of inactivity. Override with --idle-timeout:
bunwv start --idle-timeout 3600000 # 1 hour
bunwv start --idle-timeout 0 # never auto-shutdown
Reuse detection: bunwv start on an existing session prints the current URL and exits 0.
Best practice: Run bunwv sessions at the start of a conversation to check for orphaned daemons. Close any you don't need with bunwv close --all.
Look, then act, then look again. A canonical single-turn loop:
bunwv start (no-op if already running)bunwv navigate http://localhost:3000bunwv screenshot — prints /tmp/bunwv-screenshot-<session>.png to stdoutbunwv click --selector "button.submit" (or --text)bunwv wait-for --url "/next" (or wait-for "<selector>") before the next screenshotbunwv close when the task is doneFor multi-step flows, prefer bunwv batch (see below) — it runs the whole sequence in one process and returns an NDJSON transcript you can inspect.
click is polymorphic — use exactly one of --selector, --text, or --at:
bunwv click --selector "button.submit"
bunwv click --text "Sign In" # default: trimmed contains match
bunwv click --text "Sign In" --text-match exact
bunwv click --text "^Sign.+In$" --text-match regex
bunwv click --at 100,200
Modifiers, button, and click count are orthogonal:
bunwv click --selector "#ctx" --button right # context menu
bunwv click --selector ".item" --count 2 # double-click
bunwv click --selector "a" --mod Shift # shift+click
bunwv click --selector "a" --mod Meta --mod Shift # cmd+shift+click
bunwv click --selector "button" --timeout 60000 # longer actionability wait
--text defaults to trimmed substring match (case-sensitive). Use --text-match exact for strict equality or --text-match regex for a regex pattern. --selector and --text both produce native isTrusted: true events with the actionability wait; --at skips the wait.
Do NOT use Cmd+A / Backspace to clear React inputs — it doesn't update React state. Use clear:
bunwv clear "input[name='email']"
bunwv click --selector "input[name='email']"
bunwv type "new-value@example.com"
Always clear then click then type when editing existing input values.
Use wait-for after actions that trigger page changes:
bunwv click --text "Save Changes"
bunwv wait-for-gone "[role='dialog']" # wait for modal to close
bunwv screenshot
bunwv click --text "Edit"
bunwv wait-for "[role='dialog']" # wait for modal to appear
bunwv wait-for --url "/dashboard" # wait until URL contains substring
bunwv wait-for --title "Home" # wait until <title> contains substring
--url polls location.href and --title polls document.title. Exactly one of <selector>, --url, --title is required. Default timeout 10s.
Use exists as a cheap probe (silent; exit 0 present, 4 missing):
bunwv exists "[data-loaded]"
if [ $? -eq 0 ]; then ... fi
Prefer exists over evaluate "!!document.querySelector(...)" — fewer tokens, clearer contract.
evaluate prints the result as a JSON literal — strings keep their quotes, numbers don't, objects arrive as structured JSON:
bunwv evaluate "document.title" # "Example"
bunwv evaluate "document.querySelectorAll('.error').length" # 3
bunwv evaluate "[...document.querySelectorAll('h2')].map(h => h.textContent)"
Statements (const, let, if, etc.) are auto-wrapped in an IIFE.
Use submit instead of clicking the submit button — it uses form.requestSubmit(), which React forms accept (JS .click() produces isTrusted:false which many React handlers ignore):
bunwv submit # first form on page
bunwv submit --button "Save Changes" # submit via a specific button
bunwv submit --form "form.edit-quote" # target a specific form
After submitting, wait for the resulting DOM change:
bunwv submit --button "Save Changes"
bunwv wait-for-gone "[role='dialog']"
bunwv screenshot
Click the input first, then type. Use Tab to move between fields:
bunwv click --selector "input[name='email']"
bunwv type "user@example.com"
bunwv press Tab
bunwv type "password123"
bunwv submit --button "Sign In"
Credentials go in .env (Bun auto-loads it). The shell expands $VAR in CLI args:
bunwv type "$TEST_EMAIL"
Use --data-store to preserve cookies and localStorage across daemon restarts:
bunwv start --data-store ./bunwv-session
Log in once; future sessions stay authenticated.
Page console output is captured automatically. console.error/console.warn entries that fire during a verb are printed to stderr alongside the verb's response. To pull the full buffer:
bunwv console # terse: "<seq> [<level>] <message>", one per line
bunwv console --clear # print then clear
bunwv console --since 42 # only entries with seq > 42 (matches events cursor model)
bunwv --json console # {messages:[…], cursor, truncated?, oldest?}
Terse output escapes \n and \r in the message so each entry stays on one line. Empty buffer prints nothing (exit 0). Advance --since by using the max seq you saw (first field of each line). Use --json when you need raw message text (e.g. multi-line stack traces) or the truncation signal.
Navigation events and subscribed CDP events land in a ring buffer. Pull them with a cursor:
bunwv events # full buffer, prints {events, cursor}
bunwv events --since 42 # only events with seq > 42
Subscribe to CDP events (Chrome backend only; enable the domain first). Multiple types per call:
bunwv cdp Network.enable
bunwv cdp-subscribe Network.responseReceived Network.requestWillBeSent
bunwv navigate https://example.com
bunwv events --since 0 # inspect events
bunwv cdp-unsubscribe Network.responseReceived Network.requestWillBeSent
bunwv cdp-subscriptions # list active subscriptions, one per line
If the buffer evicted older entries, events returns "truncated":true,"oldest":<seq>.
Defaults write a JPEG (quality 80) to a session-scoped file and print its path. JPEG is the default because it's typically 5–15× smaller than a same-viewport PNG, which makes the screenshot loop dramatically cheaper.
bunwv screenshot # /tmp/bunwv-screenshot-<session>.jpg (JPEG @ q80)
bunwv screenshot --format png # opt back into PNG for pixel-exact comparisons
bunwv screenshot --format webp --quality 70 # smaller again at the cost of decode speed
bunwv screenshot --max-width 1024 # cap longest side; aspect preserved; never upscales
bunwv screenshot --max-width 800 --max-height 600 # bound both axes
bunwv screenshot --out shot.jpg # write to a specific path
bunwv screenshot --out - # bytes to stdout
bunwv screenshot --encoding base64 # base64 string to stdout
Cheap previews when you don't need full pixels:
bunwv screenshot --metadata # {"width":1920,"height":1080,"format":"png"} — size-check before Reading
bunwv screenshot --placeholder # data:image/png;base64,... (tiny blur-up preview, no file)
--placeholder and --metadata print structured output on stdout instead of writing a file, and are mutually exclusive with each other. They cannot be combined with --encoding shmem.
Platform notes: --format avif and --format heic are encode-supported on macOS/Windows running on Apple Silicon only. On other platforms a structured error is returned. Use jpeg/webp/png for portable code.
--encoding shmem (Kitty terminal) prints {name, size} and leaves the POSIX shm segment for the caller to unlink. It bypasses the Bun.Image pipeline, so --max-width/--max-height/--placeholder/--metadata are rejected when combined with it.
bunwv imageFor local image files outside the screenshot loop (uploads, downloaded assets, fixtures), bunwv image runs Bun.Image directly in the CLI process — no daemon required, no session needed.
bunwv image input.png # input.jpg (jpeg @ q80, same dir)
bunwv image input.png --out small.webp --max-width 512 # webp, capped at 512 wide, aspect preserved
bunwv image input.png --resize 200x200 # explicit resize (may distort if aspect differs)
bunwv image input.png --rotate 90 # rotate 90/180/270
bunwv image input.png --flip # vertical flip; --flop is horizontal
bunwv image input.png --metadata # {"width","height","format"} JSON on stdout
bunwv image input.png --placeholder # data: URL on stdout
Output format is inferred from the --out extension when --format isn't given; otherwise defaults to JPEG. If --out is omitted, the result is written next to the input with the new extension.
macOS defaults to WebKit; Linux/Windows auto-use Chrome. Override anywhere:
bunwv start --backend chrome
bunwv start --chrome-path /path/to/chromium
bunwv start --chrome-argv '["--headless=new"]'
bunwv start --chrome-url ws://127.0.0.1:9222/devtools/browser/<id> # attach to an existing Chrome
Raw CDP calls (Chrome only):
bunwv cdp "Page.getLayoutMetrics"
bunwv cdp "Runtime.evaluate" --params '{"expression": "1+1"}'
bunwv cdp "Emulation.setDeviceMetricsOverride" --params '{"width":375,"height":812,"deviceScaleFactor":2,"mobile":true}'
CDP is unavailable with the WebKit backend.
Route the backend process stdio to the daemon's stdio (human-debug only; agents never need these):
bunwv start --backend chrome --chrome-stderr inherit
bunwv start --webkit-stderr inherit
bunwv batch executes many commands in a single process — one socket round-trip per verb, no per-command Bun startup. Each stdin line is a JSON array of args; each response is an NDJSON envelope on stdout. Flags on batch (e.g. --session) inherit into every line unless that line specifies its own.
$ cat <<'EOF' | bunwv batch --session cmais --keep-going
["navigate","http://localhost:3000/login"]
["click","--selector","input[name='email']"]
["type","me@example.com"]
["press","Tab"]
["type","hunter2"]
["submit","--button","Sign In"]
["wait-for","--url","/dashboard"]
["screenshot"]
EOF
{"argv":[...],"ok":true,"exitCode":0}
{"argv":[...],"ok":true,"exitCode":0}
...
--keep-going runs the full list even if one line fails; the process exits 6 (batch-partial) if any failed, 0 if all succeeded, or the failing line's exit code when --keep-going is off. --file <path> reads from a file instead of stdin.
stdout fields contain the command's terse output (e.g. "\"Example Domain\"" for evaluate); stdoutBytes is base64 for binary outputs like screenshot --out -.
If a command fails or times out:
bunwv console to see any captured errorsbunwv events --since 0 to see navigation/CDP eventsbunwv evaluate to inspect the DOMbunwv start — the data store preserves authEach session's Unix socket (/tmp/bunwv-<session>.sock) and PID file are chmod 0600 — only the user who started the daemon can talk to it. On shared machines (containers, build boxes) this prevents other local users from driving your browser session.
--backend.click --selector / --text auto-wait for actionability (visible, stable, unobscured); WebView default 30s, override with --timeout.--text default is trimmed substring (contains). Use --text-match exact|regex to change.--at skips the actionability wait — requires knowing exact coordinates. Use evaluate + getBoundingClientRect() when CSS/text don't work.clear is required for React inputs — Cmd+A/Backspace don't update React's internal state.bunwv cdp and bunwv cdp-subscribe.events --since reports truncated when you missed any.