barebrowse 0.9.0 → 0.9.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,41 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.1
4
+
5
+ ### Pruning — `pruneMode` reaches MCP / bareagent and `read` finally works
6
+
7
+ - **`mode: 'read'` is now a real alias for `mode: 'browse'`** in `prune()`.
8
+ Previously, the CLI (`barebrowse snapshot --mode=read`) and the SKILL.md
9
+ advertised a `read` mode that did not exist — `MODE_REGIONS[mode] ||
10
+ MODE_REGIONS.act` silently fell back to act-mode pruning. Articles, docs,
11
+ and blog posts therefore came back gutted no matter which mode the agent
12
+ asked for, which is why Claude tended to give up and fall back to
13
+ WebFetch. One-line alias at the top of `prune()` fixes it; `act|browse|
14
+ navigate|full` still behave unchanged.
15
+ - **MCP `browse` and `snapshot` tools gained a `pruneMode: 'act'|'read'`
16
+ parameter** (mcp-server.js). Before this, the MCP surface had no way to
17
+ ask for any mode other than `act` — `browse`'s `mode` param was browser
18
+ mode (headless/headed/hybrid), and `snapshot` accepted only `maxChars`.
19
+ Tool descriptions now tell the caller when to pick `read` (content-heavy
20
+ pages: articles, docs, blogs).
21
+ - **bareagent `browse` and `snapshot` tools gained the same `pruneMode`
22
+ parameter** (`src/bareagent.js`) with identical semantics. The `browse`
23
+ handler preserves any caller-supplied default `opts.pruneMode` when the
24
+ tool is called without an arg (`pruneMode ? { ...opts, pruneMode } : opts`).
25
+ - **Auto-hint when act-mode looks suspect.** When `page.snapshot()` or
26
+ `browse()` is called in act mode against a substantial page (raw > 5 KB)
27
+ and the pruned output collapses to under 500 chars AND under 5% of raw,
28
+ the result includes a one-line `hint: act mode dropped most of the page
29
+ — retry with pruneMode='read' …` directly between the stats line and the
30
+ tree. Thresholds are deliberately conservative: an e-commerce or
31
+ search-results page (many interactive elements kept) won't trigger it;
32
+ a paragraph-heavy article will.
33
+ - **Regression test:** `test/unit/prune.test.js` — "aliases mode='read' to
34
+ browse mode" pins the alias contract by asserting `prune(tree, {mode:
35
+ 'read'})` deep-equals `prune(tree, {mode: 'browse'})` and that paragraphs
36
+ survive (the act-mode-style stripping that previously masqueraded as
37
+ read-mode is gone).
38
+
3
39
  ## 0.9.0
4
40
 
5
41
  Phase B — every H1–H9 from `docs/02-features/fix-plan.md` shipped one
package/README.md CHANGED
@@ -94,6 +94,8 @@ Or manually add to your config (`claude_desktop_config.json`, `.cursor/mcp.json`
94
94
 
95
95
  18 tools: `browse`, `goto`, `snapshot`, `click`, `type`, `press`, `scroll`, `hover`, `select`, `back`, `forward`, `reload`, `drag`, `upload`, `pdf`, `screenshot`, `wait_for`, `tabs`. Plus `assess` (privacy scan) if [wearehere](https://github.com/hamr0/wearehere) is installed. Plus opt-in `eval` (`BAREBROWSE_MCP_EVAL=1`) — runs JS in the authenticated session, off by default because it can read cookies/localStorage. Session runs in hybrid mode with automatic cookie injection. Per-tool timeouts (goto/reload/wait_for 60s, back/forward 30s, interactive ops 15s, pdf/screenshot/upload 45s) with auto-retry on transient failures (idempotent only — mutating tools fail loudly to avoid double-submits).
96
96
 
97
+ `browse` and `snapshot` accept `pruneMode: 'act'|'read'` (v0.9.1). `act` (default) keeps interactive elements — best for clicking/filling. `read` keeps paragraphs, headings, and long text — best for articles, docs, and content extraction. If act-mode collapses a content-heavy page near-totally, the snapshot includes a `hint: …` line suggesting `pruneMode='read'` so the agent doesn't bail to a separate HTTP fetch.
98
+
97
99
  Troubleshooting MCP setup: `npx barebrowse doctor` scans every known config location and flags scope conflicts. `npx barebrowse install --force` overwrites an existing entry pointing at a different endpoint.
98
100
 
99
101
  ### 3. Library -- for agentic automation
@@ -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.9.0 | Node.js >= 22 | 0 required deps | Apache-2.0
4
+ > v0.9.1 | Node.js >= 22 | 0 required deps | Apache-2.0
5
5
 
6
6
  ## What this is
7
7
 
@@ -255,6 +255,8 @@ Action tools return `'ok'` -- the agent calls `snapshot` explicitly to observe.
255
255
 
256
256
  `browse` and `snapshot` accept a `maxChars` param (default 30000). If the snapshot exceeds the limit, it's saved to `.barebrowse/page-<timestamp>.yml` and a short message with the file path is returned instead. `screenshot` always saves to `.barebrowse/screenshot-<timestamp>.{png,jpeg,webp}` and returns the file path (raw base64 in a JSON-RPC response would blow `maxChars`). `tabs` returns the JSON array, or with `switchTo: N` it switches and returns `'ok'`.
257
257
 
258
+ `browse` and `snapshot` also accept `pruneMode: 'act'|'read'`. `act` (the default) keeps interactive elements and short labels — best for clicking/filling. `read` keeps paragraphs, headings, and long text — best for articles, docs, and content extraction. Same surface on the bareagent adapter. If act mode collapses a content-heavy page (raw > 5 KB → pruned < 500 chars AND < 5% of raw), the result includes a `hint: act mode dropped most of the page — retry with pruneMode='read' …` line between the stats and the tree so the caller knows to re-snapshot in read mode instead of bailing to a separate HTTP fetch.
259
+
258
260
  Session runs in hybrid mode (headless with automatic headed fallback on bot detection). `goto` injects cookies from the user's browser before navigation for authenticated access.
259
261
 
260
262
  Session tools share a singleton page, lazy-created on first use. All session tools have auto-retry on transient failures (browser crash, WebSocket close, navigation timeout) on a per-tool deadline (v0.9.0 H5): `goto`/`reload`/`wait_for` 60s, `back`/`forward` 30s, interactive ops (`click`/`type`/`press`/`scroll`/`hover`/`select`/`drag`/`snapshot`/`eval`) 15s, `tabs` 5s, heavy I/O (`pdf`/`screenshot`/`upload`) 45s — replaces the prior blanket 30s. Session resets between attempts. Idempotent tools retry once; mutating tools (`click`/`type`/`upload`/etc.) `{ retry: false }` so partial first attempts don't replay on a fresh page. Scroll accepts `direction: "up"/"down"` in addition to numeric `deltaY`. Click falls back to JS `.click()` when elements have no layout. `browse` has a 60s timeout (no retry — stateless). Assess tries headless first; if bot-blocked, retries headed. Browser OOM/crash auto-recovers (session resets, server stays alive).
package/mcp-server.js CHANGED
@@ -150,6 +150,7 @@ export const TOOLS = [
150
150
  properties: {
151
151
  url: { type: 'string', description: 'URL to browse' },
152
152
  mode: { type: 'string', enum: ['headless', 'headed', 'hybrid'], description: 'Browser mode (default: headless)' },
153
+ pruneMode: { type: 'string', enum: ['act', 'read'], description: 'Pruning mode. "act" (default) keeps interactive elements and short labels — best for clicking/filling. "read" keeps paragraphs, headings, and long text — best for articles, docs, and content extraction. If the page is content-heavy and act-mode returns mostly empty, retry with "read".' },
153
154
  maxChars: { type: 'number', description: 'Max chars to return inline. Larger snapshots are saved to .barebrowse/ and a file path is returned instead. Default: 30000.' },
154
155
  },
155
156
  required: ['url'],
@@ -172,6 +173,7 @@ export const TOOLS = [
172
173
  inputSchema: {
173
174
  type: 'object',
174
175
  properties: {
176
+ pruneMode: { type: 'string', enum: ['act', 'read'], description: 'Pruning mode. "act" (default) keeps interactive elements and short labels — best for clicking/filling. "read" keeps paragraphs, headings, and long text — best for articles, docs, and content extraction. If a previous snapshot looked empty on a content-heavy page, retry with "read".' },
175
177
  maxChars: { type: 'number', description: 'Max chars to return inline. Larger snapshots are saved to .barebrowse/ and a file path is returned instead. Default: 30000.' },
176
178
  },
177
179
  },
@@ -374,7 +376,7 @@ async function handleToolCall(name, args) {
374
376
  case 'browse': {
375
377
  let timer;
376
378
  const text = await Promise.race([
377
- browse(args.url, { mode: args.mode }),
379
+ browse(args.url, { mode: args.mode, pruneMode: args.pruneMode }),
378
380
  new Promise((_, rej) => { timer = setTimeout(() => rej(new Error('browse timed out after 60s')), 60000); }),
379
381
  ]);
380
382
  clearTimeout(timer);
@@ -393,7 +395,7 @@ async function handleToolCall(name, args) {
393
395
  }, TIMEOUTS.goto);
394
396
  case 'snapshot': return withRetry(async () => {
395
397
  const page = await getPage();
396
- const text = await page.snapshot();
398
+ const text = await page.snapshot(args.pruneMode ? { mode: args.pruneMode } : undefined);
397
399
  const limit = args.maxChars ?? MAX_CHARS_DEFAULT;
398
400
  if (text.length > limit) {
399
401
  const file = saveSnapshot(text);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "barebrowse",
3
- "version": "0.9.0",
3
+ "version": "0.9.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/bareagent.js CHANGED
@@ -50,10 +50,11 @@ export function createBrowseTools(opts = {}) {
50
50
  type: 'object',
51
51
  properties: {
52
52
  url: { type: 'string', description: 'URL to browse' },
53
+ pruneMode: { type: 'string', enum: ['act', 'read'], description: '"act" (default) for interactive elements only; "read" for paragraphs and long text (articles/docs).' },
53
54
  },
54
55
  required: ['url'],
55
56
  },
56
- execute: async ({ url }) => await browse(url, opts),
57
+ execute: async ({ url, pruneMode }) => await browse(url, pruneMode ? { ...opts, pruneMode } : opts),
57
58
  },
58
59
  {
59
60
  name: 'goto',
@@ -70,10 +71,15 @@ export function createBrowseTools(opts = {}) {
70
71
  {
71
72
  name: 'snapshot',
72
73
  description: 'Get the current ARIA snapshot. Returns a YAML-like tree with [ref=N] markers on interactive elements.',
73
- parameters: { type: 'object', properties: {} },
74
- execute: async () => {
74
+ parameters: {
75
+ type: 'object',
76
+ properties: {
77
+ pruneMode: { type: 'string', enum: ['act', 'read'], description: '"act" (default) for interactive elements only; "read" for paragraphs and long text (articles/docs).' },
78
+ },
79
+ },
80
+ execute: async ({ pruneMode } = {}) => {
75
81
  const page = await getPage();
76
- return await page.snapshot();
82
+ return await page.snapshot(pruneMode ? { mode: pruneMode } : undefined);
77
83
  },
78
84
  },
79
85
  {
package/src/index.js CHANGED
@@ -110,7 +110,11 @@ export async function browse(url, opts = {}) {
110
110
  snapshot = raw;
111
111
  }
112
112
  const stats = `url: ${url}\n${raw.length.toLocaleString()} chars → ${snapshot.length.toLocaleString()} chars (${Math.round((1 - snapshot.length / raw.length) * 100)}% pruned)`;
113
- snapshot = stats + '\n' + snapshot;
113
+ const actMode = !opts.pruneMode || opts.pruneMode === 'act';
114
+ const hint = (actMode && raw.length > 5000 && snapshot.length < 500 && snapshot.length < raw.length * 0.05)
115
+ ? `hint: act mode dropped most of the page — retry with pruneMode='read' for paragraphs and long text\n`
116
+ : '';
117
+ snapshot = stats + '\n' + hint + snapshot;
114
118
 
115
119
  // Step 7: Clean up
116
120
  await cdp.send('Target.closeTarget', { targetId: page.targetId });
@@ -382,10 +386,14 @@ export async function connect(opts = {}) {
382
386
  const pageUrl = entries[currentIndex]?.url || '';
383
387
  const warn = botBlocked ? '[BOT CHALLENGE DETECTED — page content may be incomplete or blocked]\n' : '';
384
388
  if (pruneOpts === false) return `url: ${pageUrl}\n` + warn + raw;
385
- const pruned = pruneTree(result.tree, { mode: pruneOpts?.mode || 'act' });
389
+ const mode = pruneOpts?.mode || 'act';
390
+ const pruned = pruneTree(result.tree, { mode });
386
391
  const out = formatTree(pruned);
387
392
  const stats = `url: ${pageUrl}\n${raw.length.toLocaleString()} chars → ${out.length.toLocaleString()} chars (${Math.round((1 - out.length / raw.length) * 100)}% pruned)`;
388
- return stats + '\n' + warn + out;
393
+ const hint = (mode === 'act' && raw.length > 5000 && out.length < 500 && out.length < raw.length * 0.05)
394
+ ? `hint: act mode dropped most of the page — retry with pruneMode='read' for paragraphs and long text\n`
395
+ : '';
396
+ return stats + '\n' + hint + warn + out;
389
397
  },
390
398
 
391
399
  async click(ref) {
package/src/prune.js CHANGED
@@ -65,7 +65,8 @@ const SKIP_ROLES = new Set([
65
65
  * @returns {object|null} Pruned tree
66
66
  */
67
67
  export function prune(tree, options = {}) {
68
- const { mode = 'act', context = '' } = options;
68
+ let { mode = 'act', context = '' } = options;
69
+ if (mode === 'read') mode = 'browse';
69
70
  const allowedRegions = MODE_REGIONS[mode] || MODE_REGIONS.act;
70
71
  const isBrowse = mode === 'browse';
71
72
  const keywords = context