barebrowse 0.5.9 → 0.6.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/CHANGELOG.md CHANGED
@@ -1,5 +1,62 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.1
4
+
5
+ Headed fallback is now a per-navigation escape hatch, not a permanent mode switch. Graceful degradation when headed is unavailable.
6
+
7
+ ### Switch-back to headless (`src/index.js`)
8
+ - `connect().goto()` in hybrid mode: if currently headed from a previous fallback, kills the headed browser and launches fresh headless before navigating
9
+ - New `currentlyHeaded` runtime state variable tracks actual browser mode (vs `mode` which is user config)
10
+ - `createPage()` stealth decision uses runtime mode (`!currentlyHeaded`) instead of config mode (`mode !== 'headed'`)
11
+ - `createTab()` also uses `currentlyHeaded` for correct stealth application
12
+
13
+ ### Graceful degradation (`src/index.js`)
14
+ - `connect().goto()` hybrid fallback wrapped in try/catch — if `launch({ headed: true })` fails (no `$DISPLAY`, no Wayland, CI/Docker), keeps the headless result with `botBlocked: true` and `[BOT CHALLENGE DETECTED]` warning
15
+ - `browse()` hybrid fallback also wrapped in try/catch — same graceful degradation for one-shot browsing
16
+ - No crash on headless-only environments
17
+
18
+ ### Flow after changes
19
+ ```
20
+ goto(url) in hybrid mode:
21
+ 1. If currently headed → kill headed, launch headless, reset currentlyHeaded
22
+ 2. Navigate to url
23
+ 3. Check bot-blocked
24
+ 4. If bot-blocked → TRY launch headed (set currentlyHeaded=true)
25
+ → CATCH: headed unavailable, keep headless result
26
+ ```
27
+
28
+ ### Docs
29
+ - Updated hybrid mode descriptions in barebrowse.context.md, system-state.md, prd.md
30
+
31
+ ### Tests
32
+ - All existing tests pass (tests use headless mode, unaffected by hybrid logic)
33
+
34
+ ## 0.6.0
35
+
36
+ Self-launching headed fallback. Headed and hybrid modes no longer require a manually-launched browser on port 9222 — barebrowse auto-launches a visible Chromium window via `launch({ headed: true })`.
37
+
38
+ ### Headed mode auto-launch (`src/chromium.js`)
39
+ - `launch()` accepts `headed` option — skips `--headless=new` and `--hide-scrollbars` flags
40
+ - Same temp profile, same random port, same CDP parsing, same process return
41
+
42
+ ### Hybrid fallback fix (`src/index.js`)
43
+ - All 4 `getDebugUrl(port)` call sites replaced with `launch({ headed: true, proxy })` + `createCDP(browser.wsUrl)`
44
+ - `browse()` headed branch, `browse()` hybrid fallback, `connect()` headed branch, `connect().goto()` hybrid fallback
45
+ - `getDebugUrl` import removed from index.js (still exported from chromium.js for external use)
46
+ - Hybrid mode now actually works — previously it tried to connect to port 9222 which nobody ran
47
+
48
+ ### Assess handler simplified (`mcp-server.js`)
49
+ - Removed dual-path `runAssess(headed)` function (~60 lines of broken headed fallback)
50
+ - Assess now uses the session's hybrid mode: if tab is bot-blocked, triggers headed fallback via main page `goto()`, then retries in a new tab
51
+ - One flow, no separate `connect({ mode: 'headed' })` call
52
+
53
+ ### Docs
54
+ - Removed all "launch browser with --remote-debugging-port=9222" instructions
55
+ - Updated headed/hybrid mode descriptions across barebrowse.context.md, README.md, system-state.md, prd.md
56
+
57
+ ### Tests
58
+ - 71/71 passing — no test changes needed (all tests use headless mode)
59
+
3
60
  ## 0.5.8
4
61
 
5
62
  Bot challenge detection for all browsing, not just assess.
