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.
@@ -0,0 +1,137 @@
1
+ /**
2
+ * YouTube headed-mode demo — search and play "Family Portrait Pink"
3
+ *
4
+ * SETUP — run this command first in a separate terminal:
5
+ *
6
+ * chromium-browser --remote-debugging-port=9222 \
7
+ * --disable-notifications \
8
+ * --autoplay-policy=no-user-gesture-required \
9
+ * --use-fake-device-for-media-stream \
10
+ * --use-fake-ui-for-media-stream \
11
+ * --disable-features=MediaRouter
12
+ *
13
+ * Then run this script:
14
+ *
15
+ * node examples/yt-demo.js
16
+ *
17
+ * Uses Firefox cookies to bypass YouTube consent wall.
18
+ */
19
+
20
+ import { connect } from '../src/index.js';
21
+
22
+ function wait(ms = 2000) {
23
+ return new Promise((r) => setTimeout(r, ms));
24
+ }
25
+
26
+ function findRef(snapshot, role, nameFragment) {
27
+ const escaped = nameFragment.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
28
+ const re = new RegExp(`${role} "[^"]*${escaped}[^"]*".*?\\[ref=([^\\]]+)\\]`, 'i');
29
+ const m = snapshot.match(re);
30
+ return m ? m[1] : null;
31
+ }
32
+
33
+ function printSnap(snapshot, limit = 600) {
34
+ const t = snapshot.length > limit
35
+ ? snapshot.slice(0, limit) + `\n... (${snapshot.length - limit} more chars)`
36
+ : snapshot;
37
+ console.log(t);
38
+ }
39
+
40
+ async function main() {
41
+ console.log('=== YouTube Demo — Family Portrait by Pink ===\n');
42
+ console.log('Connecting to Chromium on port 9222...\n');
43
+
44
+ const page = await connect({ mode: 'headed', port: 9222 });
45
+
46
+ try {
47
+ // Step 1: Inject Firefox cookies for youtube.com (bypasses consent wall)
48
+ console.log('[1] Injecting Firefox cookies for youtube.com...');
49
+ await page.injectCookies('https://www.youtube.com', { browser: 'firefox' });
50
+ await wait(500);
51
+
52
+ // Step 2: Navigate to YouTube
53
+ console.log('[2] Navigating to YouTube...');
54
+ await page.goto('https://www.youtube.com');
55
+ await wait(2000);
56
+
57
+ // Step 3: Find the search box
58
+ console.log('[3] Taking snapshot to find search box...');
59
+ let snap = await page.snapshot();
60
+
61
+ let searchRef = findRef(snap, 'combobox', 'Search')
62
+ || findRef(snap, 'textbox', 'Search')
63
+ || findRef(snap, 'searchbox', 'Search');
64
+
65
+ if (!searchRef) {
66
+ console.log(' Could not find search box. Snapshot:');
67
+ printSnap(snap, 1000);
68
+ return;
69
+ }
70
+
71
+ // Step 4: Type the search query
72
+ console.log(`[4] Found search box ref=${searchRef}, typing query...`);
73
+ await page.click(searchRef);
74
+ await wait(500);
75
+ await page.type(searchRef, 'Family Portrait Pink', { clear: true });
76
+ await wait(1000);
77
+
78
+ // Step 5: Press Enter to search
79
+ // YouTube is an SPA — loadEventFired won't fire, so just wait for results to render
80
+ console.log('[5] Pressing Enter to search...');
81
+ await page.press('Enter');
82
+ await wait(3000);
83
+
84
+ // Step 6: Find the video in results
85
+ console.log('[6] Looking for Family Portrait in results...');
86
+ snap = await page.snapshot();
87
+
88
+ let videoRef = findRef(snap, 'link', 'Family Portrait')
89
+ || findRef(snap, 'link', 'family portrait');
90
+
91
+ if (!videoRef) {
92
+ console.log(' Could not find video link. Trying broader match...');
93
+ printSnap(snap, 1500);
94
+ // Try any link with "Pink" in it
95
+ videoRef = findRef(snap, 'link', 'Pink');
96
+ }
97
+
98
+ if (!videoRef) {
99
+ console.log(' No matching video found.');
100
+ return;
101
+ }
102
+
103
+ // Step 7: Click the video (SPA nav — no loadEventFired)
104
+ console.log(`[7] Found video ref=${videoRef}, clicking to play...`);
105
+ await page.click(videoRef);
106
+ await wait(4000);
107
+
108
+ // Step 8: Snapshot the video page
109
+ console.log('[8] Video page snapshot:\n');
110
+ snap = await page.snapshot();
111
+ printSnap(snap, 800);
112
+
113
+ console.log('\n=== Video should be playing! ===');
114
+ console.log('Press Ctrl+C to exit when done watching.\n');
115
+
116
+ // Keep alive so user can watch
117
+ await new Promise(() => {});
118
+ } finally {
119
+ await page.close();
120
+ }
121
+ }
122
+
123
+ main().catch((err) => {
124
+ if (err.message?.includes('ECONNREFUSED')) {
125
+ console.error('\nError: Could not connect to Chromium on port 9222.');
126
+ console.error('Start Chromium first:\n');
127
+ console.error(' chromium-browser --remote-debugging-port=9222 \\');
128
+ console.error(' --disable-notifications \\');
129
+ console.error(' --autoplay-policy=no-user-gesture-required \\');
130
+ console.error(' --use-fake-device-for-media-stream \\');
131
+ console.error(' --use-fake-ui-for-media-stream \\');
132
+ console.error(' --disable-features=MediaRouter\n');
133
+ } else {
134
+ console.error('\nError:', err.message || err);
135
+ }
136
+ process.exit(1);
137
+ });
package/mcp-server.js ADDED
@@ -0,0 +1,216 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * mcp-server.js — MCP server for barebrowse.
4
+ *
5
+ * Raw JSON-RPC 2.0 over stdio. No SDK dependency.
6
+ * 7 tools: browse (one-shot), goto, snapshot, click, type, press, scroll.
7
+ *
8
+ * Session tools share a singleton page, lazy-created on first use.
9
+ * Action tools return 'ok' — agent calls snapshot explicitly to observe.
10
+ */
11
+
12
+ import { browse, connect } from './src/index.js';
13
+
14
+ let _page = null;
15
+
16
+ async function getPage() {
17
+ if (!_page) _page = await connect();
18
+ return _page;
19
+ }
20
+
21
+ const TOOLS = [
22
+ {
23
+ name: 'browse',
24
+ description: 'One-shot: navigate to a URL and return a pruned ARIA snapshot. Stateless — does not use the session page.',
25
+ inputSchema: {
26
+ type: 'object',
27
+ properties: {
28
+ url: { type: 'string', description: 'URL to browse' },
29
+ mode: { type: 'string', enum: ['headless', 'headed', 'hybrid'], description: 'Browser mode (default: headless)' },
30
+ },
31
+ required: ['url'],
32
+ },
33
+ },
34
+ {
35
+ name: 'goto',
36
+ description: 'Navigate the session page to a URL. Returns ok — call snapshot to observe.',
37
+ inputSchema: {
38
+ type: 'object',
39
+ properties: {
40
+ url: { type: 'string', description: 'URL to navigate to' },
41
+ },
42
+ required: ['url'],
43
+ },
44
+ },
45
+ {
46
+ name: 'snapshot',
47
+ description: 'Get the current ARIA snapshot of the session page. Returns a YAML-like tree with [ref=N] markers on interactive elements.',
48
+ inputSchema: { type: 'object', properties: {} },
49
+ },
50
+ {
51
+ name: 'click',
52
+ description: 'Click an element by its ref from the snapshot. Returns ok — call snapshot to observe.',
53
+ inputSchema: {
54
+ type: 'object',
55
+ properties: {
56
+ ref: { type: 'string', description: 'Element ref from snapshot (e.g. "8")' },
57
+ },
58
+ required: ['ref'],
59
+ },
60
+ },
61
+ {
62
+ name: 'type',
63
+ description: 'Type text into an element by its ref. Returns ok — call snapshot to observe.',
64
+ inputSchema: {
65
+ type: 'object',
66
+ properties: {
67
+ ref: { type: 'string', description: 'Element ref from snapshot' },
68
+ text: { type: 'string', description: 'Text to type' },
69
+ clear: { type: 'boolean', description: 'Clear existing content first (default: false)' },
70
+ },
71
+ required: ['ref', 'text'],
72
+ },
73
+ },
74
+ {
75
+ name: 'press',
76
+ description: 'Press a special key (Enter, Tab, Escape, Backspace, Delete, ArrowUp/Down/Left/Right, Home, End, PageUp, PageDown, Space). Returns ok.',
77
+ inputSchema: {
78
+ type: 'object',
79
+ properties: {
80
+ key: { type: 'string', description: 'Key name' },
81
+ },
82
+ required: ['key'],
83
+ },
84
+ },
85
+ {
86
+ name: 'scroll',
87
+ description: 'Scroll the page. Positive deltaY scrolls down, negative scrolls up. Returns ok.',
88
+ inputSchema: {
89
+ type: 'object',
90
+ properties: {
91
+ deltaY: { type: 'number', description: 'Pixels to scroll (positive=down, negative=up)' },
92
+ },
93
+ required: ['deltaY'],
94
+ },
95
+ },
96
+ ];
97
+
98
+ async function handleToolCall(name, args) {
99
+ switch (name) {
100
+ case 'browse':
101
+ return await browse(args.url, { mode: args.mode });
102
+
103
+ case 'goto': {
104
+ const page = await getPage();
105
+ await page.goto(args.url);
106
+ return 'ok';
107
+ }
108
+ case 'snapshot': {
109
+ const page = await getPage();
110
+ return await page.snapshot();
111
+ }
112
+ case 'click': {
113
+ const page = await getPage();
114
+ await page.click(args.ref);
115
+ return 'ok';
116
+ }
117
+ case 'type': {
118
+ const page = await getPage();
119
+ await page.type(args.ref, args.text, { clear: args.clear });
120
+ return 'ok';
121
+ }
122
+ case 'press': {
123
+ const page = await getPage();
124
+ await page.press(args.key);
125
+ return 'ok';
126
+ }
127
+ case 'scroll': {
128
+ const page = await getPage();
129
+ await page.scroll(args.deltaY);
130
+ return 'ok';
131
+ }
132
+ default:
133
+ throw new Error(`Unknown tool: ${name}`);
134
+ }
135
+ }
136
+
137
+ function jsonrpcResponse(id, result) {
138
+ return JSON.stringify({ jsonrpc: '2.0', id, result });
139
+ }
140
+
141
+ function jsonrpcError(id, code, message) {
142
+ return JSON.stringify({ jsonrpc: '2.0', id, error: { code, message } });
143
+ }
144
+
145
+ async function handleMessage(msg) {
146
+ const { id, method, params } = msg;
147
+
148
+ if (method === 'initialize') {
149
+ return jsonrpcResponse(id, {
150
+ protocolVersion: '2024-11-05',
151
+ capabilities: { tools: {} },
152
+ serverInfo: { name: 'barebrowse', version: '0.2.0' },
153
+ });
154
+ }
155
+
156
+ if (method === 'notifications/initialized') {
157
+ return null; // notification, no response
158
+ }
159
+
160
+ if (method === 'tools/list') {
161
+ return jsonrpcResponse(id, { tools: TOOLS });
162
+ }
163
+
164
+ if (method === 'tools/call') {
165
+ const { name, arguments: args } = params;
166
+ try {
167
+ const result = await handleToolCall(name, args || {});
168
+ return jsonrpcResponse(id, {
169
+ content: [{ type: 'text', text: typeof result === 'string' ? result : JSON.stringify(result) }],
170
+ });
171
+ } catch (err) {
172
+ return jsonrpcResponse(id, {
173
+ content: [{ type: 'text', text: `Error: ${err.message}` }],
174
+ isError: true,
175
+ });
176
+ }
177
+ }
178
+
179
+ return jsonrpcError(id, -32601, `Method not found: ${method}`);
180
+ }
181
+
182
+ // --- Stdio transport ---
183
+
184
+ let buffer = '';
185
+
186
+ process.stdin.setEncoding('utf8');
187
+ process.stdin.on('data', async (chunk) => {
188
+ buffer += chunk;
189
+ let newlineIdx;
190
+ while ((newlineIdx = buffer.indexOf('\n')) !== -1) {
191
+ const line = buffer.slice(0, newlineIdx).trim();
192
+ buffer = buffer.slice(newlineIdx + 1);
193
+ if (!line) continue;
194
+
195
+ try {
196
+ const msg = JSON.parse(line);
197
+ const response = await handleMessage(msg);
198
+ if (response) {
199
+ process.stdout.write(response + '\n');
200
+ }
201
+ } catch (err) {
202
+ process.stdout.write(jsonrpcError(null, -32700, `Parse error: ${err.message}`) + '\n');
203
+ }
204
+ }
205
+ });
206
+
207
+ // Clean up on exit
208
+ process.on('SIGINT', async () => {
209
+ if (_page) await _page.close().catch(() => {});
210
+ process.exit(0);
211
+ });
212
+
213
+ process.on('SIGTERM', async () => {
214
+ if (_page) await _page.close().catch(() => {});
215
+ process.exit(0);
216
+ });
package/package.json CHANGED
@@ -1,19 +1,32 @@
1
1
  {
2
2
  "name": "barebrowse",
3
- "version": "0.1.0",
4
- "description": "Authenticated web browsing for autonomous agents via CDP",
3
+ "version": "0.2.0",
4
+ "description": "Authenticated web browsing for autonomous agents via CDP. URL in, pruned ARIA snapshot out.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
7
- "exports": "./src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js",
9
+ "./bareagent": "./src/bareagent.js"
10
+ },
11
+ "bin": {
12
+ "barebrowse": "./cli.js"
13
+ },
8
14
  "engines": {
9
15
  "node": ">=22"
10
16
  },
