@drakulavich/bowser-cli
v0.5.0
Published
A Bun-native, drop-in command-compatible alternative to Microsoft playwright-cli for AI agents.
Maintainers
Readme
Bowser
A Bun-native, drop-in command-compatible alternative to Microsoft playwright-cli for AI agents. Same commands, same flag syntax, same snapshot YAML — replace playwright with bowser and existing playwright-cli skills work unchanged.
Built on Bun.WebView (new in Bun 1.3.12), so on macOS there's nothing to install beyond Bun itself, and on Linux / Windows it drives any installed Chrome / Chromium / Edge over the DevTools Protocol.
Why
What sets it apart from playwright-cli:
- Bun-native. Single static binary via
bun build --compile. Fast cold start. No Node / npm / Playwright install dance. - Token-efficient. Capabilities are shell commands, not MCP tool schemas. A skill description of a few hundred tokens covers the whole API.
- Persistent sessions. Each named session keeps a long-lived browser process so multi-step flows survive between commands.
Install
# From npm (requires Bun ≥ 1.3.12 on your PATH)
npm install -g @drakulavich/bowser-cli
# ...or directly from source
git clone https://github.com/drakulavich/bowser.git
cd bowser
bun install
bun link # exposes `bowser` on $PATHThen fetch a headless Chromium into Bowser's own cache (skipped if a system Chromium is already available):
bowser installPrebuilt single-file binaries for Linux (x64/arm64) and macOS (arm64/x64) are also attached to every GitHub Release — see Releases.
Requires Bun ≥ 1.3.12 for the npm/source install.
Browser backend
On macOS, bowser uses the native WKWebView engine by default — nothing to install.
It switches to Chrome/Chromium automatically if you opted in by running
bowser install (which caches a headless Chromium under ~/.bowser/chromium) or by
setting BOWSER_CHROMIUM_PATH. On Linux and Windows it always uses Chrome/Chromium.
Override the choice with BOWSER_BACKEND:
| Value | Effect |
| --- | --- |
| BOWSER_BACKEND=webkit | Force native WebKit (macOS only; errors elsewhere). |
| BOWSER_BACKEND=chrome | Force Chrome/Chromium. |
Screenshots are written as PNG files. bowser screenshot --filename out.png writes
to out.png (relative paths resolve against your current directory); without
--filename it writes screenshot-<session>.png, auto-incrementing (-1, -2, …)
if that file already exists. Captures are full-page (element-bounded screenshots are
not supported yet).
How Chromium is resolved
Bowser looks for a Chromium/Chrome binary in this order and uses the first one found:
$BOWSER_CHROMIUM_PATH(explicit override)~/.bowser/chromium/...(populated bybowser install)- System-wide installs:
/usr/bin/chromium-headless-shell,/usr/bin/chromium,/usr/bin/chromium-browser,/usr/bin/google-chrome,/Applications/Google Chrome.app/...,/Applications/Chromium.app/...
If none of those exist, run bowser install. It uses Playwright's downloader under the hood but writes into Bowser's own cache — it won't touch your Playwright setup. Use bowser install --force to re-download even when a system Chrome is already present.
Quickstart
bowser open https://example.com # navigate, save state
bowser snapshot # aria-tree YAML with [ref=eN]
bowser click e3 # click a ref
bowser fill e5 "[email protected]" # fill a form field
bowser press Enter # submit
bowser screenshot --filename=shot.png # capture
bowser close # end sessionEach session runs one persistent browser process (spawned lazily on first command, addressed over a Unix socket). Commands attach, run, and detach — so typed text, modals, dynamic DOM, cookies, and auth all survive across invocations. Session state lives under ~/.bowser/sessions/<name>/.
Multiple sessions
bowser -s=login open https://app.example.com/login
bowser -s=login fill e1 "[email protected]"
bowser -s=login fill e2 "$PASSWORD"
bowser -s=login click e3JSON output for agent pipelines
bowser --json snapshot | jq '.refs[] | select(.role == "button")'Command reference
| Command | Description |
| --- | --- |
| install [--force] | Download a headless Chromium |
| open [url] | Start session; navigate if URL given |
| goto <url> | Navigate within current session |
| snapshot [--filename=f] [--depth=N] | aria-tree YAML of interactive refs; --depth=N clips landmark nesting (N=1 is flat, default is unbounded) |
| click <ref> | Click an element |
| fill <ref> <text> | Focus, clear, type |
| type <text> | Type into focused element |
| press <key> | Press a keyboard key |
| hover <ref> | Hover an element |
| select <ref> <value> | Choose a <select> option |
| check <ref> / uncheck <ref> | Toggle a checkbox |
| screenshot [--filename=f] | Full-page screenshot (PNG) |
| resize <width> <height> | Set the viewport size in pixels. Works on both backends. |
| go-back / go-forward / reload | Navigation |
| list | List sessions |
| close [name] | End a session (defaults to --session; positional name overrides) |
| close --all | Close every open session |
| localstorage-list | List all localStorage entries (key=value per line, or JSON with --json) |
| localstorage-get <key> | Read a localStorage value |
| localstorage-set <key> <value> | Write a localStorage entry |
| localstorage-delete <key> | Remove a localStorage entry |
| localstorage-clear | Clear all localStorage entries |
| sessionstorage-list | List all sessionStorage entries (key=value per line, or JSON with --json) |
| sessionstorage-get <key> | Read a sessionStorage value |
| sessionstorage-set <key> <value> | Write a sessionStorage entry |
| sessionstorage-delete <key> | Remove a sessionStorage entry |
| sessionstorage-clear | Clear all sessionStorage entries |
| eval <expression> | Evaluate a JS expression in the current page; prints the result |
| run-code <code> | Run multi-statement JS in the current page; wrap in an IIFE, use return to produce a value |
| cookie-list [--domain=<d>] [--url=<u>] | List cookies for the current page (or specified scope). HttpOnly cookies are first-class. Requires the chrome backend. |
| cookie-get <name> [--domain=<d>] [--url=<u>] | Print a cookie's value (empty if not found). HttpOnly cookies are visible. Requires the chrome backend. |
| cookie-set <name> <value> [--domain=<d>] [--url=<u>] [--path=<p>] [--http-only] [--secure] [--same-site=Lax\|Strict\|None] [--expires=<unix-s>] | Set a cookie. Defaults URL to current page. --http-only sets the HttpOnly flag. Requires the chrome backend. |
| cookie-delete <name> [--domain=<d>] [--url=<u>] [--path=<p>] | Delete a cookie. Requires the chrome backend. |
| cookie-clear | Wipe all browser cookies in this session. Requires the chrome backend. |
| state-save <file> | Dump the cookie jar + current-origin localStorage to a Playwright-compatible storageState JSON file. Requires the chrome backend. |
| state-load <file> | Restore cookies + localStorage from a storageState file. localStorage restores for origins matching the current page; others are reported skipped. Requires the chrome backend. |
| mcp | Run a Model Context Protocol stdio server exposing every command above as an MCP tool. |
Global flags: -s=<name> / --session=<name>, --json, -h/--help.
MCP bridge
bowser mcp runs a Model Context Protocol server over stdio, exposing every browser command as an MCP tool — so MCP clients (Claude Desktop, etc.) can drive the browser without shelling out. Each tool maps 1:1 to a CLI command and takes an optional session argument; outputs are the same JSON as --json mode.
Register it in an MCP client config (e.g. claude_desktop_config.json):
{
"mcpServers": {
"bowser": { "command": "bowser", "args": ["mcp"] }
}
}Notes:
- Run
bowser installonce first (it is intentionally not exposed as a tool — it shells out and downloads Chromium). - The server is a thin client of the same per-session daemons the CLI uses; the first tool call on a fresh session spawns one.
- The protocol is hand-rolled (newline-delimited JSON-RPC) with zero runtime dependencies.
How it works
bowser openspawns a per-session daemon holding aBun.WebView, navigates, and saves{url, title}to~/.bowser/sessions/<name>/state.json.bowser snapshotruns a snapshot script in the page that walks the DOM, picks interactive elements, computes a stable CSS path (#idwhen safe, otherwise annth-of-typechain), and returns the refs as aria-tree YAML. Refs are persisted so later commands can resolvee3→html > body > button:nth-of-type(2).bowser click e3resolves the ref from state and dispatches the click via the daemon, usingBun.WebView's built-in actionability auto-wait — no polling, no hard-coded timeouts.
Because selectors are stable paths (not injected data- attributes), they survive page reloads between commands.
Environment variables
| Variable | Effect |
| --- | --- |
| BOWSER_BACKEND | webkit or chrome — override the auto-selected browser backend. |
| BOWSER_CHROMIUM_PATH | Explicit path to a chrome-headless-shell binary; bypasses auto-detection. |
| BOWSER_OP_TIMEOUT_MS | Per-operation timeout in milliseconds (default 30000; 0 disables). Bounds a wedged daemon operation — if the browser hangs, the command exits with a timeout error instead of blocking forever. |
Tests
bun test # unit + command tests with a fake daemon
BOWSER_E2E=1 bun test # + end-to-end against real headless Chromium
BOWSER_E2E=1 BOWSER_E2E_NET=1 bun test # + live-internet e2e (GitHub search)End-to-end examples included:
tests/e2e.test.ts— open/snapshot/click on adata:URL (no network)tests/e2e-todo.test.ts— a local todo app served byBun.serve: add three todos, toggle one, clear completed. Proves the daemon keeps state across commands.tests/e2e-search.test.ts— live web: search GitHub for OpenClaw, find the repo link, type into the search box and press Enter.
Build a single binary
bun build src/cli.ts --compile --outfile dist/bowser
./dist/bowser open https://example.comCross-compile for other platforms:
bun build src/cli.ts --compile --target=bun-darwin-arm64 --outfile dist/bowser-macos-arm64
bun build src/cli.ts --compile --target=bun-linux-x64 --outfile dist/bowser-linux-x64
bun build src/cli.ts --compile --target=bun-windows-x64 --outfile dist/bowser.exeRoadmap
- [x] Persistent session daemon over Unix socket
- [x] playwright-cli command compatibility for the core agent loop
- [x] Snapshot nesting honoring
--depth=N - [ ] Storage commands (
cookie-*,localstorage-*,state-save/load)- [x]
localstorage-{list,get,set,delete,clear} - [x]
sessionstorage-{list,get,set,delete,clear} - [x]
cookie-{list,get,set,delete,clear}— HttpOnly cookies are first-class; usesBun.WebView.cdp()(chrome backend only; see design) - [x]
state-save/state-load— Playwright-compatiblestorageStateJSON (cookies + per-origin localStorage; chrome backend only)
- [x]
- [ ] Tab management (
tab-list/tab-new/tab-select/tab-close) - [ ] Network mocking (
route,unroute) - [ ] Tracing / video / PDF output
- [x]
eval,run-code - [x]
resize - [ ]
dialog-accept/dismiss - [x] MCP bridge subcommand for non-CLI clients (
bowser mcp) - [ ] Agent skill published to agentskills.io
Migrating from 0.1.0
The 0.2.0 release is a clean break: snap → snapshot, @e3 → e3, --session → -s=, session list → list. See CHANGELOG.md for the full migration table.
License
MIT