package/README.md CHANGED
@@ -100,8 +100,8 @@ For code examples, API reference, and wiring instructions, see **[barebrowse.con
100
100
  | Mode | What happens | Best for |
101
101
  |------|-------------|----------|
102
102
  | **Headless** (default) | Launches a fresh Chromium, no UI | Fast automation, scraping, reading pages |
103
- | **Headed** | Connects to your running browser on CDP port | Bot-detected sites, visual debugging, CAPTCHAs |
104
- | **Hybrid** | Tries headless first, falls back to headed if blocked | General-purpose agent browsing |
103
+ | **Headed** | Auto-launches a visible Chromium window | Bot-detected sites, visual debugging, CAPTCHAs |
104
+ | **Hybrid** | Tries headless first, auto-launches headed if blocked | General-purpose agent browsing |
105
105
 
106
106
  ## What it handles automatically
107
107
 
@@ -1,7 +1,7 @@
1
1
  # barebrowse -- Integration Guide
2
2
 
3
3
  > For AI assistants and developers wiring barebrowse into a project.
4
- > v0.5.8 | Node.js >= 22 | 0 required deps | MIT
4
+ > v0.6.1 | Node.js >= 22 | 0 required deps | MIT
5
5
 
6
6
  ## What this is
7
7
 
@@ -23,10 +23,8 @@ Three integration paths:
23
23
  | Mode | What it does | When to use |
24
24
  |---|---|---|
25
25
  | `headless` (default) | Launches a fresh Chromium, no UI | Scraping, reading, fast automation |
26
- | `headed` | Connects to user's running browser on CDP port | Bot-detected sites, debugging, visual tasks |
27
- | `hybrid` | Tries headless first, falls back to headed if blocked | General-purpose agent browsing |
28
-
29
- Headed mode requires the browser to be launched with `--remote-debugging-port=9222`.
26
+ | `headed` | Auto-launches a visible Chromium window | Bot-detected sites, debugging, visual tasks |
27
+ | `hybrid` | Tries headless first, headed fallback per-navigation (switches back to headless next time) | General-purpose agent browsing |
30
28
 
31
29
  ## Minimal usage: one-shot browse
32
30
 
@@ -45,13 +43,12 @@ const snapshot = await browse('https://example.com', {
45
43
  pruneMode: 'act', // 'act' (interactive elements) | 'read' (all content)
46
44
  consent: true, // auto-dismiss cookie consent dialogs
47
45
  timeout: 30000, // navigation timeout in ms
48
- port: 9222, // CDP port for headed/hybrid mode
49
46
  });
50
47
  ```
51
48
 
52
49
  ## connect() API
53
50
 
54
- `connect(opts)` returns a page handle for interactive sessions. Same opts as `browse()` for mode/port. Supports `hybrid` mode — starts headless, falls back to headed on bot detection (same as `browse()`).
51
+ `connect(opts)` returns a page handle for interactive sessions. Same opts as `browse()` for mode. Supports `hybrid` mode — starts headless, auto-launches headed on bot detection (same as `browse()`).
55
52
 
56
53
  | Method | Args | Returns | Notes |
57
54
  |---|---|---|---|
@@ -312,13 +309,13 @@ Useful for agent threshold decisions: "skip sites above score 40", "warn if term
312
309
 
313
310
  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.
314
311
 
315
- 4. **Headed mode requires manual browser launch.** Start your browser with `--remote-debugging-port=9222`. barebrowse connects to it -- it does not launch it.
312
+ 4. **Headed mode auto-launches Chromium.** No need to start a browser manually barebrowse launches a headed Chromium instance with CDP enabled automatically.
316
313
 
317
314
  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.
318
315
 
319
- 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.
316
+ 6. **Hybrid mode is per-navigation.** If headless is bot-blocked, hybrid kills headless and launches headed for that URL. On the next `goto()`, it switches back to headless automatically. If headed can't launch (no display CI, Docker), it degrades gracefully with the headless result and a `[BOT CHALLENGE DETECTED]` warning.
320
317
 
321
- 7. **One page per connect().** Each `connect()` call creates one page. For multiple tabs, call `connect()` multiple times.
318
+ 7. **One page per connect(), but tabs are supported.** Each `connect()` call creates one page. Use `createTab()` for additional tabs in the same browser.
322
319
 
323
320
  8. **Consent dismiss is best-effort.** It handles 16+ tested sites across 29 languages but novel consent implementations may need manual handling. Disable with `{ consent: false }`.
324
321
 
package/mcp-server.js CHANGED
@@ -286,61 +286,45 @@ async function handleToolCall(name, args) {
286
286
  if (!assessFn) throw new Error('wearehere is not installed. Run: npm install wearehere');
287
287
  const releaseSlot = await acquireAssessSlot();
288
288
  try {
289
- const runAssess = async (headed) => {
290
- let tab;
291
- if (headed) {
292
- tab = await connect({ mode: 'headed' });
293
- } else {
294
- const page = await getPage();
295
- tab = await page.createTab();
296
- }
297
- let timer;
298
- try {
299
- const result = await Promise.race([
300
- (async () => {
301
- await tab.injectCookies(args.url).catch(() => {});
302
- return await assessFn(tab, args.url, { timeout: args.timeout, settle: args.settle });
303
- })(),
304
- new Promise((_, reject) => {
305
- timer = setTimeout(() => {
306
- tab.close().catch(() => {});
307
- reject(new Error('assess timeout'));
308
- }, 30000);
309
- }),
310
- ]);
311
- clearTimeout(timer);
312
- const wasBotBlocked = tab.botBlocked;
313
- await tab.close().catch(() => {});
314
- return { result, botBlocked: wasBotBlocked };
315
- } catch (err) {
316
- clearTimeout(timer);
317
- await tab.close().catch(() => {});
318
- throw err;
319
- }
320
- };
321
-
322
- // Try headless first
289
+ const page = await getPage();
290
+ const tab = await page.createTab();
291
+ let timer;
323
292
  try {
324
- const { result, botBlocked } = await runAssess(false);
325
- if (botBlocked) {
326
- // Bot-blocked in headless retry headed
293
+ await tab.injectCookies(args.url).catch(() => {});
294
+ const result = await Promise.race([
295
+ assessFn(tab, args.url, { timeout: args.timeout, settle: args.settle }),
296
+ new Promise((_, rej) => { timer = setTimeout(() => rej(new Error('assess timeout')), 30000); }),
297
+ ]);
298
+ clearTimeout(timer);
299
+ if (tab.botBlocked) {
300
+ // Bot-blocked — trigger hybrid fallback via main page, retry in new tab
301
+ await tab.close().catch(() => {});
302
+ await page.goto(args.url);
303
+ const tab2 = await page.createTab();
304
+ let timer2;
327
305
  try {
328
- const headed = await runAssess(true);
329
- return JSON.stringify(headed.result, null, 2);
330
- } catch {
331
- return JSON.stringify(result, null, 2); // headed failed, return headless result
306
+ await tab2.injectCookies(args.url).catch(() => {});
307
+ const r2 = await Promise.race([
308
+ assessFn(tab2, args.url, { timeout: args.timeout, settle: args.settle }),
309
+ new Promise((_, rej) => { timer2 = setTimeout(() => rej(new Error('assess timeout')), 30000); }),
310
+ ]);
311
+ clearTimeout(timer2);
312
+ if (tab2.botBlocked) r2._warning = 'Bot-blocked in both modes. Score may be unreliable.';
313
+ await tab2.close().catch(() => {});
314
+ return JSON.stringify(r2, null, 2);
315
+ } catch (err2) {
316
+ clearTimeout(timer2);
317
+ await tab2.close().catch(() => {});
318
+ throw err2;
332
319
  }
333
320
  }
321
+ await tab.close().catch(() => {});
334
322
  return JSON.stringify(result, null, 2);
335
323
  } catch (err) {
324
+ clearTimeout(timer);
325
+ await tab.close().catch(() => {});
336
326
  if (isCdpDead(err)) _page = null;
337
- // Headless crashed — try headed
338
- try {
339
- const headed = await runAssess(true);
340
- return JSON.stringify(headed.result, null, 2);
341
- } catch (retryErr) {
342
- throw retryErr;
343
- }
327
+ throw err;
344
328
  }
345
329
  } finally {
346
330
  releaseSlot();
@@ -366,7 +350,7 @@ async function handleMessage(msg) {
366
350
  return jsonrpcResponse(id, {
367
351
  protocolVersion: '2024-11-05',
368
352
  capabilities: { tools: {} },
369
- serverInfo: { name: 'barebrowse', version: '0.5.9' },
353
+ serverInfo: { name: 'barebrowse', version: '0.6.0' },
370
354
  });
371
355
  }
372
356
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "barebrowse",
3
- "version": "0.5.9",
3
+ "version": "0.6.1",
4
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",
package/src/chromium.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * chromium.js — Find, launch, and connect to Chromium-based browsers.
3
3
  *
4
4
  * Supports: Chrome, Chromium, Brave, Edge, Vivaldi, Arc, Opera.
5
- * Modes: headless (launch new), headed (connect to running).
5
+ * Modes: headless (launch new, no UI), headed (launch new, visible window).
6
6
  */
7
7
 
8
8
  import { execSync, spawn } from 'node:child_process';
@@ -55,11 +55,12 @@ export function findBrowser() {
55
55
  }
56
56
 
57
57
  /**
58
- * Launch a headless Chromium instance with CDP enabled.
58
+ * Launch a Chromium instance with CDP enabled.
59
59
  * @param {object} [opts]
60
60
  * @param {string} [opts.binary] - Path to browser binary (auto-detected if omitted)
61
61
  * @param {number} [opts.port=0] - CDP port (0 = random available port)
62
62
  * @param {string} [opts.userDataDir] - Browser profile directory
63
+ * @param {boolean} [opts.headed=false] - Launch in headed mode (with visible window)
63
64
  * @returns {Promise<{wsUrl: string, process: ChildProcess, port: number}>}
64
65
  */
65
66
  export async function launch(opts = {}) {
@@ -67,7 +68,6 @@ export async function launch(opts = {}) {
67
68
  const port = opts.port || 0;
68
69
 
69
70
  const args = [
70
- '--headless=new',
71
71
  `--remote-debugging-port=${port}`,
72
72
  '--no-first-run',
73
73
  '--no-default-browser-check',
@@ -75,7 +75,8 @@ export async function launch(opts = {}) {
75
75
  '--disable-sync',
76
76
  '--disable-translate',
77
77
  '--mute-audio',
78
- '--hide-scrollbars',
78
+ // Headless-only flags
79
+ ...(!opts.headed ? ['--headless=new', '--hide-scrollbars'] : []),
79
80
  // Suppress permission prompts (location, notifications, camera, mic, etc.)
80
81
  '--disable-notifications',
81
82
  '--autoplay-policy=no-user-gesture-required',
package/src/index.js CHANGED
@@ -8,7 +8,7 @@
8
8
  * const snapshot = await browse('https://example.com');
9
9
  */
10
10
 
11
- import { launch, getDebugUrl } from './chromium.js';
11
+ import { launch } from './chromium.js';
12
12
  import { createCDP } from './cdp.js';
13
13
  import { formatTree } from './aria.js';
14
14
  import { authenticate } from './auth.js';
@@ -27,7 +27,6 @@ import { applyStealth } from './stealth.js';
27
27
  * @param {boolean} [opts.cookies=true] - Inject user's cookies (Phase 2)
28
28
  * @param {boolean} [opts.prune=true] - Apply ARIA pruning (Phase 2)
29
29
  * @param {number} [opts.timeout=30000] - Navigation timeout in ms
30
- * @param {number} [opts.port] - CDP port for headed mode
31
30
  * @returns {Promise<string>} ARIA snapshot text
32
31
  */
33
32
  export async function browse(url, opts = {}) {
@@ -40,9 +39,8 @@ export async function browse(url, opts = {}) {
40
39
  try {
41
40
  // Step 1: Get a CDP connection
42
41
  if (mode === 'headed') {
43
- const port = opts.port || 9222;
44
- const wsUrl = await getDebugUrl(port);
45
- cdp = await createCDP(wsUrl);
42
+ browser = await launch({ headed: true, proxy: opts.proxy });
43
+ cdp = await createCDP(browser.wsUrl);
46
44
  } else {
47
45
  // headless or hybrid (start headless)
48
46
  browser = await launch({ proxy: opts.proxy });
@@ -81,17 +79,20 @@ export async function browse(url, opts = {}) {
81
79
  cdp.close();
82
80
  if (browser) { browser.process.kill(); browser = null; }
83
81
 
84
- const port = opts.port || 9222;
85
- const wsUrl = await getDebugUrl(port);
86
- cdp = await createCDP(wsUrl);
87
- page = await createPage(cdp, false, { viewport: opts.viewport });
88
- await suppressPermissions(cdp);
89
- if (opts.cookies !== false) {
90
- try { await authenticate(page.session, url, { browser: opts.browser }); } catch {}
82
+ try {
83
+ browser = await launch({ headed: true, proxy: opts.proxy });
84
+ cdp = await createCDP(browser.wsUrl);
85
+ page = await createPage(cdp, false, { viewport: opts.viewport });
86
+ await suppressPermissions(cdp);
87
+ if (opts.cookies !== false) {
88
+ try { await authenticate(page.session, url, { browser: opts.browser }); } catch {}
89
+ }
90
+ await navigate(page, url, timeout);
91
+ if (opts.consent !== false) await dismissConsent(page.session);
92
+ ({ tree } = await ariaTree(page));
93
+ } catch {
94
+ // Headed launch failed (no display?) — return headless result as-is
91
95
  }
92
- await navigate(page, url, timeout);
93
- if (opts.consent !== false) await dismissConsent(page.session);
94
- ({ tree } = await ariaTree(page));
95
96
  }
96
97
 
97
98
  // Step 6: Prune for agent consumption
@@ -121,7 +122,6 @@ export async function browse(url, opts = {}) {
121
122
  *
122
123
  * @param {object} [opts]
123
124
  * @param {'headless'|'headed'|'hybrid'} [opts.mode='headless'] - Browser mode
124
- * @param {number} [opts.port=9222] - CDP port for headed mode
125
125
  * @returns {Promise<object>} Page handle with goto, snapshot, close
126
126
  */
127
127
  export async function connect(opts = {}) {
@@ -130,15 +130,15 @@ export async function connect(opts = {}) {
130
130
  let cdp;
131
131
 
132
132
  if (mode === 'headed') {
133
- const port = opts.port || 9222;
134
- const wsUrl = await getDebugUrl(port);
135
- cdp = await createCDP(wsUrl);
133
+ browser = await launch({ headed: true, proxy: opts.proxy });
134
+ cdp = await createCDP(browser.wsUrl);
136
135
  } else {
137
136
  browser = await launch({ proxy: opts.proxy });
138
137
  cdp = await createCDP(browser.wsUrl);
139
138
  }
140
139
 
141
- let page = await createPage(cdp, mode !== 'headed', { viewport: opts.viewport });
140
+ let currentlyHeaded = (mode === 'headed');
141
+ let page = await createPage(cdp, !currentlyHeaded, { viewport: opts.viewport });
142
142
  let refMap = new Map();
143
143
  let botBlocked = false;
144
144
 
@@ -175,6 +175,20 @@ export async function connect(opts = {}) {
175
175
 
176
176
  return {
177
177
  async goto(url, timeout = 30000) {
178
+ // Switch back to headless if we fell back to headed previously
179
+ if (currentlyHeaded && mode === 'hybrid') {
180
+ await cdp.send('Target.closeTarget', { targetId: page.targetId });
181
+ cdp.close();
182
+ if (browser) { browser.process.kill(); browser = null; }
183
+
184
+ browser = await launch({ proxy: opts.proxy });
185
+ cdp = await createCDP(browser.wsUrl);
186
+ page = await createPage(cdp, true, { viewport: opts.viewport });
187
+ setupDialogHandler(page.session);
188
+ await suppressPermissions(cdp);
189
+ currentlyHeaded = false;
190
+ }
191
+
178
192
  await navigate(page, url, timeout);
179
193
  if (opts.consent !== false) {
180
194
  await dismissConsent(page.session);
@@ -190,18 +204,22 @@ export async function connect(opts = {}) {
190
204
  cdp.close();
191
205
  if (browser) { browser.process.kill(); browser = null; }
192
206
 
193
- const port = opts.port || 9222;
194
- const wsUrl = await getDebugUrl(port);
195
- cdp = await createCDP(wsUrl);
196
- page = await createPage(cdp, false, { viewport: opts.viewport });
197
- setupDialogHandler(page.session);
198
- await suppressPermissions(cdp);
199
- await navigate(page, url, timeout);
200
- if (opts.consent !== false) await dismissConsent(page.session);
201
-
202
- // Re-check after headed fallback
203
- const after = await ariaTree(page);
204
- botBlocked = isChallengePage(after.tree, after.nodeCount);
207
+ try {
208
+ browser = await launch({ headed: true, proxy: opts.proxy });
209
+ cdp = await createCDP(browser.wsUrl);
210
+ page = await createPage(cdp, false, { viewport: opts.viewport });
211
+ setupDialogHandler(page.session);
212
+ await suppressPermissions(cdp);
213
+ await navigate(page, url, timeout);
214
+ if (opts.consent !== false) await dismissConsent(page.session);
215
+
216
+ // Re-check after headed fallback
217
+ const after = await ariaTree(page);
218
+ botBlocked = isChallengePage(after.tree, after.nodeCount);
219
+ currentlyHeaded = true;
220
+ } catch {
221
+ // Headed launch failed (no display?) — keep headless result, botBlocked stays true
222
+ }
205
223
  }
206
224
  },
207
225
 
@@ -375,7 +393,7 @@ export async function connect(opts = {}) {
375
393
  cdp: page.session,
376
394
 
377
395
  async createTab() {
378
- const tab = await createPage(cdp, mode !== 'headed', { viewport: opts.viewport });
396
+ const tab = await createPage(cdp, !currentlyHeaded, { viewport: opts.viewport });
379
397
  await suppressPermissions(cdp);
380
398
  let tabBotBlocked = false;
381
399
  return {