11
17
  "scripts": {
12
- "test": "node --test test/**/*.test.js"
18
+ "test": "node --test test/unit/*.test.js test/integration/*.test.js"
13
19
  },
14
- "license": "MIT",
15
- "files": [
16
- "README.md",
17
- "LICENSE"
18
- ]
20
+ "keywords": [
21
+ "browser",
22
+ "cdp",
23
+ "chromium",
24
+ "aria",
25
+ "accessibility",
26
+ "web-scraping",
27
+ "agent",
28
+ "mcp",
29
+ "headless"
30
+ ],
31
+ "license": "MIT"
19
32
  }
package/src/aria.js ADDED
@@ -0,0 +1,69 @@
1
+ /**
2
+ * aria.js — Format ARIA accessibility tree nodes for agent consumption.
3
+ *
4
+ * Takes a nested tree (built from CDP's Accessibility.getFullAXTree)
5
+ * and formats it as readable YAML-like text, similar to Playwright's ariaSnapshot.
6
+ */
7
+
8
+ /**
9
+ * Format a nested ARIA tree as readable text output.
10
+ *
11
+ * Output format (one node per line, indented):
12
+ * - role "name" [props] [ref=nodeId]
13
+ *
14
+ * @param {object} node - Tree node { role, name, properties, children, ignored, nodeId }
15
+ * @param {number} [depth=0] - Current indentation depth
16
+ * @returns {string} Formatted ARIA tree text
17
+ */
18
+ export function formatTree(node, depth = 0) {
19
+ if (!node) return '';
20
+
21
+ // Skip ignored nodes but still process their children
22
+ if (node.ignored) {
23
+ return node.children.map((c) => formatTree(c, depth)).join('');
24
+ }
25
+
26
+ // Skip low-level rendering nodes that are noise for agents
27
+ const SKIP_ROLES = new Set(['InlineTextBox', 'LineBreak']);
28
+ if (SKIP_ROLES.has(node.role)) return '';
29
+
30
+ const indent = ' '.repeat(depth);
31
+ const lines = [];
32
+
33
+ // Build line: "- role "name" [properties] [ref=id]"
34
+ let line = `${indent}- ${node.role || 'none'}`;
35
+
36
+ if (node.name) {
37
+ line += ` "${node.name}"`;
38
+ }
39
+
40
+ // Notable properties that agents care about
41
+ const props = node.properties || {};
42
+ const propParts = [];
43
+ if (props.checked !== undefined) propParts.push(`checked=${props.checked}`);
44
+ if (props.disabled) propParts.push('disabled');
45
+ if (props.expanded !== undefined) propParts.push(`expanded=${props.expanded}`);
46
+ if (props.level) propParts.push(`level=${props.level}`);
47
+ if (props.selected) propParts.push('selected');
48
+ if (props.required) propParts.push('required');
49
+ if (props.value !== undefined && props.value !== '') propParts.push(`value="${props.value}"`);
50
+
51
+ if (propParts.length > 0) {
52
+ line += ` [${propParts.join(', ')}]`;
53
+ }
54
+
55
+ // Node ID as ref — agents use this to target interactions
56
+ if (node.nodeId) {
57
+ line += ` [ref=${node.nodeId}]`;
58
+ }
59
+
60
+ lines.push(line);
61
+
62
+ // Recurse into children
63
+ for (const child of node.children) {
64
+ const childText = formatTree(child, depth + 1);
65
+ if (childText) lines.push(childText);
66
+ }
67
+
68
+ return lines.join('\n');
69
+ }