barebrowse 0.11.0 → 0.13.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/CHANGELOG.md +78 -0
- package/README.md +9 -4
- package/barebrowse.context.md +34 -5
- package/cli.js +3 -0
- package/mcp-server.js +35 -7
- package/package.json +35 -5
- package/src/auth.js +7 -4
- package/src/bareagent.js +26 -5
- package/src/cdp.js +15 -4
- package/src/chromium.js +7 -2
- package/src/daemon.js +37 -4
- package/src/index.js +28 -1
- package/src/network-idle.js +4 -1
- package/src/prune.js +1 -1
- package/src/readable.js +116 -0
- package/src/session-client.js +1 -1
- package/src/wearehere.d.ts +6 -0
- package/types/aria.d.ts +17 -0
- package/types/auth.d.ts +35 -0
- package/types/bareagent.d.ts +25 -0
- package/types/blocklist.d.ts +21 -0
- package/types/cdp.d.ts +6 -0
- package/types/chromium.d.ts +58 -0
- package/types/consent.d.ts +9 -0
- package/types/daemon.d.ts +10 -0
- package/types/index.d.ts +138 -0
- package/types/interact.d.ts +79 -0
- package/types/network-idle.d.ts +19 -0
- package/types/prune.d.ts +13 -0
- package/types/readable.d.ts +18 -0
- package/types/session-client.d.ts +19 -0
- package/types/stealth.d.ts +14 -0
- package/types/url-guard.d.ts +26 -0
- package/.github/workflows/publish.yml +0 -26
- package/commands/barebrowse/SKILL.md +0 -137
- package/commands/barebrowse.md +0 -136
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,83 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
## [0.13.0] - 2026-06-12
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **`readable()` — clean article extraction.** New read mode that returns the
|
|
9
|
+
main article of a page as clean text (title + body prose, nav/ads/sidebars
|
|
10
|
+
stripped) via Mozilla Readability injected in-page over CDP. Companion to
|
|
11
|
+
`snapshot()`, not a replacement: `snapshot()` is the *actionable* ARIA tree
|
|
12
|
+
for clicking/typing; `readable()` is for *reading/summarising* article-like
|
|
13
|
+
pages (news, blogs, docs, wiki), where `snapshot()` is noisy and silently
|
|
14
|
+
lossy on long prose. Article detection is unreliable, so `readable()` never
|
|
15
|
+
hard-gates — it always returns the text plus an advisory `confidence`
|
|
16
|
+
(`high`/`low`) and a hint to fall back to `snapshot()` on non-article pages.
|
|
17
|
+
Exposed everywhere: `page.readable()`, MCP `readable` tool, bareagent
|
|
18
|
+
`readable` tool, and `barebrowse readable` CLI (→ `.barebrowse/article-*.txt`).
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- **Large pages no longer kill the CDP connection.** Node's built-in WebSocket
|
|
22
|
+
(undici) silently caps decompressed messages at ~3 MB and *permanently* tears
|
|
23
|
+
down the socket when a single `Accessibility.getFullAXTree` response exceeds
|
|
24
|
+
it — which broke `snapshot()` (and consent dismissal during `goto()`) on big
|
|
25
|
+
pages (e.g. long Wikipedia articles). `cdp.js` now uses the `ws` package with
|
|
26
|
+
a 256 MB `maxPayload`; the built-in exposes no way to raise the limit.
|
|
27
|
+
Regression test: `connect.test.js` snapshots a 12k-node page that tripped the
|
|
28
|
+
old cap.
|
|
29
|
+
|
|
30
|
+
### Security
|
|
31
|
+
- **MCP output files are now owner-only (`0600`).** `saveSnapshot()` and the
|
|
32
|
+
screenshot tool previously wrote snapshots / articles / screenshots with
|
|
33
|
+
default perms (`0644` in a `0755` dir under the standard umask) —
|
|
34
|
+
authenticated page content readable by other local users on a shared host.
|
|
35
|
+
They now write `0600` files in a `0700` dir, umask-independent, matching the
|
|
36
|
+
daemon's existing invariant. Regression-guarded by a test that fails on a
|
|
37
|
+
`0644` write.
|
|
38
|
+
- **Daemon hardening:** `GET /status` (the only pre-auth endpoint) no longer
|
|
39
|
+
returns the pid; `/command` now caps the request body at 16 MB (→ `413`).
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
- **Two runtime dependencies (previously zero):** `ws` (CDP transport, above)
|
|
43
|
+
and `@mozilla/readability` (`readable()`). Both are lightweight, widely
|
|
44
|
+
adopted, and actively maintained, per the project's dependency rule (external
|
|
45
|
+
only when the stdlib genuinely can't do the job).
|
|
46
|
+
|
|
47
|
+
## [0.12.0] - 2026-05-29
|
|
48
|
+
|
|
49
|
+
### Added
|
|
50
|
+
- **Shipped TypeScript types, generated from JSDoc.** The package now ships
|
|
51
|
+
`.d.ts` declarations so adopters get autocomplete and type errors out of the
|
|
52
|
+
box — no `@types/barebrowse`. The `.js` we author is still the `.js` that
|
|
53
|
+
ships; there is **no build step for runtime code**. Types are generated by
|
|
54
|
+
`tsc` (`checkJs` + `strictNullChecks`), emitted to a git-ignored `types/`, and
|
|
55
|
+
built into the tarball at publish via `prepublishOnly`. Because they are
|
|
56
|
+
generated-and-never-committed, the JSDoc, the `.d.ts`, and CI cannot drift.
|
|
57
|
+
`exports` now carries a `types` condition on every subpath.
|
|
58
|
+
- **`ci.yml` (push/PR gate):** `npm ci → typecheck → build:types → test`. A
|
|
59
|
+
JSDoc/code mismatch is now a type error that blocks merge. No lint step — `tsc`
|
|
60
|
+
covers the bug class that matters for a vanilla-ESM lib.
|
|
61
|
+
- Dev-only tooling: `typescript` + `@types/node` (devDependencies; never
|
|
62
|
+
shipped), `tsconfig.json`, and `typecheck` / `build:types` / `prepublishOnly`
|
|
63
|
+
scripts.
|
|
64
|
+
|
|
65
|
+
### Changed
|
|
66
|
+
- **`publish.yml` is now manual-only (`workflow_dispatch`) — npm OIDC trusted publishing with provenance, idempotent, and verifies the registry end-state.**
|
|
67
|
+
- **Packaging now uses a `files` allowlist** (`src/`, generated `types/`,
|
|
68
|
+
`cli.js`, `mcp-server.js`, and the doc set) instead of the old `.npmignore`
|
|
69
|
+
denylist, which was removed. Repo-only files (`test/`, `docs/`, `CLAUDE.md`)
|
|
70
|
+
are excluded from the tarball.
|
|
71
|
+
|
|
72
|
+
### Fixed
|
|
73
|
+
- **`auth.js`: cookie databases are now opened with `readOnly: true`.** The
|
|
74
|
+
previous `readonly` (lowercase) key is silently ignored by `node:sqlite`;
|
|
75
|
+
surfaced by the new `tsc` typecheck. Read-only was already enforced via the
|
|
76
|
+
`?immutable=1` connection URI, so observable behavior is unchanged — this
|
|
77
|
+
honors the intended option. Added minimal, behavior-preserving null/type
|
|
78
|
+
guards in a few spots (`server.address()`, SQLite row values) flagged by
|
|
79
|
+
`strictNullChecks`.
|
|
80
|
+
|
|
3
81
|
## 0.11.0
|
|
4
82
|
|
|
5
83
|
### Security hardening — audit findings fixed, safe-by-default
|
package/README.md
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
```
|
|
11
11
|
|
|
12
12
|
<p align="center">
|
|
13
|
+
<a href="https://github.com/hamr0/barebrowse/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/hamr0/barebrowse/ci.yml?label=CI" alt="CI"></a>
|
|
13
14
|
<img src="https://img.shields.io/github/package-json/v/hamr0/barebrowse?label=version&color=2a4f8c" alt="version (auto from package.json)">
|
|
14
15
|
<img src="https://img.shields.io/badge/license-Apache%202.0-2a4f8c" alt="license: Apache 2.0">
|
|
15
16
|
</p>
|
|
@@ -25,7 +26,7 @@ barebrowse gives your AI agent a real browser. Navigate, read, interact, move on
|
|
|
25
26
|
|
|
26
27
|
It uses the browser you already have -- your sessions, your cookies. Pages come back stripped to what matters -- 40-90% fewer tokens than raw output.
|
|
27
28
|
|
|
28
|
-
No Playwright.
|
|
29
|
+
No Playwright. No bundled browser. No 200MB download. Two tiny dependencies (`ws` + Mozilla Readability).
|
|
29
30
|
|
|
30
31
|
## Install
|
|
31
32
|
|
|
@@ -35,6 +36,8 @@ npm install barebrowse
|
|
|
35
36
|
|
|
36
37
|
Requires Node.js >= 22 and any installed Chromium-based browser.
|
|
37
38
|
|
|
39
|
+
Ships with TypeScript types (generated from JSDoc) — autocomplete and type-checking work out of the box, no `@types/barebrowse` needed. The library is vanilla JS with no build step.
|
|
40
|
+
|
|
38
41
|
## Three ways to use it
|
|
39
42
|
|
|
40
43
|
### 1. CLI session -- for coding agents and quick testing
|
|
@@ -42,6 +45,7 @@ Requires Node.js >= 22 and any installed Chromium-based browser.
|
|
|
42
45
|
```bash
|
|
43
46
|
barebrowse open https://example.com # Start session + navigate
|
|
44
47
|
barebrowse snapshot # ARIA snapshot → .barebrowse/page-*.yml
|
|
48
|
+
barebrowse readable # Clean article text → .barebrowse/article-*.txt
|
|
45
49
|
barebrowse click 8 # Click element
|
|
46
50
|
barebrowse close # End session
|
|
47
51
|
```
|
|
@@ -92,7 +96,7 @@ Or manually add to your config (`claude_desktop_config.json`, `.cursor/mcp.json`
|
|
|
92
96
|
}
|
|
93
97
|
```
|
|
94
98
|
|
|
95
|
-
|
|
99
|
+
19 tools: `browse`, `goto`, `snapshot`, `readable`, `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
100
|
|
|
97
101
|
`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
102
|
|
|
@@ -100,7 +104,7 @@ Troubleshooting MCP setup: `npx barebrowse doctor` scans every known config loca
|
|
|
100
104
|
|
|
101
105
|
### 3. Library -- for agentic automation
|
|
102
106
|
|
|
103
|
-
Import barebrowse in your agent code. One-shot reads, interactive sessions, full observe-think-act loops. Works with any LLM orchestration library. Ships with a ready-made adapter for [bareagent](https://www.npmjs.com/package/bare-agent) (
|
|
107
|
+
Import barebrowse in your agent code. One-shot reads, interactive sessions, full observe-think-act loops. Works with any LLM orchestration library. Ships with a ready-made adapter for [bareagent](https://www.npmjs.com/package/bare-agent) (18 tools, auto-snapshot after every action).
|
|
104
108
|
|
|
105
109
|
For code examples, API reference, and wiring instructions, see **[barebrowse.context.md](barebrowse.context.md)** -- the full integration guide.
|
|
106
110
|
|
|
@@ -167,6 +171,7 @@ Everything the agent can do through barebrowse:
|
|
|
167
171
|
| **Navigate** | Load a URL, wait for page load, auto-dismiss consent |
|
|
168
172
|
| **Back / Forward** | Browser history navigation |
|
|
169
173
|
| **Snapshot** | Pruned ARIA tree with `[ref=N]` markers. Two modes: `act` (buttons, links, inputs) and `read` (full text). 40-90% token reduction. |
|
|
174
|
+
| **Readable** | Clean article text (title + body, chrome stripped — Reader-View engine). For *reading* article-like pages, not interacting. Advisory `confidence`; falls back to snapshot on non-articles. |
|
|
170
175
|
| **Click** | Scroll into view + mouse click at element center, JS fallback for hidden elements |
|
|
171
176
|
| **Type** | Focus + insert text, with option to clear existing content first |
|
|
172
177
|
| **Press** | Special keys: Enter, Tab, Escape, Backspace, Delete, arrows, Space |
|
|
@@ -213,7 +218,7 @@ URL -> find/launch browser (chromium.js)
|
|
|
213
218
|
-> agent-ready snapshot with [ref=N] markers
|
|
214
219
|
```
|
|
215
220
|
|
|
216
|
-
|
|
221
|
+
14 modules, ~3,000 lines, two small dependencies (`ws`, `@mozilla/readability`).
|
|
217
222
|
|
|
218
223
|
## Requirements
|
|
219
224
|
|
package/barebrowse.context.md
CHANGED
|
@@ -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.
|
|
4
|
+
> v0.12.0 | Node.js >= 22 | 0 required deps | Apache-2.0
|
|
5
5
|
|
|
6
6
|
## What this is
|
|
7
7
|
|
|
@@ -13,6 +13,11 @@ No Playwright. No bundled browser. No build step. Vanilla JS, ES modules.
|
|
|
13
13
|
npm install barebrowse
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
+
**TypeScript:** ships with `.d.ts` types generated from the source JSDoc, so
|
|
17
|
+
autocomplete and type-checking work out of the box — no `@types/barebrowse`
|
|
18
|
+
needed. The library itself is vanilla JS with no build step; the types are a
|
|
19
|
+
publish-time artifact.
|
|
20
|
+
|
|
16
21
|
Three integration paths:
|
|
17
22
|
1. **Library:** `import { browse, connect } from 'barebrowse'` -- one-shot or interactive session
|
|
18
23
|
2. **MCP server:** `barebrowse mcp` -- JSON-RPC over stdio for Claude Desktop, Cursor, etc.
|
|
@@ -62,6 +67,7 @@ const snapshot = await browse('https://example.com', {
|
|
|
62
67
|
| `goForward()` | -- | void | Navigate forward in browser history |
|
|
63
68
|
| `reload(opts?)` | { ignoreCache?: boolean, timeout?: number } | void | Reload the current page. Clears refMap (refs from pre-reload reject). |
|
|
64
69
|
| `snapshot(pruneOpts?)` | false or { mode: 'act'\|'read' } | string | ARIA tree with `[ref=N]` markers. Pass `false` for raw. |
|
|
70
|
+
| `readable()` | -- | object | Clean article text (Reader-View engine). `{ ok, title, byline, text, length, confidence: 'high'\|'low', readerable, hint? }` or `{ ok: false, hint }`. For *reading*, not interacting — see note below. |
|
|
65
71
|
| `click(ref)` | ref: string | void | Scroll into view + mouse press+release at center |
|
|
66
72
|
| `type(ref, text, opts?)` | ref: string, text: string, opts: { clear?, keyEvents? } | void | Focus + insert text. `clear: true` replaces existing. |
|
|
67
73
|
| `press(key)` | key: string | void | Special key: Enter, Tab, Escape, Backspace, Delete, arrows, Home, End, PageUp, PageDown, Space |
|
|
@@ -117,6 +123,28 @@ Key rules:
|
|
|
117
123
|
- `click(ref)` / `type(ref, text)` / `hover(ref)` / `select(ref, value)` use these ref strings
|
|
118
124
|
- Pruning removes noise (~47-95% token reduction) while keeping all interactive elements
|
|
119
125
|
|
|
126
|
+
## readable() vs snapshot() — which to use
|
|
127
|
+
|
|
128
|
+
Two different jobs:
|
|
129
|
+
|
|
130
|
+
- **`snapshot()`** → the *actionable* ARIA tree (`[ref=N]` markers). Use it to **interact** (click/type/fill) or on **any** page type (home pages, search results, app UIs).
|
|
131
|
+
- **`readable()`** → the *article* as clean reading text (title + body prose; nav/ads/sidebars stripped). Use it **only** to **read or summarise** article-like pages (news, blogs, docs, wiki), where `snapshot()` is both noisy and *silently lossy on long prose* (`read` pruning can drop body text).
|
|
132
|
+
|
|
133
|
+
`readable()` is **not** a token-savings feature — vs a read-mode snapshot it can be smaller *or* larger depending on the page; its value is **complete, clean prose**. It returns no refs, so you cannot interact with its output.
|
|
134
|
+
|
|
135
|
+
**Article detection is unreliable**, so `readable()` never hard-gates. It always returns whatever it extracted plus an advisory `confidence`:
|
|
136
|
+
- `confidence: 'high'` → safe to treat as an article.
|
|
137
|
+
- `confidence: 'low'` (or `ok: false`) → probably not an article; the `hint` tells the agent to fall back to `snapshot()`.
|
|
138
|
+
|
|
139
|
+
```js
|
|
140
|
+
const r = await page.readable();
|
|
141
|
+
if (r.ok && r.confidence === 'high') {
|
|
142
|
+
use(r.text); // clean article prose
|
|
143
|
+
} else {
|
|
144
|
+
const snap = await page.snapshot({ mode: 'read' }); // fall back
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
120
148
|
## Interaction loop: observe, think, act
|
|
121
149
|
|
|
122
150
|
```javascript
|
|
@@ -202,10 +230,10 @@ try {
|
|
|
202
230
|
```
|
|
203
231
|
|
|
204
232
|
`createBrowseTools(opts)` returns:
|
|
205
|
-
- `tools` -- array of bareagent-compatible tool objects: `browse`, `goto`, `snapshot`, `click`, `type`, `press`, `scroll`, `select`, `hover`, `back`, `forward`, `reload` (v0.9.0), `drag`, `upload`, `tabs`, `switchTab`, `pdf`, `screenshot`, `wait_for` (v0.9.0), `downloads` (v0.9.0), plus `assess` if wearehere installed
|
|
233
|
+
- `tools` -- array of bareagent-compatible tool objects: `browse`, `goto`, `snapshot`, `readable`, `click`, `type`, `press`, `scroll`, `select`, `hover`, `back`, `forward`, `reload` (v0.9.0), `drag`, `upload`, `tabs`, `switchTab`, `pdf`, `screenshot`, `wait_for` (v0.9.0), `downloads` (v0.9.0), plus `assess` if wearehere installed
|
|
206
234
|
- `close()` -- cleanup function, call when done
|
|
207
235
|
|
|
208
|
-
Action tools (click, type, press, scroll, hover, goto, back, forward, reload, drag, upload, select, switchTab, wait_for) auto-return a fresh snapshot so the LLM always sees the result. 300ms settle delay after actions for DOM updates.
|
|
236
|
+
Action tools (click, type, press, scroll, hover, goto, back, forward, reload, drag, upload, select, switchTab, wait_for) auto-return a fresh snapshot so the LLM always sees the result. 300ms settle delay after actions for DOM updates. `readable` is a read tool (like `snapshot`): it returns the article text directly, not a follow-up snapshot.
|
|
209
237
|
|
|
210
238
|
`onDialog` is intentionally not exposed as a tool — it's a callback shape that doesn't fit a request/response tool loop. If your bareagent flow needs to override a confirm/prompt, drop to `import { connect }` directly and pass the page through.
|
|
211
239
|
|
|
@@ -216,6 +244,7 @@ For coding agents (Claude Code, Copilot, Cursor) and quick interactive testing.
|
|
|
216
244
|
```bash
|
|
217
245
|
barebrowse open https://example.com # Start daemon + navigate
|
|
218
246
|
barebrowse snapshot # → .barebrowse/page-<timestamp>.yml
|
|
247
|
+
barebrowse readable # → .barebrowse/article-<timestamp>.txt (clean article text)
|
|
219
248
|
barebrowse click 8 # Click element ref=8
|
|
220
249
|
barebrowse type 12 hello world # Type into element ref=12
|
|
221
250
|
barebrowse back # Go back in history
|
|
@@ -257,11 +286,11 @@ barebrowse ships an MCP server for direct use with Claude Desktop, Cursor, or an
|
|
|
257
286
|
}
|
|
258
287
|
```
|
|
259
288
|
|
|
260
|
-
|
|
289
|
+
19 core tools: `browse` (one-shot), `goto`, `snapshot`, `readable`, `click`, `type`, `press`, `scroll`, `hover`, `select`, `back`, `forward`, `reload`, `drag`, `upload`, `pdf`, `screenshot`, `wait_for`, `tabs`. Plus `assess` (privacy scan) if `wearehere` is installed (`npm install wearehere`). Plus the **opt-in `eval` tool** gated by `BAREBROWSE_MCP_EVAL=1` (default OFF) — `Runtime.evaluate` in the user's authenticated session can read cookies/localStorage and hit any same-origin endpoint, so opt-in only.
|
|
261
290
|
|
|
262
291
|
Action tools return `'ok'` -- the agent calls `snapshot` explicitly to observe. This avoids double-token output since MCP tool calls are cheap to chain.
|
|
263
292
|
|
|
264
|
-
`browse` and `
|
|
293
|
+
`browse`, `snapshot`, and `readable` accept a `maxChars` param (default 30000). If the output exceeds the limit it's saved to `.barebrowse/` and a short message with the file path is returned instead (`page-<timestamp>.yml` for snapshots, `article-<timestamp>.txt` for `readable`). `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'`. All files MCP writes are owner-only (`0600` in a `0700` dir) — they can hold authenticated page content, so they're not world-readable on a shared host.
|
|
265
294
|
|
|
266
295
|
`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.
|
|
267
296
|
|
package/cli.js
CHANGED
|
@@ -38,6 +38,8 @@ if (args.includes('--daemon-internal')) {
|
|
|
38
38
|
await cmdProxy('goto', { url: args[1], timeout: parseFlag('--timeout') });
|
|
39
39
|
} else if (cmd === 'snapshot') {
|
|
40
40
|
await cmdProxy('snapshot', { mode: parseFlag('--mode') });
|
|
41
|
+
} else if (cmd === 'readable') {
|
|
42
|
+
await cmdProxy('readable');
|
|
41
43
|
} else if (cmd === 'screenshot') {
|
|
42
44
|
await cmdProxy('screenshot', { format: parseFlag('--format') });
|
|
43
45
|
} else if (cmd === 'click' && args[1]) {
|
|
@@ -504,6 +506,7 @@ Navigation:
|
|
|
504
506
|
barebrowse forward Go forward in history
|
|
505
507
|
barebrowse reload [--no-cache] Reload current page
|
|
506
508
|
barebrowse snapshot [--mode=M] ARIA snapshot -> .barebrowse/page-*.yml
|
|
509
|
+
barebrowse readable Clean article text -> .barebrowse/article-*.txt
|
|
507
510
|
barebrowse screenshot [--format] Screenshot -> .barebrowse/screenshot-*.png
|
|
508
511
|
barebrowse pdf [--landscape] PDF export -> .barebrowse/page-*.pdf
|
|
509
512
|
|
package/mcp-server.js
CHANGED
|
@@ -3,13 +3,15 @@
|
|
|
3
3
|
* mcp-server.js — MCP server for barebrowse.
|
|
4
4
|
*
|
|
5
5
|
* Raw JSON-RPC 2.0 over stdio. No SDK dependency.
|
|
6
|
-
*
|
|
6
|
+
* Tools: browse, goto, snapshot, readable, click, type, press, scroll, back,
|
|
7
|
+
* forward, drag, upload, pdf, reload, screenshot, wait_for, tabs, select, hover.
|
|
7
8
|
*
|
|
8
9
|
* Session tools share a singleton page, lazy-created on first use.
|
|
9
10
|
* Action tools return 'ok' — agent calls snapshot explicitly to observe.
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
13
|
import { browse, connect } from './src/index.js';
|
|
14
|
+
import { formatReadable } from './src/readable.js';
|
|
13
15
|
import { mkdirSync, writeFileSync, readFileSync } from 'node:fs';
|
|
14
16
|
import { join, dirname } from 'node:path';
|
|
15
17
|
import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
@@ -102,11 +104,14 @@ async function withRetry(fn, timeoutMs, { retry = true } = {}) {
|
|
|
102
104
|
const MAX_CHARS_DEFAULT = 30000;
|
|
103
105
|
const OUTPUT_DIR = join(process.cwd(), '.barebrowse');
|
|
104
106
|
|
|
105
|
-
function saveSnapshot(text) {
|
|
106
|
-
|
|
107
|
+
export function saveSnapshot(text, { prefix = 'page', ext = 'yml' } = {}) {
|
|
108
|
+
// Owner-only: snapshots/articles can hold authenticated page content
|
|
109
|
+
// (logged-in text, reflected session data). Matches the daemon's 0600/0700
|
|
110
|
+
// invariant — and is umask-independent because the modes are explicit.
|
|
111
|
+
mkdirSync(OUTPUT_DIR, { recursive: true, mode: 0o700 });
|
|
107
112
|
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
108
|
-
const file = join(OUTPUT_DIR,
|
|
109
|
-
writeFileSync(file, text);
|
|
113
|
+
const file = join(OUTPUT_DIR, `${prefix}-${ts}.${ext}`);
|
|
114
|
+
writeFileSync(file, text, { mode: 0o600 });
|
|
110
115
|
return file;
|
|
111
116
|
}
|
|
112
117
|
|
|
@@ -178,6 +183,16 @@ export const TOOLS = [
|
|
|
178
183
|
},
|
|
179
184
|
},
|
|
180
185
|
},
|
|
186
|
+
{
|
|
187
|
+
name: 'readable',
|
|
188
|
+
description: 'Extract the main article of the current page as clean reading text (title + body prose, nav/ads/sidebars stripped — the Firefox Reader View engine). Use ONLY when your goal is to READ or SUMMARISE article-like content (news, blog posts, docs, wiki). For clicking/typing/forms, or for non-article pages (home pages, search results, app UIs), use snapshot instead. On a non-article page this returns a low-confidence result with a hint to use snapshot.',
|
|
189
|
+
inputSchema: {
|
|
190
|
+
type: 'object',
|
|
191
|
+
properties: {
|
|
192
|
+
maxChars: { type: 'number', description: 'Max chars to return inline. Longer articles are saved to .barebrowse/ and a file path is returned instead. Default: 30000.' },
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
},
|
|
181
196
|
{
|
|
182
197
|
name: 'click',
|
|
183
198
|
description: 'Click an element by its ref from the snapshot. Returns ok — call snapshot to observe.',
|
|
@@ -403,6 +418,18 @@ async function handleToolCall(name, args) {
|
|
|
403
418
|
}
|
|
404
419
|
return text;
|
|
405
420
|
}, TIMEOUTS.snapshot);
|
|
421
|
+
case 'readable': return withRetry(async () => {
|
|
422
|
+
const page = await getPage();
|
|
423
|
+
const r = await page.readable();
|
|
424
|
+
if (!r.ok) return r.hint;
|
|
425
|
+
const body = formatReadable(r);
|
|
426
|
+
const limit = args.maxChars ?? MAX_CHARS_DEFAULT;
|
|
427
|
+
if (body.length > limit) {
|
|
428
|
+
const file = saveSnapshot(body, { prefix: 'article', ext: 'txt' });
|
|
429
|
+
return `Article "${r.title}" (${r.text.length} chars, confidence: ${r.confidence}) saved to ${file}`;
|
|
430
|
+
}
|
|
431
|
+
return body;
|
|
432
|
+
}, TIMEOUTS.snapshot);
|
|
406
433
|
case 'click': return withRetry(async () => {
|
|
407
434
|
const page = await getPage();
|
|
408
435
|
await page.click(args.ref);
|
|
@@ -463,10 +490,11 @@ async function handleToolCall(name, args) {
|
|
|
463
490
|
const page = await getPage();
|
|
464
491
|
const format = args.format || 'png';
|
|
465
492
|
const b64 = await page.screenshot({ format, quality: args.quality });
|
|
466
|
-
mkdirSync(OUTPUT_DIR, { recursive: true });
|
|
493
|
+
mkdirSync(OUTPUT_DIR, { recursive: true, mode: 0o700 });
|
|
467
494
|
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
468
495
|
const file = join(OUTPUT_DIR, `screenshot-${ts}.${format}`);
|
|
469
|
-
|
|
496
|
+
// Owner-only: a screenshot of an authenticated page is sensitive too.
|
|
497
|
+
writeFileSync(file, Buffer.from(b64, 'base64'), { mode: 0o600 });
|
|
470
498
|
return file;
|
|
471
499
|
}, TIMEOUTS.screenshot);
|
|
472
500
|
case 'wait_for': return withRetry(async () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "barebrowse",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Authenticated web browsing for autonomous agents via CDP. URL in, pruned ARIA snapshot out.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -10,18 +10,40 @@
|
|
|
10
10
|
"bugs": "https://github.com/hamr0/barebrowse/issues",
|
|
11
11
|
"type": "module",
|
|
12
12
|
"main": "src/index.js",
|
|
13
|
+
"types": "./types/index.d.ts",
|
|
13
14
|
"exports": {
|
|
14
|
-
".":
|
|
15
|
-
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./types/index.d.ts",
|
|
17
|
+
"default": "./src/index.js"
|
|
18
|
+
},
|
|
19
|
+
"./bareagent": {
|
|
20
|
+
"types": "./types/bareagent.d.ts",
|
|
21
|
+
"default": "./src/bareagent.js"
|
|
22
|
+
}
|
|
16
23
|
},
|
|
17
24
|
"bin": {
|
|
18
25
|
"barebrowse": "./cli.js"
|
|
19
26
|
},
|
|
27
|
+
"files": [
|
|
28
|
+
"src/",
|
|
29
|
+
"types/",
|
|
30
|
+
"cli.js",
|
|
31
|
+
"mcp-server.js",
|
|
32
|
+
"barebrowse.context.md",
|
|
33
|
+
"README.md",
|
|
34
|
+
"CHANGELOG.md",
|
|
35
|
+
"NOTICE"
|
|
36
|
+
],
|
|
20
37
|
"engines": {
|
|
21
38
|
"node": ">=22"
|
|
22
39
|
},
|
|
23
40
|
"scripts": {
|
|
24
|
-
"test": "node --test test/unit/*.test.js test/integration/*.test.js"
|
|
41
|
+
"test": "node --test test/unit/*.test.js test/integration/*.test.js",
|
|
42
|
+
"test:unit": "node --test test/unit/*.test.js",
|
|
43
|
+
"test:integration": "node --test test/integration/*.test.js",
|
|
44
|
+
"typecheck": "tsc --noEmit",
|
|
45
|
+
"build:types": "tsc",
|
|
46
|
+
"prepublishOnly": "npm run build:types"
|
|
25
47
|
},
|
|
26
48
|
"keywords": [
|
|
27
49
|
"browser",
|
|
@@ -37,5 +59,13 @@
|
|
|
37
59
|
"optionalDependencies": {
|
|
38
60
|
"wearehere": "1.0.0"
|
|
39
61
|
},
|
|
40
|
-
"license": "Apache-2.0"
|
|
62
|
+
"license": "Apache-2.0",
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"@types/node": "^25.9.1",
|
|
65
|
+
"typescript": "^6.0.3"
|
|
66
|
+
},
|
|
67
|
+
"dependencies": {
|
|
68
|
+
"@mozilla/readability": "^0.6.0",
|
|
69
|
+
"ws": "^8.21.0"
|
|
70
|
+
}
|
|
41
71
|
}
|
package/src/auth.js
CHANGED
|
@@ -126,7 +126,7 @@ function extractChromiumCookies(dbPath, domain) {
|
|
|
126
126
|
const aesKey = deriveKey(password);
|
|
127
127
|
|
|
128
128
|
// immutable=1 bypasses WAL lock on live databases
|
|
129
|
-
const db = new DatabaseSync(`file://${dbPath}?immutable=1`, {
|
|
129
|
+
const db = new DatabaseSync(`file://${dbPath}?immutable=1`, { readOnly: true });
|
|
130
130
|
|
|
131
131
|
let sql = `SELECT host_key, name, value, encrypted_value, path,
|
|
132
132
|
CAST(expires_utc AS TEXT) AS expires_utc, is_secure, is_httponly, samesite
|
|
@@ -144,7 +144,8 @@ function extractChromiumCookies(dbPath, domain) {
|
|
|
144
144
|
const SAMESITE = { 0: 'None', 1: 'Lax', 2: 'Strict' };
|
|
145
145
|
|
|
146
146
|
return rows.map((row) => {
|
|
147
|
-
const
|
|
147
|
+
const rawEnc = row.encrypted_value;
|
|
148
|
+
const enc = rawEnc instanceof Uint8Array ? Buffer.from(rawEnc) : Buffer.alloc(0);
|
|
148
149
|
let value;
|
|
149
150
|
try {
|
|
150
151
|
value = enc.length > 0 ? decryptCookie(enc, aesKey) : row.value;
|
|
@@ -154,7 +155,9 @@ function extractChromiumCookies(dbPath, domain) {
|
|
|
154
155
|
|
|
155
156
|
// Chrome timestamp: microseconds since 1601-01-01
|
|
156
157
|
const CHROME_EPOCH = 11644473600000000n;
|
|
157
|
-
const expiresUtc = row.expires_utc
|
|
158
|
+
const expiresUtc = typeof row.expires_utc === 'string' || typeof row.expires_utc === 'number'
|
|
159
|
+
? BigInt(row.expires_utc)
|
|
160
|
+
: 0n;
|
|
158
161
|
const expires = expiresUtc > 0n
|
|
159
162
|
? Number((expiresUtc - CHROME_EPOCH) / 1000000n)
|
|
160
163
|
: -1;
|
|
@@ -179,7 +182,7 @@ function extractChromiumCookies(dbPath, domain) {
|
|
|
179
182
|
* @returns {Array<object>} Cookies in CDP Network.setCookie format
|
|
180
183
|
*/
|
|
181
184
|
function extractFirefoxCookies(dbPath, domain) {
|
|
182
|
-
const db = new DatabaseSync(`file://${dbPath}?immutable=1`, {
|
|
185
|
+
const db = new DatabaseSync(`file://${dbPath}?immutable=1`, { readOnly: true });
|
|
183
186
|
|
|
184
187
|
let sql = `SELECT host, name, value, path, expiry, isSecure, isHttpOnly, sameSite
|
|
185
188
|
FROM moz_cookies`;
|
package/src/bareagent.js
CHANGED
|
@@ -11,7 +11,10 @@
|
|
|
11
11
|
* 300ms settle delay after actions for DOM updates.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
/// <reference path="./wearehere.d.ts" />
|
|
15
|
+
|
|
14
16
|
import { browse, connect } from './index.js';
|
|
17
|
+
import { formatReadable } from './readable.js';
|
|
15
18
|
|
|
16
19
|
// Optional: privacy assessment via wearehere
|
|
17
20
|
let assessFn = null;
|
|
@@ -22,6 +25,14 @@ try {
|
|
|
22
25
|
const SETTLE_MS = 300;
|
|
23
26
|
const settle = () => new Promise((r) => setTimeout(r, SETTLE_MS));
|
|
24
27
|
|
|
28
|
+
/**
|
|
29
|
+
* @typedef {object} BrowseTool
|
|
30
|
+
* @property {string} name
|
|
31
|
+
* @property {string} description
|
|
32
|
+
* @property {object} parameters - JSON-schema-shaped parameter spec
|
|
33
|
+
* @property {(args?: any) => Promise<any>} execute
|
|
34
|
+
*/
|
|
35
|
+
|
|
25
36
|
/**
|
|
26
37
|
* Create bareagent-compatible browse tools.
|
|
27
38
|
* @param {object} [opts] - Options passed to connect() for session tools
|
|
@@ -42,6 +53,7 @@ export function createBrowseTools(opts = {}) {
|
|
|
42
53
|
return await page.snapshot();
|
|
43
54
|
}
|
|
44
55
|
|
|
56
|
+
/** @type {BrowseTool[]} */
|
|
45
57
|
const tools = [
|
|
46
58
|
{
|
|
47
59
|
name: 'browse',
|
|
@@ -77,11 +89,20 @@ export function createBrowseTools(opts = {}) {
|
|
|
77
89
|
pruneMode: { type: 'string', enum: ['act', 'read'], description: '"act" (default) for interactive elements only; "read" for paragraphs and long text (articles/docs).' },
|
|
78
90
|
},
|
|
79
91
|
},
|
|
80
|
-
execute: async ({ pruneMode } = {}) => {
|
|
92
|
+
execute: async (/** @type {{ pruneMode?: string }} */ { pruneMode } = {}) => {
|
|
81
93
|
const page = await getPage();
|
|
82
94
|
return await page.snapshot(pruneMode ? { mode: pruneMode } : undefined);
|
|
83
95
|
},
|
|
84
96
|
},
|
|
97
|
+
{
|
|
98
|
+
name: 'readable',
|
|
99
|
+
description: 'Extract the main article as clean reading text (title + body prose, chrome stripped — Firefox Reader View engine). Use ONLY to READ/SUMMARISE article-like pages (news, blogs, docs, wiki). For interacting, or for non-article pages, use snapshot. Returns a low-confidence hint to use snapshot when the page is not an article.',
|
|
100
|
+
parameters: { type: 'object', properties: {} },
|
|
101
|
+
execute: async () => {
|
|
102
|
+
const page = await getPage();
|
|
103
|
+
return formatReadable(await page.readable());
|
|
104
|
+
},
|
|
105
|
+
},
|
|
85
106
|
{
|
|
86
107
|
name: 'click',
|
|
87
108
|
description: 'Click an element by its ref from the snapshot. Returns the updated snapshot.',
|
|
@@ -231,7 +252,7 @@ export function createBrowseTools(opts = {}) {
|
|
|
231
252
|
landscape: { type: 'boolean', description: 'Landscape orientation (default: false)' },
|
|
232
253
|
},
|
|
233
254
|
},
|
|
234
|
-
execute: async ({ landscape } = {}) => {
|
|
255
|
+
execute: async (/** @type {{ landscape?: boolean }} */ { landscape } = {}) => {
|
|
235
256
|
const page = await getPage();
|
|
236
257
|
return await page.pdf({ landscape });
|
|
237
258
|
},
|
|
@@ -245,7 +266,7 @@ export function createBrowseTools(opts = {}) {
|
|
|
245
266
|
format: { type: 'string', enum: ['png', 'jpeg', 'webp'], description: 'Image format (default: png)' },
|
|
246
267
|
},
|
|
247
268
|
},
|
|
248
|
-
execute: async ({ format } = {}) => {
|
|
269
|
+
execute: async (/** @type {{ format?: string }} */ { format } = {}) => {
|
|
249
270
|
const page = await getPage();
|
|
250
271
|
return await page.screenshot({ format });
|
|
251
272
|
},
|
|
@@ -259,7 +280,7 @@ export function createBrowseTools(opts = {}) {
|
|
|
259
280
|
ignoreCache: { type: 'boolean', description: 'Bypass HTTP cache (hard reload). Default: false.' },
|
|
260
281
|
},
|
|
261
282
|
},
|
|
262
|
-
execute: async ({ ignoreCache } = {}) => actionAndSnapshot((page) => page.reload({ ignoreCache })),
|
|
283
|
+
execute: async (/** @type {{ ignoreCache?: boolean }} */ { ignoreCache } = {}) => actionAndSnapshot((page) => page.reload({ ignoreCache })),
|
|
263
284
|
},
|
|
264
285
|
{
|
|
265
286
|
name: 'wait_for',
|
|
@@ -272,7 +293,7 @@ export function createBrowseTools(opts = {}) {
|
|
|
272
293
|
timeout: { type: 'number', description: 'Timeout in ms (default: 30000)' },
|
|
273
294
|
},
|
|
274
295
|
},
|
|
275
|
-
execute: async ({ text, selector, timeout } = {}) => actionAndSnapshot((page) => page.waitFor({ text, selector, timeout })),
|
|
296
|
+
execute: async (/** @type {{ text?: string, selector?: string, timeout?: number }} */ { text, selector, timeout } = {}) => actionAndSnapshot((page) => page.waitFor({ text, selector, timeout })),
|
|
276
297
|
},
|
|
277
298
|
{
|
|
278
299
|
name: 'downloads',
|
package/src/cdp.js
CHANGED
|
@@ -2,25 +2,35 @@
|
|
|
2
2
|
* cdp.js — Minimal Chrome DevTools Protocol client over WebSocket.
|
|
3
3
|
*
|
|
4
4
|
* Sends JSON-RPC commands, receives responses and events.
|
|
5
|
-
* Uses Node
|
|
5
|
+
* Uses the `ws` package (not Node's built-in WebSocket): the built-in
|
|
6
|
+
* silently caps decompressed messages at ~3 MB and permanently kills the
|
|
7
|
+
* socket when a single CDP response (e.g. Accessibility.getFullAXTree on a
|
|
8
|
+
* large page) exceeds it — with no way to raise the limit. `ws` exposes
|
|
9
|
+
* maxPayload, so we lift the ceiling and disable compression.
|
|
6
10
|
*
|
|
7
11
|
* Supports flattened sessions: when a sessionId is provided,
|
|
8
12
|
* it's sent at the top level of the message (not inside params).
|
|
9
13
|
* Events from sessions are also dispatched by sessionId.
|
|
10
14
|
*/
|
|
11
15
|
|
|
16
|
+
import WebSocket from 'ws';
|
|
17
|
+
|
|
18
|
+
/** Lift the message ceiling well past any realistic AX/DOM payload. */
|
|
19
|
+
const MAX_PAYLOAD = 256 * 1024 * 1024; // 256 MB
|
|
20
|
+
|
|
12
21
|
/**
|
|
13
22
|
* Create a CDP client connected to the given WebSocket URL.
|
|
14
23
|
* @param {string} wsUrl - WebSocket URL (ws://127.0.0.1:PORT/devtools/...)
|
|
15
|
-
* @returns {Promise<
|
|
24
|
+
* @returns {Promise<object>} CDP client ({ send, on, once, session, close })
|
|
16
25
|
*/
|
|
17
26
|
export async function createCDP(wsUrl) {
|
|
18
|
-
const ws = new WebSocket(wsUrl);
|
|
27
|
+
const ws = new WebSocket(wsUrl, { maxPayload: MAX_PAYLOAD, perMessageDeflate: false });
|
|
19
28
|
let nextId = 1;
|
|
20
29
|
const pending = new Map(); // id → { resolve, reject }
|
|
21
30
|
const listeners = new Map(); // "method" or "sessionId:method" → Set<callback>
|
|
22
31
|
|
|
23
|
-
|
|
32
|
+
/** @type {Promise<void>} */
|
|
33
|
+
const connected = new Promise((resolve, reject) => {
|
|
24
34
|
const timeout = setTimeout(() => reject(new Error('CDP connection timeout (5s)')), 5000);
|
|
25
35
|
ws.onopen = () => { clearTimeout(timeout); resolve(); };
|
|
26
36
|
ws.onerror = (e) => {
|
|
@@ -28,6 +38,7 @@ export async function createCDP(wsUrl) {
|
|
|
28
38
|
reject(new Error(`CDP WebSocket connection failed: ${e.message || 'unknown error'}`));
|
|
29
39
|
};
|
|
30
40
|
});
|
|
41
|
+
await connected;
|
|
31
42
|
|
|
32
43
|
ws.onmessage = (event) => {
|
|
33
44
|
const msg = JSON.parse(typeof event.data === 'string' ? event.data : event.data.toString());
|
package/src/chromium.js
CHANGED
|
@@ -112,7 +112,8 @@ export function findBrowser() {
|
|
|
112
112
|
* @param {number} [opts.port=0] - CDP port (0 = random available port)
|
|
113
113
|
* @param {string} [opts.userDataDir] - Browser profile directory
|
|
114
114
|
* @param {boolean} [opts.headed=false] - Launch in headed mode (with visible window)
|
|
115
|
-
* @
|
|
115
|
+
* @param {string} [opts.proxy] - Proxy server (e.g. 'http://host:port')
|
|
116
|
+
* @returns {Promise<{wsUrl: string, process: import('node:child_process').ChildProcess, port: number}>}
|
|
116
117
|
*/
|
|
117
118
|
export async function launch(opts = {}) {
|
|
118
119
|
const binary = opts.binary || findBrowser();
|
|
@@ -235,6 +236,7 @@ export async function cleanupBrowser(browser) {
|
|
|
235
236
|
if (!browser) return;
|
|
236
237
|
activeBrowsers.delete(browser);
|
|
237
238
|
if (browser.process && !browser.process.killed && browser.process.exitCode === null) {
|
|
239
|
+
/** @type {Promise<void>} */
|
|
238
240
|
const exited = new Promise((resolve) => {
|
|
239
241
|
const timer = setTimeout(resolve, 2000);
|
|
240
242
|
browser.process.once('exit', () => { clearTimeout(timer); resolve(); });
|
|
@@ -282,7 +284,10 @@ export async function cleanupBrowser(browser) {
|
|
|
282
284
|
export async function getDebugUrl(port) {
|
|
283
285
|
const res = await fetch(`http://127.0.0.1:${port}/json/version`);
|
|
284
286
|
if (!res.ok) throw new Error(`Cannot reach browser debug port at ${port}: ${res.status}`);
|
|
285
|
-
const data = await res.json();
|
|
287
|
+
const data = /** @type {{ webSocketDebuggerUrl?: string }} */ (await res.json());
|
|
288
|
+
if (!data.webSocketDebuggerUrl) {
|
|
289
|
+
throw new Error(`Browser debug port at ${port} returned no webSocketDebuggerUrl`);
|
|
290
|
+
}
|
|
286
291
|
return data.webSocketDebuggerUrl;
|
|
287
292
|
}
|
|
288
293
|
|