barebrowse 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/memory/AGENT_RULES.md +251 -0
- package/.claude/settings.local.json +30 -0
- package/.claude/stash/barebrowse-research-2026-02-22.md +49 -0
- package/.claude/stash/phase3-interactions-complete.md +69 -0
- package/.claude/stash/phase3-prep.md +88 -0
- package/.mcp.json +8 -0
- package/CHANGELOG.md +93 -0
- package/CLAUDE.md +22 -0
- package/README.md +200 -41
- package/barebrowse.context.md +261 -0
- package/cli.js +156 -0
- package/docs/blueprint.md +361 -0
- package/docs/poc-plan.md +230 -0
- package/docs/prd.md +284 -0
- package/docs/testing.md +202 -0
- package/examples/headed-demo.js +157 -0
- package/examples/yt-demo.js +137 -0
- package/mcp-server.js +216 -0
- package/package.json +22 -9
- package/src/aria.js +69 -0
- package/src/auth.js +279 -0
- package/src/bareagent.js +161 -0
- package/src/cdp.js +148 -0
- package/src/chromium.js +148 -0
- package/src/consent.js +210 -0
- package/src/index.js +186 -10
- package/src/interact.js +208 -0
- package/src/prune.js +472 -0
- package/src/stealth.js +51 -0
- package/test/integration/browse.test.js +108 -0
- package/test/integration/interact.test.js +514 -0
- package/test/unit/auth.test.js +66 -0
- package/test/unit/cdp.test.js +110 -0
- package/test/unit/prune.test.js +292 -0
package/README.md
CHANGED
|
@@ -1,69 +1,228 @@
|
|
|
1
1
|
# barebrowse
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
No Playwright, no bundled browser, no build step.
|
|
3
|
+
**URL in, agent-ready snapshot out.** Zero dependencies. Uses your own browser.
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
barebrowse gives autonomous agents eyes and hands on the web. It launches your installed Chromium, navigates to any page, and returns a pruned ARIA snapshot — a compact, semantic representation of what's on screen. The agent reads the snapshot, picks an element by ref, acts, and reads the next snapshot. Observe, think, act.
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
No Playwright. No bundled browser. No 200MB download. Just CDP over a WebSocket to whatever Chromium you already have.
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
import { browse, connect } from 'barebrowse';
|
|
9
|
+
## The idea
|
|
12
10
|
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
LLMs don't need DOM. They need to know what's on the page and what they can interact with. That's exactly what the browser's accessibility tree provides — every heading, button, link, input, and landmark, structured semantically.
|
|
12
|
+
|
|
13
|
+
But raw ARIA trees are noisy. A typical page produces 50-100KB of ARIA data. Most of it is decorative wrappers, hidden elements, and structural noise. barebrowse includes a 9-step pruning pipeline (ported from [mcprune](https://github.com/nickvdyck/mcprune)) that strips 47-95% of tokens while keeping every interactive element and meaningful label. A page that costs $0.15 in tokens raw costs $0.02-0.08 pruned.
|
|
14
|
+
|
|
15
|
+
The snapshot format uses `[ref=N]` markers on interactive elements. The agent says "click ref 8" and barebrowse scrolls the element into view, calculates coordinates, and dispatches real mouse events. No CSS selectors. No XPath. Just semantic refs from the ARIA tree.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
npm install barebrowse
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Requires Node.js >= 22 and any installed Chromium-based browser (Chrome, Chromium, Brave, Edge, Vivaldi).
|
|
24
|
+
|
|
25
|
+
## Quick start
|
|
26
|
+
|
|
27
|
+
```javascript
|
|
28
|
+
import { browse } from 'barebrowse';
|
|
29
|
+
|
|
30
|
+
// One line. That's it.
|
|
31
|
+
const snapshot = await browse('https://news.ycombinator.com');
|
|
32
|
+
console.log(snapshot);
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Output (pruned, ~50% smaller than raw):
|
|
36
|
+
```
|
|
37
|
+
- WebArea "Hacker News" [ref=1]
|
|
38
|
+
- link "Hacker News" [ref=4]
|
|
39
|
+
- link "new" [ref=7]
|
|
40
|
+
- link "past" [ref=9]
|
|
41
|
+
- link "comments" [ref=11]
|
|
42
|
+
...
|
|
43
|
+
- link "Show HN: I built a thing" [ref=42]
|
|
44
|
+
- link "197 comments" [ref=45]
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Two ways to use it
|
|
48
|
+
|
|
49
|
+
### 1. As a library (framework mode)
|
|
50
|
+
|
|
51
|
+
Import and call directly. You control the loop.
|
|
52
|
+
|
|
53
|
+
**One-shot** — read a page and get the snapshot:
|
|
54
|
+
|
|
55
|
+
```javascript
|
|
56
|
+
import { browse } from 'barebrowse';
|
|
57
|
+
|
|
58
|
+
const snapshot = await browse('https://example.com', {
|
|
59
|
+
mode: 'headless', // 'headless' | 'headed' | 'hybrid'
|
|
60
|
+
cookies: true, // inject cookies from your browser
|
|
61
|
+
prune: true, // ARIA pruning (47-95% token reduction)
|
|
62
|
+
consent: true, // auto-dismiss cookie consent dialogs
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Session** — navigate and interact across multiple pages:
|
|
67
|
+
|
|
68
|
+
```javascript
|
|
69
|
+
import { connect } from 'barebrowse';
|
|
15
70
|
|
|
16
|
-
// Session: navigate, interact, observe
|
|
17
71
|
const page = await connect();
|
|
18
|
-
await page.goto('https://
|
|
19
|
-
|
|
20
|
-
await page.
|
|
21
|
-
|
|
22
|
-
|
|
72
|
+
await page.goto('https://duckduckgo.com');
|
|
73
|
+
|
|
74
|
+
let snap = await page.snapshot();
|
|
75
|
+
// Agent sees: combobox "Search" [ref=5]
|
|
76
|
+
|
|
77
|
+
await page.type('5', 'barebrowse github');
|
|
78
|
+
await page.press('Enter');
|
|
79
|
+
await page.waitForNavigation();
|
|
80
|
+
|
|
81
|
+
snap = await page.snapshot();
|
|
82
|
+
// Agent sees search results with clickable refs
|
|
83
|
+
|
|
84
|
+
await page.click('12'); // click first result
|
|
23
85
|
await page.close();
|
|
24
86
|
```
|
|
25
87
|
|
|
26
|
-
|
|
88
|
+
### 2. As an MCP server
|
|
27
89
|
|
|
28
|
-
|
|
29
|
-
- **ARIA snapshots** — semantic, token-efficient output for LLMs
|
|
30
|
-
- **Built-in pruning** — 47-95% token reduction via 9-step pipeline
|
|
31
|
-
- **Cookie extraction** — authenticated browsing from your existing sessions (Chromium + Firefox)
|
|
32
|
-
- **Interactions** — click, type, scroll via CDP Input domain
|
|
33
|
-
- **Three modes** — headless (default), headed (connect to running browser), hybrid (planned)
|
|
34
|
-
|
|
35
|
-
## Architecture
|
|
90
|
+
For Claude Desktop, Cursor, Windsurf, or any MCP client.
|
|
36
91
|
|
|
92
|
+
```bash
|
|
93
|
+
npm install -g barebrowse
|
|
94
|
+
npx barebrowse install
|
|
37
95
|
```
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
96
|
+
|
|
97
|
+
That's it. `install` auto-detects Claude Desktop, Cursor, and Claude Code, and writes the MCP config for you. No manual JSON editing.
|
|
98
|
+
|
|
99
|
+
If you prefer manual setup, add this to your MCP config:
|
|
100
|
+
```json
|
|
101
|
+
{
|
|
102
|
+
"mcpServers": {
|
|
103
|
+
"barebrowse": {
|
|
104
|
+
"command": "npx",
|
|
105
|
+
"args": ["barebrowse", "mcp"]
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
46
109
|
```
|
|
47
110
|
|
|
48
|
-
|
|
111
|
+
This exposes 7 tools: `browse`, `goto`, `snapshot`, `click`, `type`, `press`, `scroll`. The LLM calls `goto` to navigate, `snapshot` to observe, and action tools to interact. Action tools return `'ok'` -- the LLM calls `snapshot` explicitly to see what changed.
|
|
112
|
+
|
|
113
|
+
**Same package, two entry points.** `npm install barebrowse` gives you both the library (`import { browse } from 'barebrowse'`) and the MCP server (`npx barebrowse mcp`). Pick whichever fits your setup.
|
|
114
|
+
|
|
115
|
+
## Three modes
|
|
116
|
+
|
|
117
|
+
| Mode | Flag | What happens | Use for |
|
|
118
|
+
|------|------|-------------|---------|
|
|
119
|
+
| **Headless** | `mode: 'headless'` (default) | Launches a fresh Chromium, no UI | Scraping, reading, fast automation |
|
|
120
|
+
| **Headed** | `mode: 'headed'` | Connects to your running browser via CDP port | Bot-detected sites, visual debugging |
|
|
121
|
+
| **Hybrid** | `mode: 'hybrid'` | Tries headless first, falls back to headed if bot-blocked | General-purpose agent browsing |
|
|
122
|
+
|
|
123
|
+
Headed mode requires your browser running with `--remote-debugging-port=9222`.
|
|
124
|
+
|
|
125
|
+
## What it handles automatically
|
|
126
|
+
|
|
127
|
+
You don't need to write code for any of this:
|
|
128
|
+
|
|
129
|
+
- **Cookie consent dialogs** — ARIA scan + jsClick across 7 languages (EN, NL, DE, FR, ES, IT, PT). Tested on 16+ sites.
|
|
130
|
+
- **Permission prompts** — notifications, geolocation, camera, mic all auto-denied via CDP
|
|
131
|
+
- **Login walls** — cookies extracted from your Firefox or Chromium profile, injected via CDP
|
|
132
|
+
- **Off-screen elements** — scrolled into view before every click
|
|
133
|
+
- **Bot detection** — stealth patches in headless (navigator.webdriver, plugins, chrome object)
|
|
134
|
+
- **Profile locking** — unique temp dir per headless instance
|
|
135
|
+
- **ARIA noise** — 9-step pruning pipeline strips decorative wrappers, hidden nodes, structural noise
|
|
136
|
+
|
|
137
|
+
## connect() API reference
|
|
138
|
+
|
|
139
|
+
| Method | Description |
|
|
140
|
+
|--------|-------------|
|
|
141
|
+
| `goto(url)` | Navigate + wait for load + dismiss consent |
|
|
142
|
+
| `snapshot()` | Pruned ARIA tree with `[ref=N]` markers |
|
|
143
|
+
| `click(ref)` | Scroll into view + mouse click at element center |
|
|
144
|
+
| `type(ref, text, opts?)` | Focus + insert text. `{ clear: true }` replaces existing. |
|
|
145
|
+
| `press(key)` | Special key: Enter, Tab, Escape, Backspace, Delete, arrows, Space |
|
|
146
|
+
| `scroll(deltaY)` | Mouse wheel. Positive = down, negative = up. |
|
|
147
|
+
| `hover(ref)` | Move mouse to element center |
|
|
148
|
+
| `select(ref, value)` | Set `<select>` value or click custom dropdown option |
|
|
149
|
+
| `screenshot(opts?)` | Returns base64 PNG/JPEG/WebP |
|
|
150
|
+
| `waitForNavigation()` | Wait for page load (SPA-aware) |
|
|
151
|
+
| `waitForNetworkIdle()` | Wait until no pending requests for 500ms |
|
|
152
|
+
| `injectCookies(url)` | Extract + inject cookies from your browser |
|
|
153
|
+
| `cdp` | Raw CDP session escape hatch |
|
|
154
|
+
| `close()` | Clean up everything |
|
|
155
|
+
|
|
156
|
+
## bareagent integration
|
|
157
|
+
|
|
158
|
+
barebrowse ships a tool adapter for [bareagent](https://github.com/nickvdyck/bareagent):
|
|
159
|
+
|
|
160
|
+
```javascript
|
|
161
|
+
import { Loop } from 'bare-agent';
|
|
162
|
+
import { Anthropic } from 'bare-agent/providers';
|
|
163
|
+
import { createBrowseTools } from 'barebrowse/bareagent';
|
|
164
|
+
|
|
165
|
+
const provider = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
|
|
166
|
+
const loop = new Loop({ provider });
|
|
167
|
+
|
|
168
|
+
const { tools, close } = createBrowseTools();
|
|
169
|
+
try {
|
|
170
|
+
const result = await loop.run(
|
|
171
|
+
[{ role: 'user', content: 'Find the top story on Hacker News' }],
|
|
172
|
+
tools
|
|
173
|
+
);
|
|
174
|
+
console.log(result.text);
|
|
175
|
+
} finally {
|
|
176
|
+
await close();
|
|
177
|
+
}
|
|
178
|
+
```
|
|
49
179
|
|
|
50
|
-
|
|
180
|
+
`createBrowseTools(opts)` returns 9 tools: browse, goto, snapshot, click, type, press, scroll, select, screenshot. Action tools auto-return a fresh snapshot after each action so the LLM always sees the result.
|
|
51
181
|
|
|
52
|
-
|
|
53
|
-
- Any installed Chromium-based browser (Chrome, Chromium, Brave, Edge, Vivaldi, Arc, Opera)
|
|
182
|
+
You can pass any `connect()` options to `createBrowseTools()` — mode, port, cookies, consent.
|
|
54
183
|
|
|
55
|
-
##
|
|
184
|
+
## How it works
|
|
56
185
|
|
|
57
186
|
```
|
|
58
|
-
|
|
59
|
-
|
|
187
|
+
URL -> chromium.js find/launch browser, permission-suppressing flags
|
|
188
|
+
-> cdp.js WebSocket CDP client, flattened sessions
|
|
189
|
+
-> stealth.js navigator.webdriver patches (headless only)
|
|
190
|
+
-> Browser.setPermission suppress all prompts
|
|
191
|
+
-> auth.js extract cookies from Firefox/Chromium -> inject via CDP
|
|
192
|
+
-> Page.navigate go to URL, wait for load
|
|
193
|
+
-> consent.js detect + dismiss cookie consent dialogs
|
|
194
|
+
-> aria.js Accessibility.getFullAXTree -> nested tree
|
|
195
|
+
-> prune.js 9-step pruning: wrappers, noise, landmarks
|
|
196
|
+
-> interact.js click/type/scroll/hover/select via CDP Input domain
|
|
197
|
+
-> snapshot agent-ready ARIA text with [ref=N] markers
|
|
60
198
|
```
|
|
61
199
|
|
|
62
|
-
|
|
200
|
+
11 modules, 2,400 lines, zero dependencies.
|
|
201
|
+
|
|
202
|
+
## Token savings
|
|
203
|
+
|
|
204
|
+
Real-world measurements on the pruning pipeline:
|
|
205
|
+
|
|
206
|
+
| Page | Raw ARIA | Pruned (act) | Reduction | Est. cost saved |
|
|
207
|
+
|------|----------|-------------|-----------|----------------|
|
|
208
|
+
| Hacker News | 52K chars | 27K chars | 47% | ~$0.04/call |
|
|
209
|
+
| Wikipedia article | 180K chars | 12K chars | 93% | ~$0.25/call |
|
|
210
|
+
| Amazon product | 95K chars | 8K chars | 92% | ~$0.13/call |
|
|
211
|
+
| Google results | 45K chars | 5K chars | 89% | ~$0.06/call |
|
|
63
212
|
|
|
64
|
-
|
|
213
|
+
Two pruning modes:
|
|
214
|
+
- **act** (default) — keeps interactive elements + visible labels. For clicking, typing, navigating.
|
|
215
|
+
- **read** — keeps all text content. For reading articles, extracting information.
|
|
216
|
+
|
|
217
|
+
## Context file
|
|
218
|
+
|
|
219
|
+
`barebrowse.context.md` in the repo root is an LLM-consumable integration guide. Feed it to an AI assistant that needs to wire barebrowse into a project — it covers the full API, snapshot format, interaction loop, auth options, and gotchas.
|
|
220
|
+
|
|
221
|
+
## Requirements
|
|
65
222
|
|
|
66
|
-
|
|
223
|
+
- Node.js >= 22 (built-in WebSocket, built-in SQLite)
|
|
224
|
+
- Any Chromium-based browser installed (Chrome, Chromium, Brave, Edge, Vivaldi)
|
|
225
|
+
- Linux tested (Fedora/KDE). macOS/Windows cookie paths exist but untested.
|
|
67
226
|
|
|
68
227
|
## License
|
|
69
228
|
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
# barebrowse -- Integration Guide
|
|
2
|
+
|
|
3
|
+
> For AI assistants and developers wiring barebrowse into a project.
|
|
4
|
+
> v0.1.0 | Node.js >= 22 | 0 required deps | MIT
|
|
5
|
+
|
|
6
|
+
## What this is
|
|
7
|
+
|
|
8
|
+
barebrowse is a CDP-direct browsing library for autonomous agents (~1,800 lines). URL in, pruned ARIA snapshot out. It launches the user's installed Chromium browser, navigates, handles consent/permissions/cookies, and returns a token-efficient ARIA tree with `[ref=N]` markers for interaction.
|
|
9
|
+
|
|
10
|
+
No Playwright. No bundled browser. No build step. Vanilla JS, ES modules.
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
npm install barebrowse
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Two entry points:
|
|
17
|
+
- `import { browse } from 'barebrowse'` -- one-shot: URL in, snapshot out
|
|
18
|
+
- `import { connect } from 'barebrowse'` -- session: navigate, interact, observe
|
|
19
|
+
|
|
20
|
+
## Which mode do I need?
|
|
21
|
+
|
|
22
|
+
| Mode | What it does | When to use |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| `headless` (default) | Launches a fresh Chromium, no UI | Scraping, reading, fast automation |
|
|
25
|
+
| `headed` | Connects to user's running browser on CDP port | Bot-detected sites, debugging, visual tasks |
|
|
26
|
+
| `hybrid` | Tries headless first, falls back to headed if blocked | General-purpose agent browsing |
|
|
27
|
+
|
|
28
|
+
Headed mode requires the browser to be launched with `--remote-debugging-port=9222`.
|
|
29
|
+
|
|
30
|
+
## Minimal usage: one-shot browse
|
|
31
|
+
|
|
32
|
+
```javascript
|
|
33
|
+
import { browse } from 'barebrowse';
|
|
34
|
+
|
|
35
|
+
// Defaults: headless, cookies injected, pruned, consent dismissed
|
|
36
|
+
const snapshot = await browse('https://example.com');
|
|
37
|
+
|
|
38
|
+
// All options
|
|
39
|
+
const snapshot = await browse('https://example.com', {
|
|
40
|
+
mode: 'headless', // 'headless' | 'headed' | 'hybrid'
|
|
41
|
+
cookies: true, // inject user's browser cookies
|
|
42
|
+
browser: 'firefox', // cookie source: 'firefox' | 'chromium' (auto-detected)
|
|
43
|
+
prune: true, // apply ARIA pruning (47-95% token reduction)
|
|
44
|
+
pruneMode: 'act', // 'act' (interactive elements) | 'read' (all content)
|
|
45
|
+
consent: true, // auto-dismiss cookie consent dialogs
|
|
46
|
+
timeout: 30000, // navigation timeout in ms
|
|
47
|
+
port: 9222, // CDP port for headed/hybrid mode
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## connect() API
|
|
52
|
+
|
|
53
|
+
`connect(opts)` returns a page handle for interactive sessions. Same opts as `browse()` for mode/port.
|
|
54
|
+
|
|
55
|
+
| Method | Args | Returns | Notes |
|
|
56
|
+
|---|---|---|---|
|
|
57
|
+
| `goto(url, timeout?)` | url: string, timeout: number (default 30000) | void | Navigate + wait for load + dismiss consent |
|
|
58
|
+
| `snapshot(pruneOpts?)` | false or { mode: 'act'\|'read' } | string | ARIA tree with `[ref=N]` markers. Pass `false` for raw. |
|
|
59
|
+
| `click(ref)` | ref: string | void | Scroll into view + mouse press+release at center |
|
|
60
|
+
| `type(ref, text, opts?)` | ref: string, text: string, opts: { clear?, keyEvents? } | void | Focus + insert text. `clear: true` replaces existing. |
|
|
61
|
+
| `press(key)` | key: string | void | Special key: Enter, Tab, Escape, Backspace, Delete, arrows, Home, End, PageUp, PageDown, Space |
|
|
62
|
+
| `scroll(deltaY)` | deltaY: number | void | Mouse wheel. Positive = down, negative = up. |
|
|
63
|
+
| `hover(ref)` | ref: string | void | Move mouse to element center |
|
|
64
|
+
| `select(ref, value)` | ref: string, value: string | void | Set `<select>` value or click custom dropdown option |
|
|
65
|
+
| `screenshot(opts?)` | { format?: 'png'\|'jpeg'\|'webp', quality?: number } | string (base64) | Page screenshot |
|
|
66
|
+
| `waitForNavigation(timeout?)` | timeout: number (default 30000) | void | Wait for page load or frame navigation |
|
|
67
|
+
| `waitForNetworkIdle(opts?)` | { timeout?: number, idle?: number } | void | Wait until no pending requests for `idle` ms (default 500) |
|
|
68
|
+
| `injectCookies(url, opts?)` | url: string, { browser?: string } | void | Extract cookies from user's browser and inject via CDP |
|
|
69
|
+
| `cdp` | -- | object | Raw CDP session for escape hatch: `page.cdp.send(method, params)` |
|
|
70
|
+
| `close()` | -- | void | Close page, disconnect CDP, kill browser (if headless) |
|
|
71
|
+
|
|
72
|
+
## Snapshot format
|
|
73
|
+
|
|
74
|
+
The snapshot is a YAML-like ARIA tree. Each line is one node:
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
- WebArea "Example Domain" [ref=1]
|
|
78
|
+
- heading "Example Domain" [level=1] [ref=3]
|
|
79
|
+
- paragraph [ref=5]
|
|
80
|
+
- StaticText "This domain is for use in illustrative examples." [ref=6]
|
|
81
|
+
- link "More information..." [ref=8]
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Key rules:
|
|
85
|
+
- `[ref=N]` markers appear on interactive and named elements
|
|
86
|
+
- Refs are **ephemeral** -- they change on every `snapshot()` call
|
|
87
|
+
- Always call `snapshot()` to get fresh refs before interacting
|
|
88
|
+
- `click(ref)` / `type(ref, text)` / `hover(ref)` / `select(ref, value)` use these ref strings
|
|
89
|
+
- Pruning removes noise (~47-95% token reduction) while keeping all interactive elements
|
|
90
|
+
|
|
91
|
+
## Interaction loop: observe, think, act
|
|
92
|
+
|
|
93
|
+
```javascript
|
|
94
|
+
import { connect } from 'barebrowse';
|
|
95
|
+
|
|
96
|
+
const page = await connect();
|
|
97
|
+
await page.goto('https://example.com');
|
|
98
|
+
|
|
99
|
+
// 1. Observe
|
|
100
|
+
let snap = await page.snapshot();
|
|
101
|
+
|
|
102
|
+
// 2. Think (LLM decides what to do based on snapshot)
|
|
103
|
+
// 3. Act
|
|
104
|
+
await page.click('8'); // click the "More information..." link
|
|
105
|
+
await page.waitForNavigation();
|
|
106
|
+
|
|
107
|
+
// 4. Observe again (refs are now different)
|
|
108
|
+
snap = await page.snapshot();
|
|
109
|
+
|
|
110
|
+
// ... repeat until goal is achieved
|
|
111
|
+
|
|
112
|
+
await page.close();
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Auth / cookie options
|
|
116
|
+
|
|
117
|
+
barebrowse can inject cookies from the user's real browser sessions, bypassing login walls.
|
|
118
|
+
|
|
119
|
+
| Source | How | Notes |
|
|
120
|
+
|---|---|---|
|
|
121
|
+
| Firefox (default) | SQLite `cookies.sqlite`, plaintext | Works on Linux. Auto-detected default profile. |
|
|
122
|
+
| Chromium | SQLite `Cookies` + AES decryption via keyring | Linux: KWallet or GNOME Keyring. Profile must not be locked. |
|
|
123
|
+
| Manual | `page.injectCookies(url, { browser: 'firefox' })` | Explicit injection on connect() sessions |
|
|
124
|
+
| Disabled | `{ cookies: false }` | No cookie injection |
|
|
125
|
+
|
|
126
|
+
`browse()` auto-injects cookies before navigation. `connect()` exposes `injectCookies()` for manual control.
|
|
127
|
+
|
|
128
|
+
## Obstacle course -- what barebrowse handles automatically
|
|
129
|
+
|
|
130
|
+
| Obstacle | How | Mode |
|
|
131
|
+
|---|---|---|
|
|
132
|
+
| Cookie consent (GDPR) | ARIA scan + jsClick accept button, 7 languages | Both |
|
|
133
|
+
| Consent behind iframes | JS `.click()` via DOM.resolveNode bypasses overlays | Both |
|
|
134
|
+
| Permission prompts | Launch flags + CDP Browser.setPermission auto-deny | Both |
|
|
135
|
+
| Media autoplay blocked | `--autoplay-policy=no-user-gesture-required` | Both |
|
|
136
|
+
| Login walls | Cookie extraction from Firefox/Chromium + CDP injection | Both |
|
|
137
|
+
| Pre-filled form inputs | `type({ clear: true })` selects all + deletes first | Both |
|
|
138
|
+
| Off-screen elements | `DOM.scrollIntoViewIfNeeded` before every click | Both |
|
|
139
|
+
| Form submission | `press('Enter')` triggers onsubmit | Both |
|
|
140
|
+
| SPA navigation | `waitForNavigation()` uses loadEventFired + frameNavigated | Both |
|
|
141
|
+
| Bot detection | Headed mode with real cookies bypasses most checks | Headed |
|
|
142
|
+
| `navigator.webdriver` | Stealth patches in headless (webdriver, plugins, chrome obj) | Headless |
|
|
143
|
+
| Profile locking | Unique temp dir per headless instance | Headless |
|
|
144
|
+
| ARIA noise | 9-step pruning: wrapper collapse, noise removal, landmark promotion | Both |
|
|
145
|
+
|
|
146
|
+
## bareagent wiring
|
|
147
|
+
|
|
148
|
+
barebrowse provides a tool adapter for bareagent's Loop:
|
|
149
|
+
|
|
150
|
+
```javascript
|
|
151
|
+
import { Loop } from 'bare-agent';
|
|
152
|
+
import { Anthropic } from 'bare-agent/providers';
|
|
153
|
+
import { createBrowseTools } from 'barebrowse/src/bareagent.js';
|
|
154
|
+
|
|
155
|
+
const provider = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
|
|
156
|
+
const loop = new Loop({ provider });
|
|
157
|
+
|
|
158
|
+
const { tools, close } = createBrowseTools();
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
const result = await loop.run(
|
|
162
|
+
[{ role: 'user', content: 'Search for "barebrowse" on DuckDuckGo and tell me the first result' }],
|
|
163
|
+
tools
|
|
164
|
+
);
|
|
165
|
+
console.log(result.text);
|
|
166
|
+
} finally {
|
|
167
|
+
await close();
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
`createBrowseTools(opts)` returns:
|
|
172
|
+
- `tools` -- array of bareagent-compatible tool objects (browse, goto, snapshot, click, type, press, scroll, select, screenshot)
|
|
173
|
+
- `close()` -- cleanup function, call when done
|
|
174
|
+
|
|
175
|
+
Action tools (click, type, press, scroll, goto) auto-return a fresh snapshot so the LLM always sees the result. 300ms settle delay after actions for DOM updates.
|
|
176
|
+
|
|
177
|
+
## MCP wrapper
|
|
178
|
+
|
|
179
|
+
barebrowse ships an MCP server for direct use with Claude Desktop, Cursor, or any MCP client.
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
npm install barebrowse # or npm install -g barebrowse
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Add to your MCP client config (`.mcp.json`, `claude_desktop_config.json`, etc.):
|
|
186
|
+
```json
|
|
187
|
+
{
|
|
188
|
+
"mcpServers": {
|
|
189
|
+
"barebrowse": {
|
|
190
|
+
"command": "npx",
|
|
191
|
+
"args": ["barebrowse", "mcp"]
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
7 tools exposed: `browse` (one-shot), `goto`, `snapshot`, `click`, `type`, `press`, `scroll`.
|
|
198
|
+
|
|
199
|
+
Action tools return `'ok'` -- the agent calls `snapshot` explicitly to observe. This avoids double-token output since MCP tool calls are cheap to chain.
|
|
200
|
+
|
|
201
|
+
Session tools (goto, snapshot, click, type, press, scroll) share a singleton page, lazy-created on first use.
|
|
202
|
+
|
|
203
|
+
## Architecture
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
URL -> chromium.js (find/launch browser, permission flags)
|
|
207
|
+
-> cdp.js (WebSocket CDP client)
|
|
208
|
+
-> stealth.js (navigator.webdriver patches, headless only)
|
|
209
|
+
-> Browser.setPermission (suppress prompts)
|
|
210
|
+
-> auth.js (extract cookies -> inject via CDP)
|
|
211
|
+
-> Page.navigate
|
|
212
|
+
-> consent.js (detect + dismiss cookie dialogs)
|
|
213
|
+
-> aria.js (Accessibility.getFullAXTree -> nested tree)
|
|
214
|
+
-> prune.js (9-step role-based pruning)
|
|
215
|
+
-> interact.js (click/type/scroll/hover/select via Input domain)
|
|
216
|
+
-> agent-ready snapshot
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
| Module | Lines | Purpose |
|
|
220
|
+
|---|---|---|
|
|
221
|
+
| `src/index.js` | ~370 | Public API: `browse()`, `connect()`, screenshot, network idle, hybrid |
|
|
222
|
+
| `src/cdp.js` | 148 | WebSocket CDP client, flattened sessions |
|
|
223
|
+
| `src/chromium.js` | 148 | Find/launch Chromium browsers, permission-suppressing flags |
|
|
224
|
+
| `src/aria.js` | 69 | Format ARIA tree as text |
|
|
225
|
+
| `src/auth.js` | 279 | Cookie extraction (Chromium AES + keyring, Firefox), CDP injection |
|
|
226
|
+
| `src/prune.js` | 472 | ARIA pruning pipeline (ported from mcprune) |
|
|
227
|
+
| `src/interact.js` | ~170 | Click, type, press, scroll, hover, select |
|
|
228
|
+
| `src/consent.js` | 200 | Auto-dismiss cookie consent dialogs across languages |
|
|
229
|
+
| `src/stealth.js` | ~40 | Navigator patches for headless anti-detection |
|
|
230
|
+
| `src/bareagent.js` | ~120 | Tool adapter for bareagent Loop |
|
|
231
|
+
| `mcp-server.js` | ~170 | MCP server (JSON-RPC over stdio) |
|
|
232
|
+
|
|
233
|
+
## Gotchas
|
|
234
|
+
|
|
235
|
+
1. **Refs are ephemeral.** Every `snapshot()` call generates new refs. Always snapshot before interacting. Never cache refs across snapshots.
|
|
236
|
+
|
|
237
|
+
2. **SPA navigation has no loadEventFired.** For single-page apps (React, YouTube, GitHub), use `waitForNetworkIdle()` or a timed wait after click instead of `waitForNavigation()`.
|
|
238
|
+
|
|
239
|
+
3. **Pruning modes matter.** `act` mode (default) keeps interactive elements + visible labels. `read` mode keeps all text content. Use `read` for content extraction, `act` for form filling and navigation.
|
|
240
|
+
|
|
241
|
+
4. **Headed mode requires manual browser launch.** Start your browser with `--remote-debugging-port=9222`. barebrowse connects to it -- it does not launch it.
|
|
242
|
+
|
|
243
|
+
5. **Cookie extraction needs unlocked profile.** Chromium cookies are AES-encrypted with a keyring key. If Chromium is running, the profile may be locked. Firefox cookies are plaintext and always accessible.
|
|
244
|
+
|
|
245
|
+
6. **Hybrid mode kills and relaunches.** If headless is bot-blocked, hybrid mode kills the headless browser and connects to headed on port 9222. The headed browser must already be running.
|
|
246
|
+
|
|
247
|
+
7. **One page per connect().** Each `connect()` call creates one page. For multiple tabs, call `connect()` multiple times.
|
|
248
|
+
|
|
249
|
+
8. **Consent dismiss is best-effort.** It handles 16+ tested sites across 7 languages but novel consent implementations may need manual handling. Disable with `{ consent: false }`.
|
|
250
|
+
|
|
251
|
+
9. **Screenshot returns base64.** Write to file with `fs.writeFileSync('shot.png', Buffer.from(base64, 'base64'))` or pass directly to a vision model.
|
|
252
|
+
|
|
253
|
+
10. **Chromium-only.** CDP protocol limits us to Chrome, Chromium, Edge, Brave, Vivaldi (~80% desktop share). Firefox support via WebDriver BiDi is not yet implemented.
|
|
254
|
+
|
|
255
|
+
## Constraints
|
|
256
|
+
|
|
257
|
+
- **Node >= 22** -- built-in WebSocket, built-in SQLite
|
|
258
|
+
- **Chromium-only** -- CDP protocol
|
|
259
|
+
- **Linux first** -- tested on Fedora/KDE, macOS/Windows cookie paths exist but untested
|
|
260
|
+
- **Not a server** -- library that agents import. Wrap as MCP (included) or HTTP if needed.
|
|
261
|
+
- **Zero required deps** -- everything uses Node stdlib
|
package/cli.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* cli.js -- barebrowse CLI entry point.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* npx barebrowse mcp Start the MCP server (JSON-RPC over stdio)
|
|
7
|
+
* npx barebrowse install Auto-configure MCP in Claude Desktop / Cursor / Claude Code
|
|
8
|
+
* npx barebrowse browse <url> One-shot browse, print snapshot to stdout
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { homedir, platform } from 'node:os';
|
|
14
|
+
|
|
15
|
+
const cmd = process.argv[2];
|
|
16
|
+
|
|
17
|
+
if (cmd === 'mcp') {
|
|
18
|
+
await import('./mcp-server.js');
|
|
19
|
+
|
|
20
|
+
} else if (cmd === 'install') {
|
|
21
|
+
install();
|
|
22
|
+
|
|
23
|
+
} else if (cmd === 'browse' && process.argv[3]) {
|
|
24
|
+
const { browse } = await import('./src/index.js');
|
|
25
|
+
const url = process.argv[3];
|
|
26
|
+
const mode = process.argv[4] || 'headless';
|
|
27
|
+
try {
|
|
28
|
+
const snapshot = await browse(url, { mode });
|
|
29
|
+
process.stdout.write(snapshot + '\n');
|
|
30
|
+
process.exit(0);
|
|
31
|
+
} catch (err) {
|
|
32
|
+
process.stderr.write(`Error: ${err.message}\n`);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
} else {
|
|
37
|
+
process.stdout.write(`barebrowse -- CDP-direct browsing for autonomous agents
|
|
38
|
+
|
|
39
|
+
Usage:
|
|
40
|
+
barebrowse mcp Start MCP server (JSON-RPC over stdio)
|
|
41
|
+
barebrowse install Auto-configure MCP for Claude Desktop / Cursor / Claude Code
|
|
42
|
+
barebrowse browse <url> One-shot browse, print ARIA snapshot
|
|
43
|
+
|
|
44
|
+
As a library:
|
|
45
|
+
import { browse, connect } from 'barebrowse';
|
|
46
|
+
|
|
47
|
+
As bareagent tools:
|
|
48
|
+
import { createBrowseTools } from 'barebrowse/bareagent';
|
|
49
|
+
|
|
50
|
+
More: see README.md or barebrowse.context.md
|
|
51
|
+
`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// --- MCP auto-installer ---
|
|
55
|
+
|
|
56
|
+
function install() {
|
|
57
|
+
const mcpEntry = {
|
|
58
|
+
command: 'npx',
|
|
59
|
+
args: ['barebrowse', 'mcp'],
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const targets = detectTargets();
|
|
63
|
+
|
|
64
|
+
if (targets.length === 0) {
|
|
65
|
+
console.log('No MCP clients detected. You can manually add this to your MCP config:\n');
|
|
66
|
+
console.log(JSON.stringify({ mcpServers: { barebrowse: mcpEntry } }, null, 2));
|
|
67
|
+
console.log('\nSupported clients: Claude Desktop, Cursor, Claude Code');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let installed = 0;
|
|
72
|
+
|
|
73
|
+
for (const target of targets) {
|
|
74
|
+
try {
|
|
75
|
+
const config = readJsonOrEmpty(target.path);
|
|
76
|
+
if (!config.mcpServers) config.mcpServers = {};
|
|
77
|
+
|
|
78
|
+
if (config.mcpServers.barebrowse) {
|
|
79
|
+
console.log(` ${target.name}: already configured`);
|
|
80
|
+
installed++;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
config.mcpServers.barebrowse = mcpEntry;
|
|
85
|
+
|
|
86
|
+
// Ensure parent dir exists
|
|
87
|
+
const dir = join(target.path, '..');
|
|
88
|
+
mkdirSync(dir, { recursive: true });
|
|
89
|
+
|
|
90
|
+
writeFileSync(target.path, JSON.stringify(config, null, 2) + '\n');
|
|
91
|
+
console.log(` ${target.name}: installed -> ${target.path}`);
|
|
92
|
+
installed++;
|
|
93
|
+
} catch (err) {
|
|
94
|
+
console.log(` ${target.name}: failed (${err.message})`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (installed > 0) {
|
|
99
|
+
console.log(`\nDone. Restart your MCP client to pick up the new server.`);
|
|
100
|
+
console.log('Tools available: browse, goto, snapshot, click, type, press, scroll');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function detectTargets() {
|
|
105
|
+
const home = homedir();
|
|
106
|
+
const os = platform();
|
|
107
|
+
const targets = [];
|
|
108
|
+
|
|
109
|
+
// Claude Desktop
|
|
110
|
+
let claudeDesktop;
|
|
111
|
+
if (os === 'darwin') {
|
|
112
|
+
claudeDesktop = join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
|
|
113
|
+
} else if (os === 'linux') {
|
|
114
|
+
claudeDesktop = join(home, '.config', 'Claude', 'claude_desktop_config.json');
|
|
115
|
+
} else if (os === 'win32') {
|
|
116
|
+
claudeDesktop = join(home, 'AppData', 'Roaming', 'Claude', 'claude_desktop_config.json');
|
|
117
|
+
}
|
|
118
|
+
if (claudeDesktop) {
|
|
119
|
+
// Check if Claude Desktop dir exists (even if config doesn't yet)
|
|
120
|
+
const dir = join(claudeDesktop, '..');
|
|
121
|
+
if (existsSync(dir)) {
|
|
122
|
+
targets.push({ name: 'Claude Desktop', path: claudeDesktop });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Cursor
|
|
127
|
+
let cursorDir;
|
|
128
|
+
if (os === 'darwin') {
|
|
129
|
+
cursorDir = join(home, '.cursor');
|
|
130
|
+
} else if (os === 'linux') {
|
|
131
|
+
cursorDir = join(home, '.cursor');
|
|
132
|
+
} else if (os === 'win32') {
|
|
133
|
+
cursorDir = join(home, '.cursor');
|
|
134
|
+
}
|
|
135
|
+
if (cursorDir && existsSync(cursorDir)) {
|
|
136
|
+
targets.push({ name: 'Cursor', path: join(cursorDir, 'mcp.json') });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Claude Code (project-level .mcp.json in cwd)
|
|
140
|
+
const cwd = process.cwd();
|
|
141
|
+
const claudeCodePath = join(cwd, '.mcp.json');
|
|
142
|
+
// Only suggest if we're in a project directory (has package.json or .git)
|
|
143
|
+
if (existsSync(join(cwd, 'package.json')) || existsSync(join(cwd, '.git'))) {
|
|
144
|
+
targets.push({ name: 'Claude Code (this project)', path: claudeCodePath });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return targets;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function readJsonOrEmpty(path) {
|
|
151
|
+
try {
|
|
152
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
153
|
+
} catch {
|
|
154
|
+
return {};
|
|
155
|
+
}
|
|
156
|
+
}
|