barebrowse 0.2.2 → 0.3.1
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/skills/barebrowse/SKILL.md +107 -0
- package/CHANGELOG.md +57 -0
- package/CLAUDE.md +4 -2
- package/README.md +52 -5
- package/barebrowse.context.md +27 -8
- package/cli.js +289 -48
- package/docs/00-context/assumptions.md +38 -0
- package/docs/{blueprint.md → 00-context/system-state.md} +30 -5
- package/docs/00-context/vision.md +52 -0
- package/docs/01-product/prd.md +284 -0
- package/docs/03-logs/bug-log.md +16 -0
- package/docs/03-logs/decisions-log.md +32 -0
- package/docs/03-logs/implementation-log.md +54 -0
- package/docs/03-logs/insights.md +35 -0
- package/docs/03-logs/validation-log.md +123 -0
- package/docs/04-process/definition-of-done.md +31 -0
- package/docs/04-process/dev-workflow.md +68 -0
- package/docs/{testing.md → 04-process/testing.md} +21 -2
- package/docs/README.md +55 -0
- package/docs/archive/poc-plan.md +230 -0
- package/package.json +1 -1
- package/src/aria.js +1 -1
- package/src/daemon.js +321 -0
- package/src/session-client.js +70 -0
package/cli.js
CHANGED
|
@@ -2,28 +2,192 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* cli.js -- barebrowse CLI entry point.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* Session commands:
|
|
6
|
+
* barebrowse open [url] [flags] Open browser session (daemon)
|
|
7
|
+
* barebrowse close Close session + kill daemon
|
|
8
|
+
* barebrowse status Check if session is running
|
|
9
|
+
*
|
|
10
|
+
* Navigation:
|
|
11
|
+
* barebrowse goto <url> Navigate to URL
|
|
12
|
+
* barebrowse snapshot [--mode] Get pruned ARIA snapshot → file
|
|
13
|
+
* barebrowse screenshot [--format] Take screenshot → file
|
|
14
|
+
*
|
|
15
|
+
* Interaction:
|
|
16
|
+
* barebrowse click <ref> Click element by ref
|
|
17
|
+
* barebrowse type <ref> <text> Type text into element
|
|
18
|
+
* barebrowse fill <ref> <text> Clear + type (replace content)
|
|
19
|
+
* barebrowse press <key> Press special key
|
|
20
|
+
* barebrowse scroll <deltaY> Scroll page
|
|
21
|
+
* barebrowse hover <ref> Hover over element
|
|
22
|
+
* barebrowse select <ref> <value> Select dropdown value
|
|
23
|
+
*
|
|
24
|
+
* Self-sufficiency:
|
|
25
|
+
* barebrowse eval <expression> Evaluate JS in page
|
|
26
|
+
* barebrowse wait-idle [--timeout] Wait for network idle
|
|
27
|
+
* barebrowse console-logs Dump console logs → file
|
|
28
|
+
* barebrowse network-log Dump network log → file
|
|
29
|
+
*
|
|
30
|
+
* Legacy / tools:
|
|
31
|
+
* barebrowse browse <url> [mode] One-shot browse (stdout)
|
|
32
|
+
* barebrowse mcp Start MCP server (stdio)
|
|
33
|
+
* barebrowse install [--skill] Auto-configure MCP or install skill
|
|
9
34
|
*/
|
|
10
35
|
|
|
11
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
12
|
-
import { join } from 'node:path';
|
|
36
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync } from 'node:fs';
|
|
37
|
+
import { join, resolve } from 'node:path';
|
|
13
38
|
import { homedir, platform } from 'node:os';
|
|
39
|
+
import { fileURLToPath } from 'node:url';
|
|
14
40
|
|
|
15
|
-
const
|
|
41
|
+
const args = process.argv.slice(2);
|
|
42
|
+
const cmd = args[0];
|
|
16
43
|
|
|
17
|
-
|
|
44
|
+
// Hidden internal flag: --daemon-internal
|
|
45
|
+
if (args.includes('--daemon-internal')) {
|
|
46
|
+
await runDaemonInternal();
|
|
47
|
+
} else if (cmd === 'mcp') {
|
|
18
48
|
await import('./mcp-server.js');
|
|
19
|
-
|
|
20
49
|
} else if (cmd === 'install') {
|
|
21
50
|
install();
|
|
51
|
+
} else if (cmd === 'browse' && args[1]) {
|
|
52
|
+
await oneShot();
|
|
53
|
+
} else if (cmd === 'open') {
|
|
54
|
+
await cmdOpen();
|
|
55
|
+
} else if (cmd === 'close') {
|
|
56
|
+
await cmdProxy('close');
|
|
57
|
+
} else if (cmd === 'status') {
|
|
58
|
+
await cmdStatus();
|
|
59
|
+
} else if (cmd === 'goto' && args[1]) {
|
|
60
|
+
await cmdProxy('goto', { url: args[1], timeout: parseFlag('--timeout') });
|
|
61
|
+
} else if (cmd === 'snapshot') {
|
|
62
|
+
await cmdProxy('snapshot', { mode: parseFlag('--mode') });
|
|
63
|
+
} else if (cmd === 'screenshot') {
|
|
64
|
+
await cmdProxy('screenshot', { format: parseFlag('--format') });
|
|
65
|
+
} else if (cmd === 'click' && args[1]) {
|
|
66
|
+
await cmdProxy('click', { ref: args[1] });
|
|
67
|
+
} else if (cmd === 'type' && args[1] && args[2]) {
|
|
68
|
+
await cmdProxy('type', { ref: args[1], text: args.slice(2).filter(a => !a.startsWith('--')).join(' '), clear: hasFlag('--clear') });
|
|
69
|
+
} else if (cmd === 'fill' && args[1] && args[2]) {
|
|
70
|
+
await cmdProxy('fill', { ref: args[1], text: args.slice(2).filter(a => !a.startsWith('--')).join(' ') });
|
|
71
|
+
} else if (cmd === 'press' && args[1]) {
|
|
72
|
+
await cmdProxy('press', { key: args[1] });
|
|
73
|
+
} else if (cmd === 'scroll' && args[1]) {
|
|
74
|
+
await cmdProxy('scroll', { deltaY: Number(args[1]) });
|
|
75
|
+
} else if (cmd === 'hover' && args[1]) {
|
|
76
|
+
await cmdProxy('hover', { ref: args[1] });
|
|
77
|
+
} else if (cmd === 'select' && args[1] && args[2]) {
|
|
78
|
+
await cmdProxy('select', { ref: args[1], value: args[2] });
|
|
79
|
+
} else if (cmd === 'eval' && args[1]) {
|
|
80
|
+
await cmdProxy('eval', { expression: args.slice(1).join(' ') });
|
|
81
|
+
} else if (cmd === 'wait-idle') {
|
|
82
|
+
await cmdProxy('wait-idle', { timeout: parseFlag('--timeout') });
|
|
83
|
+
} else if (cmd === 'console-logs') {
|
|
84
|
+
await cmdProxy('console-logs', { level: parseFlag('--level'), clear: hasFlag('--clear') });
|
|
85
|
+
} else if (cmd === 'network-log') {
|
|
86
|
+
await cmdProxy('network-log', { failed: hasFlag('--failed') });
|
|
87
|
+
} else {
|
|
88
|
+
printUsage();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
// --- Command implementations ---
|
|
93
|
+
|
|
94
|
+
async function cmdOpen() {
|
|
95
|
+
const { startDaemon } = await import('./src/daemon.js');
|
|
96
|
+
const { isAlive } = await import('./src/session-client.js');
|
|
97
|
+
const outputDir = resolve('.barebrowse');
|
|
98
|
+
|
|
99
|
+
// Check for existing session
|
|
100
|
+
if (await isAlive(outputDir)) {
|
|
101
|
+
process.stdout.write('Session already running. Use `barebrowse close` first.\n');
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const url = args[1] && !args[1].startsWith('--') ? args[1] : undefined;
|
|
106
|
+
const opts = {
|
|
107
|
+
mode: parseFlag('--mode') || 'headless',
|
|
108
|
+
port: parseFlag('--port'),
|
|
109
|
+
cookies: !hasFlag('--no-cookies'),
|
|
110
|
+
browser: parseFlag('--browser'),
|
|
111
|
+
timeout: parseFlag('--timeout'),
|
|
112
|
+
pruneMode: parseFlag('--prune-mode') || 'act',
|
|
113
|
+
consent: !hasFlag('--no-consent'),
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const session = await startDaemon(opts, outputDir, url);
|
|
118
|
+
process.stdout.write(`Session started (pid ${session.pid}, port ${session.port})\n`);
|
|
119
|
+
if (url) process.stdout.write(`Navigated to ${url}\n`);
|
|
120
|
+
process.stdout.write(`Output dir: ${outputDir}\n`);
|
|
121
|
+
} catch (err) {
|
|
122
|
+
process.stderr.write(`Error: ${err.message}\n`);
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
22
126
|
|
|
23
|
-
|
|
127
|
+
async function cmdStatus() {
|
|
128
|
+
const { readSession, isAlive } = await import('./src/session-client.js');
|
|
129
|
+
const outputDir = resolve('.barebrowse');
|
|
130
|
+
const session = readSession(outputDir);
|
|
131
|
+
|
|
132
|
+
if (!session) {
|
|
133
|
+
process.stdout.write('No session found.\n');
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const alive = await isAlive(outputDir);
|
|
138
|
+
if (alive) {
|
|
139
|
+
process.stdout.write(`Session running (pid ${session.pid}, port ${session.port}, started ${session.startedAt})\n`);
|
|
140
|
+
} else {
|
|
141
|
+
process.stdout.write(`Session stale (pid ${session.pid} not responding). Run \`barebrowse close\` to clean up.\n`);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function cmdProxy(command, cmdArgs) {
|
|
147
|
+
const { sendCommand, readSession } = await import('./src/session-client.js');
|
|
148
|
+
const { unlinkSync } = await import('node:fs');
|
|
149
|
+
const outputDir = resolve('.barebrowse');
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const result = await sendCommand(command, cmdArgs, outputDir);
|
|
153
|
+
|
|
154
|
+
if (!result.ok) {
|
|
155
|
+
process.stderr.write(`Error: ${result.error}\n`);
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Print result
|
|
160
|
+
if (result.file && result.count !== undefined) {
|
|
161
|
+
process.stdout.write(`${result.file} (${result.count} entries)\n`);
|
|
162
|
+
} else if (result.file) {
|
|
163
|
+
process.stdout.write(`${result.file}\n`);
|
|
164
|
+
} else if (result.value !== undefined) {
|
|
165
|
+
process.stdout.write(JSON.stringify(result.value) + '\n');
|
|
166
|
+
} else if (command === 'close') {
|
|
167
|
+
// Clean up session.json in case daemon didn't
|
|
168
|
+
const sessionPath = join(outputDir, 'session.json');
|
|
169
|
+
try { unlinkSync(sessionPath); } catch { /* already gone */ }
|
|
170
|
+
process.stdout.write('Session closed.\n');
|
|
171
|
+
} else {
|
|
172
|
+
process.stdout.write('ok\n');
|
|
173
|
+
}
|
|
174
|
+
} catch (err) {
|
|
175
|
+
if (command === 'close') {
|
|
176
|
+
// Daemon may have exited before responding — that's fine
|
|
177
|
+
const sessionPath = join(outputDir, 'session.json');
|
|
178
|
+
try { unlinkSync(sessionPath); } catch { /* already gone */ }
|
|
179
|
+
process.stdout.write('Session closed.\n');
|
|
180
|
+
} else {
|
|
181
|
+
process.stderr.write(`Error: ${err.message}\n`);
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function oneShot() {
|
|
24
188
|
const { browse } = await import('./src/index.js');
|
|
25
|
-
const url =
|
|
26
|
-
const mode =
|
|
189
|
+
const url = args[1];
|
|
190
|
+
const mode = args[2] || 'headless';
|
|
27
191
|
try {
|
|
28
192
|
const snapshot = await browse(url, { mode });
|
|
29
193
|
process.stdout.write(snapshot + '\n');
|
|
@@ -32,28 +196,50 @@ if (cmd === 'mcp') {
|
|
|
32
196
|
process.stderr.write(`Error: ${err.message}\n`);
|
|
33
197
|
process.exit(1);
|
|
34
198
|
}
|
|
199
|
+
}
|
|
35
200
|
|
|
36
|
-
|
|
37
|
-
|
|
201
|
+
async function runDaemonInternal() {
|
|
202
|
+
const { runDaemon } = await import('./src/daemon.js');
|
|
203
|
+
const opts = {
|
|
204
|
+
mode: parseFlag('--mode') || 'headless',
|
|
205
|
+
port: parseFlag('--port'),
|
|
206
|
+
cookies: !hasFlag('--no-cookies'),
|
|
207
|
+
browser: parseFlag('--browser'),
|
|
208
|
+
timeout: parseFlag('--timeout'),
|
|
209
|
+
pruneMode: parseFlag('--prune-mode') || 'act',
|
|
210
|
+
consent: !hasFlag('--no-consent'),
|
|
211
|
+
};
|
|
212
|
+
const outputDir = parseFlag('--output-dir') || resolve('.barebrowse');
|
|
213
|
+
const url = parseFlag('--url');
|
|
214
|
+
await runDaemon(opts, outputDir, url || undefined);
|
|
215
|
+
}
|
|
38
216
|
|
|
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
217
|
|
|
44
|
-
|
|
45
|
-
import { browse, connect } from 'barebrowse';
|
|
218
|
+
// --- Flag parsing helpers ---
|
|
46
219
|
|
|
47
|
-
|
|
48
|
-
|
|
220
|
+
function parseFlag(name) {
|
|
221
|
+
// --name=value or --name value
|
|
222
|
+
for (let i = 0; i < args.length; i++) {
|
|
223
|
+
if (args[i].startsWith(name + '=')) return args[i].slice(name.length + 1);
|
|
224
|
+
if (args[i] === name && args[i + 1] && !args[i + 1].startsWith('--')) return args[i + 1];
|
|
225
|
+
}
|
|
226
|
+
return undefined;
|
|
227
|
+
}
|
|
49
228
|
|
|
50
|
-
|
|
51
|
-
|
|
229
|
+
function hasFlag(name) {
|
|
230
|
+
return args.includes(name);
|
|
52
231
|
}
|
|
53
232
|
|
|
233
|
+
|
|
54
234
|
// --- MCP auto-installer ---
|
|
55
235
|
|
|
56
236
|
function install() {
|
|
237
|
+
// Handle --skill flag
|
|
238
|
+
if (hasFlag('--skill')) {
|
|
239
|
+
installSkill();
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
57
243
|
const mcpEntry = {
|
|
58
244
|
command: 'npx',
|
|
59
245
|
args: ['barebrowse', 'mcp'],
|
|
@@ -62,10 +248,7 @@ function install() {
|
|
|
62
248
|
const targets = detectTargets();
|
|
63
249
|
|
|
64
250
|
if (targets.length === 0) {
|
|
65
|
-
console.log('No MCP clients detected
|
|
66
|
-
console.log(JSON.stringify({ mcpServers: { barebrowse: mcpEntry } }, null, 2));
|
|
67
|
-
console.log('\nSupported clients: Claude Desktop, Cursor, Claude Code');
|
|
68
|
-
return;
|
|
251
|
+
console.log('No MCP clients detected.\n');
|
|
69
252
|
}
|
|
70
253
|
|
|
71
254
|
let installed = 0;
|
|
@@ -83,7 +266,6 @@ function install() {
|
|
|
83
266
|
|
|
84
267
|
config.mcpServers.barebrowse = mcpEntry;
|
|
85
268
|
|
|
86
|
-
// Ensure parent dir exists
|
|
87
269
|
const dir = join(target.path, '..');
|
|
88
270
|
mkdirSync(dir, { recursive: true });
|
|
89
271
|
|
|
@@ -97,8 +279,28 @@ function install() {
|
|
|
97
279
|
|
|
98
280
|
if (installed > 0) {
|
|
99
281
|
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
282
|
}
|
|
283
|
+
|
|
284
|
+
// Always print Claude Code hint (it uses `claude mcp add`, not JSON config)
|
|
285
|
+
console.log(`\nClaude Code: run this instead of install:`);
|
|
286
|
+
console.log(` claude mcp add barebrowse -- npx barebrowse mcp\n`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function installSkill() {
|
|
290
|
+
const thisDir = fileURLToPath(new URL('.', import.meta.url));
|
|
291
|
+
const src = join(thisDir, '.claude', 'skills', 'barebrowse', 'SKILL.md');
|
|
292
|
+
|
|
293
|
+
if (!existsSync(src)) {
|
|
294
|
+
console.error('SKILL.md not found in package. Reinstall barebrowse.');
|
|
295
|
+
process.exit(1);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const dest = join(homedir(), '.config', 'claude', 'skills', 'barebrowse', 'SKILL.md');
|
|
299
|
+
const destDir = join(dest, '..');
|
|
300
|
+
mkdirSync(destDir, { recursive: true });
|
|
301
|
+
copyFileSync(src, dest);
|
|
302
|
+
console.log(`Skill installed: ${dest}`);
|
|
303
|
+
console.log('Claude Code will now see barebrowse as an available skill.');
|
|
102
304
|
}
|
|
103
305
|
|
|
104
306
|
function detectTargets() {
|
|
@@ -116,7 +318,6 @@ function detectTargets() {
|
|
|
116
318
|
claudeDesktop = join(home, 'AppData', 'Roaming', 'Claude', 'claude_desktop_config.json');
|
|
117
319
|
}
|
|
118
320
|
if (claudeDesktop) {
|
|
119
|
-
// Check if Claude Desktop dir exists (even if config doesn't yet)
|
|
120
321
|
const dir = join(claudeDesktop, '..');
|
|
121
322
|
if (existsSync(dir)) {
|
|
122
323
|
targets.push({ name: 'Claude Desktop', path: claudeDesktop });
|
|
@@ -124,26 +325,11 @@ function detectTargets() {
|
|
|
124
325
|
}
|
|
125
326
|
|
|
126
327
|
// Cursor
|
|
127
|
-
|
|
128
|
-
if (
|
|
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)) {
|
|
328
|
+
const cursorDir = join(home, '.cursor');
|
|
329
|
+
if (existsSync(cursorDir)) {
|
|
136
330
|
targets.push({ name: 'Cursor', path: join(cursorDir, 'mcp.json') });
|
|
137
331
|
}
|
|
138
332
|
|
|
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
333
|
return targets;
|
|
148
334
|
}
|
|
149
335
|
|
|
@@ -154,3 +340,58 @@ function readJsonOrEmpty(path) {
|
|
|
154
340
|
return {};
|
|
155
341
|
}
|
|
156
342
|
}
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
// --- Usage ---
|
|
346
|
+
|
|
347
|
+
function printUsage() {
|
|
348
|
+
process.stdout.write(`barebrowse -- CDP-direct browsing for autonomous agents
|
|
349
|
+
|
|
350
|
+
Session:
|
|
351
|
+
barebrowse open [url] [flags] Open browser session
|
|
352
|
+
barebrowse close Close session
|
|
353
|
+
barebrowse status Check session status
|
|
354
|
+
|
|
355
|
+
Open flags:
|
|
356
|
+
--mode=headless|headed|hybrid Browser mode (default: headless)
|
|
357
|
+
--port=N CDP port for headed mode
|
|
358
|
+
--no-cookies Skip cookie injection
|
|
359
|
+
--browser=firefox|chromium Cookie source browser
|
|
360
|
+
--timeout=N Navigation timeout in ms
|
|
361
|
+
--prune-mode=act|read Default pruning mode
|
|
362
|
+
--no-consent Skip consent dismissal
|
|
363
|
+
|
|
364
|
+
Navigation:
|
|
365
|
+
barebrowse goto <url> Navigate to URL
|
|
366
|
+
barebrowse snapshot [--mode=M] ARIA snapshot -> .barebrowse/page-*.yml
|
|
367
|
+
barebrowse screenshot [--format] Screenshot -> .barebrowse/screenshot-*.png
|
|
368
|
+
|
|
369
|
+
Interaction:
|
|
370
|
+
barebrowse click <ref> Click element
|
|
371
|
+
barebrowse type <ref> <text> Type text (--clear to replace)
|
|
372
|
+
barebrowse fill <ref> <text> Clear + type
|
|
373
|
+
barebrowse press <key> Press key (Enter, Tab, Escape, ...)
|
|
374
|
+
barebrowse scroll <deltaY> Scroll (positive=down)
|
|
375
|
+
barebrowse hover <ref> Hover element
|
|
376
|
+
barebrowse select <ref> <value> Select dropdown value
|
|
377
|
+
|
|
378
|
+
Debugging:
|
|
379
|
+
barebrowse eval <expression> Run JS in page context
|
|
380
|
+
barebrowse wait-idle [--timeout] Wait for network idle
|
|
381
|
+
barebrowse console-logs Console logs -> .barebrowse/console-*.json
|
|
382
|
+
barebrowse network-log Network log -> .barebrowse/network-*.json
|
|
383
|
+
|
|
384
|
+
One-shot:
|
|
385
|
+
barebrowse browse <url> [mode] Browse + print snapshot to stdout
|
|
386
|
+
|
|
387
|
+
MCP:
|
|
388
|
+
barebrowse mcp Start MCP server (JSON-RPC over stdio)
|
|
389
|
+
barebrowse install Auto-configure MCP for Claude Desktop / Cursor
|
|
390
|
+
barebrowse install --skill Install SKILL.md for Claude Code
|
|
391
|
+
|
|
392
|
+
As a library:
|
|
393
|
+
import { browse, connect } from 'barebrowse';
|
|
394
|
+
|
|
395
|
+
More: see README.md or barebrowse.context.md
|
|
396
|
+
`);
|
|
397
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# barebrowse -- Assumptions & Constraints
|
|
2
|
+
|
|
3
|
+
## Hard constraints
|
|
4
|
+
|
|
5
|
+
| Constraint | Detail |
|
|
6
|
+
|-----------|--------|
|
|
7
|
+
| **Chromium-only** | CDP protocol. Covers Chrome, Chromium, Edge, Brave, Vivaldi, Arc, Opera (~80% desktop share). Firefox later via WebDriver BiDi. |
|
|
8
|
+
| **Node >= 22** | Built-in WebSocket (`globalThis.WebSocket`), built-in SQLite (`node:sqlite`). No polyfills. |
|
|
9
|
+
| **Linux first** | Tested on Fedora/KDE/Wayland. macOS/Windows cookie extraction paths exist in auth.js but are untested. |
|
|
10
|
+
| **Zero required deps** | Everything uses Node stdlib. Vanilla JS, ES modules, no build step. |
|
|
11
|
+
| **Not a server** | Library that agents import. MCP wrapper included, HTTP wrapper is DIY. |
|
|
12
|
+
|
|
13
|
+
## Assumptions
|
|
14
|
+
|
|
15
|
+
- **User has Chromium installed.** At least one of: chromium-browser, google-chrome, brave-browser, microsoft-edge. `chromium.js` searches common paths.
|
|
16
|
+
- **Cookie extraction needs unlocked profile.** Chromium cookies are AES-encrypted with a keyring key (KWallet on KDE, GNOME Keyring on GNOME). Firefox cookies are plaintext SQLite and always accessible.
|
|
17
|
+
- **Headed mode requires manual browser launch.** User must start their browser with `--remote-debugging-port=9222`. barebrowse connects to it -- does not launch it.
|
|
18
|
+
- **Hybrid fallback needs a running headed browser.** If headless is bot-blocked, hybrid kills headless and connects to headed on port 9222. That browser must already be running.
|
|
19
|
+
- **Cookies expire.** Cookie injection works for existing sessions, not new logins. For sites requiring fresh auth, headed mode with user interaction is the fallback.
|
|
20
|
+
- **One page per connect().** Each `connect()` call creates one page. For multiple tabs, call `connect()` multiple times.
|
|
21
|
+
|
|
22
|
+
## Known limitations
|
|
23
|
+
|
|
24
|
+
| Limitation | Impact | Workaround |
|
|
25
|
+
|-----------|--------|------------|
|
|
26
|
+
| No Firefox/WebKit support | ~20% of desktop users can't use native browser | Use Chromium as the automation target, Firefox as cookie source |
|
|
27
|
+
| No file upload | Can't interact with file inputs | Not yet implemented (`Input.setFiles` via CDP) |
|
|
28
|
+
| No drag and drop | Can't use drag-based UIs | Not yet implemented |
|
|
29
|
+
| No cross-origin iframes | Content inside iframes invisible to ARIA tree | Frame tree traversal via CDP (medium effort) |
|
|
30
|
+
| No CAPTCHAs | Cannot solve challenge pages | Headed mode lets user solve manually |
|
|
31
|
+
| Canvas/WebGL opaque | No ARIA representation | Needs screenshot + vision model |
|
|
32
|
+
| macOS/Windows untested | Cookie paths exist but may not work | Linux-only for now |
|
|
33
|
+
|
|
34
|
+
## Risks
|
|
35
|
+
|
|
36
|
+
- **CDP is not a stable API.** Chrome team can change it across versions. Mitigation: we use well-established domains (Accessibility, Input, Page, Network, DOM) that rarely break.
|
|
37
|
+
- **Cookie consent patterns evolve.** New consent frameworks may not be detected by `consent.js`. Mitigation: best-effort, opt-out with `{ consent: false }`.
|
|
38
|
+
- **Stealth patches are an arms race.** Bot detection evolves. Mitigation: headed mode with real browser profile is the ultimate fallback.
|
|
@@ -163,7 +163,7 @@ Every action returns a **pruned ARIA snapshot** -- the agent's view of the page
|
|
|
163
163
|
|
|
164
164
|
### Module table
|
|
165
165
|
|
|
166
|
-
|
|
166
|
+
Thirteen modules, zero required dependencies.
|
|
167
167
|
|
|
168
168
|
| Module | Lines | Purpose |
|
|
169
169
|
|---|---|---|
|
|
@@ -177,6 +177,8 @@ Eleven modules, 2,396 lines, zero required dependencies.
|
|
|
177
177
|
| `src/consent.js` | 210 | Auto-dismiss cookie consent dialogs, 7 languages |
|
|
178
178
|
| `src/stealth.js` | 51 | Navigator patches for headless anti-detection |
|
|
179
179
|
| `src/bareagent.js` | 161 | Tool adapter for bareagent Loop |
|
|
180
|
+
| `src/daemon.js` | ~230 | Background HTTP server holding connect() session for CLI mode |
|
|
181
|
+
| `src/session-client.js` | ~60 | HTTP client to daemon (sendCommand, readSession, isAlive) |
|
|
180
182
|
| `mcp-server.js` | 216 | MCP server (JSON-RPC 2.0 over stdio) |
|
|
181
183
|
|
|
182
184
|
---
|
|
@@ -254,11 +256,12 @@ Anti-detection for headless mode via `Page.addScriptToEvaluateOnNewDocument` (ru
|
|
|
254
256
|
- `Permissions.prototype.query` -> notifications return 'prompt'
|
|
255
257
|
- Applied automatically in headless mode
|
|
256
258
|
|
|
257
|
-
### Tests --
|
|
259
|
+
### Tests -- 64 passing
|
|
258
260
|
- 16 unit tests (pruning logic)
|
|
259
261
|
- 7 unit tests (cookie extraction -- 2 skip when Chromium profile locked)
|
|
260
262
|
- 5 unit tests (CDP client + browser launch)
|
|
261
263
|
- 11 integration tests (end-to-end browse pipeline)
|
|
264
|
+
- 10 integration tests (CLI session lifecycle: open/snapshot/goto/click/eval/console/network/close)
|
|
262
265
|
- 15 integration tests (real-world interactions: data: URL fixture + live sites)
|
|
263
266
|
|
|
264
267
|
---
|
|
@@ -302,6 +305,26 @@ Raw JSON-RPC 2.0 over stdio. Zero SDK dependencies. `npm install barebrowse` the
|
|
|
302
305
|
Action tools return `'ok'` -- agent calls `snapshot` explicitly (MCP tool calls are cheap to chain).
|
|
303
306
|
Session tools share a singleton page, lazy-created on first use.
|
|
304
307
|
|
|
308
|
+
### CLI session -- for coding agents + human devs
|
|
309
|
+
|
|
310
|
+
Shell commands that output to disk. Coding agents (Claude Code, Copilot, Cursor) read output files with their file tools -- no tokens wasted in tool responses.
|
|
311
|
+
|
|
312
|
+
```bash
|
|
313
|
+
barebrowse open https://example.com # Start daemon + navigate
|
|
314
|
+
barebrowse snapshot # → .barebrowse/page-*.yml
|
|
315
|
+
barebrowse click 8 # Click element
|
|
316
|
+
barebrowse console-logs # → .barebrowse/console-*.json
|
|
317
|
+
barebrowse close # Kill daemon + browser
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Architecture: `open` spawns a detached child process running an HTTP server on a random localhost port. Session state stored in `.barebrowse/session.json`. Subsequent commands POST to the daemon. `close` sends shutdown, daemon calls `page.close()` + `process.exit(0)`.
|
|
321
|
+
|
|
322
|
+
Full commands: open, close, status, goto, snapshot, screenshot, click, type, fill, press, scroll, hover, select, eval, wait-idle, console-logs, network-log.
|
|
323
|
+
|
|
324
|
+
Self-sufficiency features (console/network capture, eval) let agents debug without guessing -- they see JS errors and failed requests directly.
|
|
325
|
+
|
|
326
|
+
SKILL.md (`.claude/skills/barebrowse/SKILL.md`) teaches Claude Code the CLI commands. Install with `barebrowse install --skill`.
|
|
327
|
+
|
|
305
328
|
---
|
|
306
329
|
|
|
307
330
|
## Ecosystem
|
|
@@ -339,10 +362,12 @@ barebrowse/
|
|
|
339
362
|
│ ├── interact.js # Click, type, press, scroll, hover, select
|
|
340
363
|
│ ├── consent.js # Auto-dismiss cookie consent dialogs
|
|
341
364
|
│ ├── stealth.js # Navigator patches for headless anti-detection
|
|
342
|
-
│
|
|
365
|
+
│ ├── bareagent.js # Tool adapter for bareagent Loop
|
|
366
|
+
│ ├── daemon.js # Background HTTP server for CLI session
|
|
367
|
+
│ └── session-client.js # HTTP client to daemon
|
|
343
368
|
├── test/
|
|
344
369
|
│ ├── unit/ # prune, auth, cdp tests
|
|
345
|
-
│ └── integration/ # browse
|
|
370
|
+
│ └── integration/ # browse, interact, cli tests
|
|
346
371
|
├── examples/
|
|
347
372
|
│ ├── headed-demo.js # Interactive demo: Wikipedia → DuckDuckGo
|
|
348
373
|
│ └── yt-demo.js # YouTube demo: Firefox cookies → search → play video
|
|
@@ -352,7 +377,7 @@ barebrowse/
|
|
|
352
377
|
│ ├── blueprint.md # This file
|
|
353
378
|
│ └── testing.md # Test guide: pyramid, all 54 tests, CI strategy
|
|
354
379
|
├── mcp-server.js # MCP server (JSON-RPC 2.0 over stdio)
|
|
355
|
-
├── cli.js # CLI entry:
|
|
380
|
+
├── cli.js # CLI entry: session commands, MCP, browse, install
|
|
356
381
|
├── .mcp.json # MCP server config for Claude Desktop / Cursor
|
|
357
382
|
├── barebrowse.context.md # LLM-consumable integration guide
|
|
358
383
|
├── package.json
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# barebrowse -- Vision
|
|
2
|
+
|
|
3
|
+
## What it is
|
|
4
|
+
|
|
5
|
+
A standalone vanilla JavaScript library that gives autonomous agents authenticated access to the web through the user's own Chromium browser. One package, one import, three modes.
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
import { browse } from 'barebrowse';
|
|
9
|
+
const snapshot = await browse('https://any-page.com');
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
barebrowse handles: finding the browser, connecting via CDP, injecting cookies, navigating, extracting the ARIA accessibility tree, and pruning it down to what an agent actually needs. The output is a clean, token-efficient snapshot of any web page -- authenticated as the real user.
|
|
13
|
+
|
|
14
|
+
## What it is NOT
|
|
15
|
+
|
|
16
|
+
- **Not a framework.** No plugin system, no config files, no lifecycle hooks.
|
|
17
|
+
- **Not Playwright.** No bundled browser, no cross-engine abstraction, no 200MB download.
|
|
18
|
+
- **Not an agent.** No LLM, no planning, no orchestration -- that's bareagent's job.
|
|
19
|
+
- **Not a scraper.** It browses as the user, not as a bot harvesting data.
|
|
20
|
+
|
|
21
|
+
## The core insight
|
|
22
|
+
|
|
23
|
+
The user already has a browser. It's already logged in. It already passes Cloudflare. Instead of fighting the web with headless stealth tricks, **use what's already there**.
|
|
24
|
+
|
|
25
|
+
CDP (Chrome DevTools Protocol) lets us connect to any Chromium-based browser -- the same one the user browses with daily. We get their cookies, their sessions, their anti-detection posture, for free.
|
|
26
|
+
|
|
27
|
+
## The problem it solves
|
|
28
|
+
|
|
29
|
+
Every AI agent that needs to read or interact with the web hits the same walls:
|
|
30
|
+
|
|
31
|
+
1. **Cloudflare / bot detection** -- headless browsers get blocked
|
|
32
|
+
2. **Authentication** -- sites require login, OAuth, session cookies
|
|
33
|
+
3. **Token bloat** -- raw DOM is 100K+ tokens; agents need ~5K
|
|
34
|
+
4. **Two consumers, same need** -- research agents (read pages) and personal assistants (click/type) both need an authenticated browser, but existing tools force you to choose one path
|
|
35
|
+
|
|
36
|
+
## The bare- ecosystem
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
bareagent = the brain (orchestration, LLM loop, memory, retries)
|
|
40
|
+
barebrowse = the eyes + hands (browse, read, interact with the web)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
barebrowse is a library. bareagent imports it as a capability. barebrowse doesn't know about bareagent. bareagent doesn't know about CDP. Clean boundary. Each ships and tests independently.
|
|
44
|
+
|
|
45
|
+
## Success criteria
|
|
46
|
+
|
|
47
|
+
1. `browse(url)` returns a pruned ARIA snapshot of any page, authenticated as the user
|
|
48
|
+
2. Zero heavy dependencies -- no Playwright, no Puppeteer, no bundled browser
|
|
49
|
+
3. Works with any installed Chromium-based browser
|
|
50
|
+
4. Headless for research, headed for interaction, hybrid for autonomous agents
|
|
51
|
+
5. Plugs into bareagent as plain tool functions
|
|
52
|
+
6. An agent using barebrowse + bareagent can autonomously research the web and act on pages
|