pi-browser-harness 0.8.3 → 0.10.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/README.md CHANGED
@@ -2,326 +2,387 @@
2
2
 
3
3
  ![pi-browser-harness](https://raw.githubusercontent.com/amankumarsingh77/pi-browser-harness/main/assets/hero.png)
4
4
 
5
- Full browser control for pi agents in **your real Chrome** — your sessions, your cookies, your tabs. Drives navigation, structured page reads, network capture, clicks, typing, screenshots, and arbitrary scripts via CDP.
5
+ **Browser control for [pi](https://github.com/badlogic/pi-mono) agents, in the Chrome you're already using.**
6
+
7
+ Your profile, your logins, your cookies. The agent reads pages as an accessibility tree with stable
8
+ element handles, fills forms that React won't revert, captures network traffic and console errors,
9
+ searches the web, and drops to raw CDP when the tools run out. 38 tools over a single CDP connection —
10
+ no MCP hop, no second browser, no extra API keys.
6
11
 
7
12
  ---
8
13
 
9
- ## Why pi-browser-harness?
10
-
11
- | Capability | pi-browser-harness | Playwright MCP | Stagehand | Puppeteer MCP |
12
- |---|:---:|:---:|:---:|:---:|
13
- | **Drives your real Chrome** (logged-in sessions preserved) | ✅ | ❌ launches its own browser | ❌ | ❌ |
14
- | **Coordinate clicks** that work through iframes, shadow DOM, cross-origin | ✅ | ❌ selector-based | ❌ selector-based | ❌ selector-based |
15
- | **Inline TUI screenshot rendering** (Kitty/iTerm2/Ghostty/WezTerm) | ✅ | ❌ | ❌ | ❌ |
16
- | **Accessibility-tree snapshot with click coords `@(x,y)` per element** | ✅ | ✅ tree only, no coords | partial | ❌ |
17
- | **Network request capture** with filters + body capture | ✅ | ✅ post-hoc list | ❌ | ❌ |
18
- | **Parallel tool execution** with automatic mutation serialization | ✅ | ❌ | ❌ | ❌ |
19
- | **Temporary scripts** with daemon + full Node.js | ✅ | ❌ | ❌ | ❌ |
20
- | **Direct HTTP GET** outside the browser (10–50× faster for APIs) | ✅ | ❌ | ❌ | ❌ |
21
- | **Tab ownership isolation** — never touches the user's other tabs | ✅ | N/A | N/A | N/A |
22
- | **Pi-native** — no MCP/JSON-RPC overhead, no extra LLM API keys | ✅ | ❌ MCP roundtrip | ❌ external LLM | ❌ MCP roundtrip |
23
- | **TypeScript strict mode**, zero `any`, all CDP casts documented | ✅ | unknown | unknown | unknown |
24
- | Ctrl+O expand/collapse on tool output | ✅ | ❌ | ❌ | ❌ |
25
- | Compositor-level dispatch (works on every site, no flakey waits) | ✅ | ❌ | ❌ | ❌ |
26
-
27
- If you live in pi and you want an agent driving the same Chrome you're already signed into, this is the only one that fits.
14
+ ## How it compares
15
+
16
+ | | **pi-browser-harness** | Playwright MCP | Puppeteer MCP | Stagehand |
17
+ |---|---|---|---|---|
18
+ | **Browser** | Your running Chrome — real profile, live sessions | Launches a clean instance | Launches a clean instance | Launches a clean instance |
19
+ | **Element targeting** | AX-tree refs (`[eN]`) that re-resolve at action time; coordinate and selector fallbacks | Selectors / AX locators | CSS selectors | LLM-inferred selectors |
20
+ | **Input dispatch** | Compositor-level CDP input — works through iframes, shadow DOM, cross-origin frames | DOM-level | DOM-level | DOM-level |
21
+ | **Framework-safe writes** | `browser_fill` writes via the native setter and fires bubbling events, so controlled inputs keep the value | Native fill | Manual | Native fill |
22
+ | **Transport** | Native pi extension, in-process | MCP / JSON-RPC round trip | MCP / JSON-RPC round trip | In-process + its own LLM calls |
23
+ | **Extra credentials** | None | None | None | Its own model API key |
24
+ | **Concurrency** | Reads run in parallel; mutations auto-serialize through a shared mutex | Sequential | Sequential | Sequential |
25
+ | **Isolation** | Dedicated Chrome window per session; refuses tabs it didn't open | N/A (own browser) | N/A | N/A |
26
+ | **Terminal output** | Inline screenshots and `Ctrl+O`-expandable tool results | Plain text | Plain text | Plain text |
27
+ | **Escape hatch** | `browser_run_script` — raw CDP + full Node in one call | — | — | — |
28
+
29
+ <sub>Reflects each project's default configuration. Corrections welcome via PR.</sub>
30
+
31
+ **The short version:** if you live in pi and want an agent driving the same Chrome you're already
32
+ signed into, this is the one that fits.
28
33
 
29
34
  ---
30
35
 
31
- ## Quick Start
36
+ ## Install
32
37
 
33
38
  ```bash
34
- # 1. Install
35
39
  pi install npm:pi-browser-harness
40
+ ```
41
+
42
+ Then enable remote debugging in your browser — either open `chrome://inspect/#remote-debugging`
43
+ (or `brave://inspect`, `edge://inspect`), tick **Discover network targets**, and click **Allow** —
44
+ or relaunch the browser with `--remote-debugging-port=9222`.
36
45
 
37
- # 2. Enable Chrome remote debugging
38
- # Open chrome://inspect/#remote-debugging in Chrome,
39
- # tick "Discover network targets", click Allow.
40
- # Or launch Chrome with --remote-debugging-port=9222
46
+ Finally, run `/browser-setup` in pi to connect. It's a one-time step — the connection survives across
47
+ pi sessions and reconnects on its own when Chrome restarts.
41
48
 
42
- # 3. Connect
43
- /browser-setup
49
+ The first time you connect, you'll be asked which browser profile the agent should work in:
50
+
51
+ ```
52
+ Select the browser profile the agent should use
53
+ vertexcover.io (aman@vertexcover.io)
54
+ ✓ Aman (personal@gmail.com)
55
+ Harness (harness@gmail.com)
56
+ — Clear selection (use whichever window is focused) —
44
57
  ```
45
58
 
46
- ### Requirements
59
+ That choice is saved to `~/.pi/agent/browser-harness.json` and reused by every project until you
60
+ change it with `/browser-profile`. It decides which logins, cookies, and extensions the agent works
61
+ with, so pinning it keeps runs reproducible. Skip the prompt and the harness behaves as it always
62
+ did: tabs open in whichever profile currently has focus.
47
63
 
48
- - pi (latest)
49
- - Node.js ≥ 22
50
- - Chrome / Chromium / Edge
64
+ **Requires:** pi (latest) · Node.js ≥ 22 · Chrome, Chromium, Brave, or Edge
51
65
 
52
66
  ---
53
67
 
54
- ## Tool hierarchy — read this first
68
+ ## The mental model
69
+
70
+ Interaction is **ref-first**. `browser_snapshot` returns the accessibility tree, and every
71
+ interactive element carries a stable ref (`[eN]`) plus click coordinates `@(x,y)`:
55
72
 
56
73
  ```
57
- What do you need to know?
58
-
59
- ├─ Page structure / what's clickable / labels?
60
- │ → browser_snapshot (DEFAULT — AX tree with @(x,y) per interactive element)
61
- │
62
- ├─ A specific element's value / attribute / coords?
63
- │ → browser_execute_js (e.g. el.innerText, el.getBoundingClientRect())
64
- │
65
- ├─ Network behavior on the current page?
66
- │ → browser_network_requests
67
- │
68
- └─ Visual rendering (layout / colors / chart drew correctly)?
69
- → browser_screenshot (LAST RESORT — pixels only)
74
+ [e7] textbox "Email" @(412,288)
75
+ [e9] combobox "Country" @(412,344)
76
+ [e12] button "Create account" @(456,412)
70
77
  ```
71
78
 
72
- Pass `@(x,y)` from `browser_snapshot` straight to `browser_click`. **No screenshot round-trip needed** to find click targets — the snapshot already has them.
79
+ Pass the ref — not the coordinates — to `browser_click`, `browser_fill`, `browser_select_option`,
80
+ `browser_focus`, `browser_upload_file`, or `browser_dispatch_key`. Refs are keyed to element
81
+ identity, so they re-resolve position at action time and survive the re-renders that SPA forms
82
+ trigger on every keystroke. Coordinates go stale the moment the layout reflows.
83
+
84
+ Three things follow from that:
85
+
86
+ - **Never screenshot to find a click target.** The snapshot already has refs and coordinates.
87
+ - **Mutating calls append a "Page changes" diff.** Read it to confirm the step landed — a panel
88
+ closed, a new `[eN]` appeared — instead of taking a screenshot to check.
89
+ - **`"ref is stale"` means the page changed.** Re-snapshot for fresh refs; don't retry blindly.
90
+
91
+ ### Which tool answers your question
92
+
93
+ | You need to know… | Reach for |
94
+ |---|---|
95
+ | What's on the page, what's clickable | `browser_snapshot` — the default |
96
+ | One element's text, value, or geometry | `browser_execute_js` — one round trip |
97
+ | An article's readable content | `browser_read_page` |
98
+ | Why nothing happened after an action | `browser_console` |
99
+ | What the page requested over the network | `browser_network_requests` |
100
+ | Whether something *rendered* correctly | `browser_screenshot` — last resort, pixels only |
73
101
 
74
102
  ---
75
103
 
76
104
  ## Tools
77
105
 
78
- ### Page inspection (use these by default)
106
+ ### Reading the page
79
107
 
80
108
  | Tool | Purpose |
81
- |------|---------|
82
- | `browser_snapshot` | **Default for page inspection.** Returns the CDP accessibility tree with click coords `@(x,y)` for every interactive element. Optional `includeScreenshot:true`. |
83
- | `browser_execute_js` | Surgical DOM reads — element text, attributes, `getBoundingClientRect()`. Cheapest, most precise. |
84
- | `browser_network_requests` | List recent network requests on the current tab. Filter by url/method/status/type/recency; optional response-body capture. |
85
- | `browser_page_info` | URL, title, viewport, scroll position, or pending dialog. |
86
- | `browser_http_get` | Direct HTTP GET outside the browser — 10-50× faster for APIs. |
109
+ |---|---|
110
+ | `browser_snapshot` | **Default.** Accessibility tree with `[eN]` refs and `@(x,y)` per interactive element. Optional `includeScreenshot`. |
111
+ | `browser_execute_js` | Surgical DOM reads — text, attributes, `getBoundingClientRect()`. Cheapest precise read. |
112
+ | `browser_page_info` | URL, title, viewport, scroll position — or the pending JS dialog, if one is blocking. |
113
+ | `browser_console` | JS errors and console output. Pass `sinceSeq` from the previous `nextCursor` to see only what an action caused. |
114
+ | `browser_screenshot` | PNG/JPEG capture. For verifying visual rendering only. |
87
115
 
88
- ### Visual (last resort)
116
+ ### Web research
89
117
 
90
118
  | Tool | Purpose |
91
- |------|---------|
92
- | `browser_screenshot` | Capture PNG/JPEG. Use only when you need to verify visual rendering. |
119
+ |---|---|
120
+ | `browser_web_search` | Query → ranked `{title, url, snippet, rank}`, by scraping a real Google SERP in your Chrome. No API key. Links only. |
121
+ | `browser_read_page` | Reader mode. A `url` (opened and closed in its own tab) or an owned `targetId` → clean article text, boilerplate stripped. |
122
+
123
+ Both run in isolated tabs and never disturb your current page.
124
+
125
+ ### Interacting
93
126
 
94
- ### Navigation
127
+ | Tool | Purpose |
128
+ |---|---|
129
+ | `browser_click` | Click by `ref` (preferred) or viewport `x`/`y`. Compositor-level, so iframes and shadow DOM work. |
130
+ | `browser_fill` | Set an input, textarea, or contenteditable. Fires bubbling `input`/`change`, returns the value written. |
131
+ | `browser_select_option` | Choose in a native `<select>` by value, label, or index. Returns available options on no match. |
132
+ | `browser_focus` | Deterministic `.focus()` — no coordinate accuracy needed. |
133
+ | `browser_type` | Type into the focused element. For keystroke-sensitive widgets; prefer `browser_fill` otherwise. |
134
+ | `browser_press_key` | Press a key with an optional modifier bitfield. |
135
+ | `browser_dispatch_key` | Synthetic DOM `KeyboardEvent` on one element, for pages that ignore raw CDP keys. |
136
+ | `browser_scroll` | Scroll by delta. W3C wheel convention: positive `deltaY` = down. |
137
+ | `browser_drag_and_drop` | Drag between two coordinate pairs. |
138
+
139
+ ### Navigation and tabs
95
140
 
96
141
  | Tool | Purpose |
97
- |------|---------|
98
- | `browser_navigate` / `browser_new_tab` | Navigate or open a tab |
99
- | `browser_open_urls` | Open multiple URLs in parallel tabs |
100
- | `browser_go_back` / `browser_go_forward` / `browser_reload` | History navigation |
101
- | `browser_list_tabs` / `browser_current_tab` / `browser_switch_tab` / `browser_close_tab` | Tab management (only tabs this session opened) |
142
+ |---|---|
143
+ | `browser_navigate` | Go to a URL in the current tab. |
144
+ | `browser_new_tab` / `browser_open_urls` | Open one tab, or many URLs in parallel. |
145
+ | `browser_go_back` / `browser_go_forward` / `browser_reload` | History. |
146
+ | `browser_list_tabs` / `browser_current_tab` | List tabs (`scope:"owned"` by default) and inspect the attached one. |
147
+ | `browser_switch_tab` / `browser_close_tab` | Switch or close — owned tabs only. |
102
148
 
103
- ### Interaction
149
+ ### Waiting and dialogs
104
150
 
105
151
  | Tool | Purpose |
106
- |------|---------|
107
- | `browser_click` | Click at viewport coordinates (use `@(x,y)` from `browser_snapshot`) |
108
- | `browser_type` | Type text into the focused element |
109
- | `browser_press_key` | Press a key with optional modifiers |
110
- | `browser_scroll` | Scroll the page by delta pixels |
152
+ |---|---|
153
+ | `browser_wait_for` | Wait until a selector appears (or disappears with `gone:true`), or text shows up. **Prefer this over sleeping.** |
154
+ | `browser_wait_for_load` | Wait for `document.readyState === "complete"`. |
155
+ | `browser_wait` | Fixed delay, in seconds. |
156
+ | `browser_handle_dialog` | Accept or dismiss `alert` / `confirm` / `prompt`. |
111
157
 
112
- ### Utility
158
+ ### Network
113
159
 
114
160
  | Tool | Purpose |
115
- |------|---------|
116
- | `browser_wait` / `browser_wait_for_load` | Sleep, or wait for `readyState === 'complete'` |
117
- | `browser_handle_dialog` | Accept or dismiss `alert` / `confirm` / `prompt` |
118
- | `browser_run_script` | Execute a temporary script with daemon and Node.js access |
161
+ |---|---|
162
+ | `browser_network_requests` | Requests captured on the current tab since attach. Filter by URL, method, status, resource type, or recency; optional response bodies. |
163
+ | `browser_http_get` | Plain GET outside the browser. Much faster than navigate-then-read for JSON APIs — but no JS rendering. |
119
164
 
120
- Three tools (`browser_snapshot`, `browser_network_requests`, `browser_execute_js`) ship custom TUI rendering with **Ctrl+O** (`app.tools.expand`) to toggle between compact and full output.
165
+ ### Files and viewport
121
166
 
122
- ---
167
+ | Tool | Purpose |
168
+ |---|---|
169
+ | `browser_upload_file` | Set files on a file input, with a DataTransfer fallback for stubborn pages. |
170
+ | `browser_download` | Set the download directory and suppress the save-as prompt. |
171
+ | `browser_print_to_pdf` | Print the page to PDF via `Page.printToPDF`. |
172
+ | `browser_viewport_resize` | Override viewport size and device pixel ratio for responsive checks. |
123
173
 
124
- ## Core patterns
174
+ ### Escape hatches
125
175
 
126
- ### Page inspection
176
+ | Tool | Purpose |
177
+ |---|---|
178
+ | `browser_run_script` | Run a temporary script with raw CDP and full Node access. See [Extending](#extending). |
179
+ | `browser_setup` | Agent-callable setup. Idempotent — how the agent recovers from `not initialized` on its own. |
127
180
 
128
- ```
129
- browser_snapshot()
130
- # → AX outline with @(x,y) per button/link/input
131
- ```
181
+ `browser_snapshot`, `browser_network_requests`, `browser_console`, `browser_execute_js`,
182
+ `browser_web_search`, and `browser_read_page` ship custom TUI rendering: compact by default,
183
+ full output on **`Ctrl+O`** (`app.tools.expand`). The model always receives the complete result
184
+ regardless of render state.
185
+
186
+ ---
187
+
188
+ ## Recipes
132
189
 
133
- ### Form filling (no screenshots)
190
+ ### Fill a form on an SPA
191
+
192
+ Wait, snapshot once, then act by ref. No screenshots anywhere in this loop.
134
193
 
135
194
  ```
136
- browser_snapshot() # find input @(x,y) and labels
137
- browser_click({ x, y }) # click using snapshot's @(x,y)
138
- browser_type({ text: "query" })
139
- browser_press_key({ key: "Enter" })
140
- browser_wait_for_load()
141
- browser_snapshot() # verify next state
195
+ browser_wait_for({ selector: "#email" })
196
+ browser_snapshot() // → [e7] textbox, [e9] combobox, [e12] button
197
+ browser_fill({ ref: "e7", value: "a@b.com" })
198
+ browser_select_option({ ref: "e9", label: "India" })
199
+ browser_click({ ref: "e12" }) // re-resolves position even after reflow
142
200
  ```
143
201
 
144
- ### Data extraction
202
+ Each mutating call returns a "Page changes" diff — that's your confirmation the step landed.
203
+
204
+ ### Extract data
145
205
 
146
206
  ```js
147
207
  // One value
148
208
  browser_execute_js({ expression: "document.querySelector('.price').innerText" })
149
209
 
150
- // Direct API call outside the browser
151
- browser_http_get({ url: "https://api.github.com/repos/amankumarsingh77/pi-browser-harness" })
152
-
153
- // Structured arrays
210
+ // Structured
154
211
  browser_execute_js({ expression: `JSON.stringify(
155
212
  Array.from(document.querySelectorAll('.result')).map(el => ({
156
213
  title: el.querySelector('h3')?.textContent,
157
214
  link: el.querySelector('a')?.href,
158
215
  }))
159
216
  )` })
217
+
218
+ // Skip the browser entirely for a JSON endpoint
219
+ browser_http_get({ url: "https://api.github.com/repos/amankumarsingh77/pi-browser-harness" })
160
220
  ```
161
221
 
162
- ### Network debugging
222
+ ### Debug a failing page
163
223
 
164
224
  ```
165
- browser_navigate({ url: "https://app.example.com/feed" })
166
- browser_wait_for_load()
167
- browser_network_requests({
225
+ browser_click({ ref: "e12" })
226
+ browser_console({ levels: ["error", "warn"] }) // did JS throw?
227
+ browser_network_requests({ // did a request 4xx/5xx?
168
228
  urlPattern: "/api/",
169
229
  statusFilter: { min: 400 },
170
230
  includeResponseBodies: true
171
231
  })
172
232
  ```
173
233
 
174
- ### Research workflow
234
+ ### Search, then read
175
235
 
176
236
  ```
177
- browser_navigate({ url: "https://google.com/search?q=..." })
178
- browser_open_urls({ urls: ["url1", "url2", "url3"] })
179
- browser_list_tabs()
180
- browser_switch_tab({ targetId: "..." })
181
- browser_wait_for_load()
182
- browser_snapshot()
183
- browser_execute_js({ expression: "document.querySelector('.content').innerText" })
237
+ browser_web_search({ query: "chrome devtools protocol overview", limit: 5 })
238
+ browser_read_page({ url: "<the best result's url>" })
184
239
  ```
185
240
 
186
- ### Visual verification (only when pixels matter)
187
-
188
- ```
189
- browser_click({ x, y }) # got coords from browser_snapshot
190
- browser_snapshot() # confirm the form transitioned
191
- browser_screenshot() # ONLY if you need to verify a chart/modal/CSS rendered correctly
192
- ```
241
+ For a question that needs several sources synthesized into a **cited report**, use
242
+ `/deep-research <question>`. It decomposes the question, fans out one isolated
243
+ `web-search-researcher` subagent per sub-question, loops for coverage under a hard ceiling, and
244
+ writes a Markdown report where every claim carries a source link. The skill also auto-triggers on
245
+ research-shaped requests without the command.
193
246
 
194
247
  ### Keyboard modifiers
195
248
 
196
- | Key | Bit |
197
- |-----|-----|
198
- | Alt | 1 |
199
- | Ctrl | 2 |
200
- | Meta / Cmd | 4 |
201
- | Shift | 8 |
249
+ Modifiers are a bitfield: `1` Alt · `2` Ctrl · `4` Meta/Cmd · `8` Shift.
202
250
 
203
251
  ```
204
252
  browser_press_key({ key: "c", modifiers: 2 }) // Ctrl+C
205
- browser_press_key({ key: "v", modifiers: 4 }) // Cmd+V
206
253
  browser_press_key({ key: "T", modifiers: 10 }) // Ctrl+Shift+T
207
254
  ```
208
255
 
209
- ### Dialogs
210
-
211
- JS dialogs freeze the page. Check `browser_page_info` first — if it reports a dialog, handle it before anything else:
212
-
213
- ```
214
- browser_handle_dialog({ action: "accept" }) // confirm
215
- browser_handle_dialog({ action: "dismiss" }) // cancel
216
- browser_handle_dialog({ action: "accept", promptText: "hello" }) // prompt
217
- ```
218
-
219
256
  ---
220
257
 
221
- ## Parallel execution
258
+ ## Concurrency and isolation
222
259
 
223
- Observation tools run in parallel by default. Mutation tools (`click`, `type`, `scroll`, `navigate`, `switch_tab`, …) are automatically serialized through a shared mutex — emit them in the same turn and the harness FIFO-queues them.
260
+ **Parallel reads, serialized writes.** Observation tools run concurrently with each other and with
261
+ mutations. Mutations (`click`, `fill`, `type`, `scroll`, `navigate`, `switch_tab`, …) pass through a
262
+ shared async mutex and FIFO-queue automatically, so emitting several in one turn is safe.
224
263
 
225
264
  ```
226
- # All three run concurrently
227
- browser_snapshot()
228
- browser_network_requests({ sinceMs: 5000 })
229
- browser_http_get({ url: "..." })
265
+ browser_snapshot() + browser_network_requests({ sinceMs: 5000 }) + browser_http_get({ url: "..." })
230
266
  ```
231
267
 
232
- ---
233
-
234
- ## Tab ownership
235
-
236
- The harness never touches tabs you didn't open through it. On first attach it spawns a dedicated Chrome window; subsequent `browser_new_tab` calls open inside that window. `browser_list_tabs` defaults to `scope:"owned"` (pass `scope:"all"` to see read-only listings of your other tabs); `browser_switch_tab` and `browser_close_tab` refuse non-owned tabs.
268
+ **Tab ownership.** On first attach the harness opens a dedicated Chrome window and only operates
269
+ inside it. `browser_list_tabs` defaults to `scope:"owned"` (pass `scope:"all"` for a read-only view
270
+ of your other tabs); `browser_switch_tab` and `browser_close_tab` refuse anything this session
271
+ didn't open. Owned tabs are closed at session shutdown, so nothing stale accumulates in your browser.
237
272
 
238
273
  ---
239
274
 
240
- ## Temporary scripts
275
+ ## Extending
241
276
 
242
- When the built-in tools aren't enough, write a script to disk and run it. Scripts get a `daemon` binding for direct CDP access — much faster than chaining tool calls.
277
+ When the built-in tools can't express a workflow, write a script and run it. It executes inside the
278
+ harness process with a live CDP handle — far faster than chaining tool calls.
243
279
 
244
280
  ```js
245
- write("/tmp/scrape-pages.js", `
281
+ write("<tmpdir>/scrape.js", `
246
282
  const results = [];
247
283
  for (const url of params.urls) {
248
284
  await daemon.session().call("Page.navigate", { url });
249
285
  await new Promise(r => setTimeout(r, 2000));
250
- const title = await daemon.evaluateJs("document.title");
251
- results.push({ url, title });
286
+ results.push({ url, title: await daemon.evaluateJs("document.title") });
252
287
  }
253
288
  return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
254
289
  `)
255
290
 
256
- browser_run_script({ path: "/tmp/scrape-pages.js", params: { urls: [...] } })
291
+ browser_run_script({ path: "<tmpdir>/scrape.js", params: { urls: [...] } })
257
292
  ```
258
293
 
259
- **Bindings:** `params`, `daemon`, `require`, `signal`, `onUpdate`, `ctx`, `console`, `fetch`, `JSON`, `Buffer`, `setTimeout`, `clearTimeout`.
294
+ **Bindings:** `params`, `daemon`, `require`, `signal`, `onUpdate`, `ctx`, `console`, `fetch`,
295
+ `JSON`, `Buffer`, `setTimeout`, `clearTimeout`.
260
296
 
261
- `daemon` exposes: `evaluateJs`, `pageInfo`, `listTabs`, `switchTab`, `newTab`, `current`, and `session(targetId?)` for raw CDP via `session.call` / `session.callOnTarget` / `session.callBrowser` / `session.takeDialog`.
297
+ `daemon` exposes `evaluateJs`, `pageInfo`, `listTabs`, `switchTab`, `newTab`, `closeTab`, `current`,
298
+ `status`, and `session()` for raw CDP (`call` · `callOnTarget` · `callBrowser` · `takeDialog`).
262
299
 
263
- Scripts are written to disk — auditable and re-runnable.
300
+ Scripts must live under the OS temp dir, the cwd, or `BH_SCRIPT_DIR`, and every call carries a
301
+ mandatory timeout (60s default, 600s max). They land on disk first, so they're reviewable and
302
+ re-runnable.
264
303
 
265
304
  ---
266
305
 
267
- ## Commands
306
+ ## Commands, flags, and environment
307
+
308
+ | Command | Purpose |
309
+ |---|---|
310
+ | `/browser-setup` | Connect pi to your browser. Run once; idempotent. |
311
+ | `/browser-profile` | Pick which browser profile the agent works in. Remembered across sessions. |
312
+ | `/browser-status` | Connection state, selected profile, and current page. |
313
+ | `/deep-research <question>` | Multi-source web research → cited Markdown report. |
314
+ | `/browser-reload-daemon` | Force a client restart. Rarely needed — the transport reconnects on its own. |
268
315
 
269
- | Command | Description |
270
- |---------|-------------|
271
- | `/browser-setup` | Connect pi to Chrome (run once) |
272
- | `/browser-status` | Show daemon health and current page |
273
- | `/browser-reload-daemon` | Restart the connection |
316
+ | Flag / variable | Effect |
317
+ |---|---|
318
+ | `--browser-namespace <name>` | Override the session namespace (default: auto-generated). |
319
+ | `--browser-debug-clicks` | Save annotated click screenshots for debugging. |
320
+ | `BU_CDP_WS` | Attach to a remote browser by WebSocket URL instead of discovering a local one. |
321
+ | `BU_CDP_PORTS` | Extra ports to probe during CDP discovery, beyond 9222. |
322
+ | `BH_SCRIPT_DIR` | Additional directory `browser_run_script` will accept paths from. |
274
323
 
275
324
  ---
276
325
 
277
- ## What NOT to do
326
+ ## Anti-patterns
278
327
 
279
- - Don't launch your own browser — you're connected to the user's real Chrome
280
- - Don't type credentials — if you hit an auth wall, stop and ask
281
- - Don't screenshot to understand the page — `browser_snapshot` is the default
282
- - Don't screenshot to find click coordinates — `browser_snapshot`'s `@(x,y)` is exact
283
- - Don't screenshot to read a value — `browser_execute_js` is one round-trip
284
- - Don't ignore dialogs — check `browser_page_info` first
328
+ - **Don't launch your own browser.** You're attached to the user's real Chrome.
329
+ - **Don't type credentials.** Hit an auth wall — stop and ask.
330
+ - **Don't screenshot to understand a page, find coordinates, or read a value.** `browser_snapshot`
331
+ and `browser_execute_js` are cheaper and exact.
332
+ - **Don't guess CSS selectors** when a snapshot ref exists.
333
+ - **Don't sleep to wait.** `browser_wait_for` is deterministic; `browser_wait` is a last resort.
334
+ - **Don't ignore dialogs.** They freeze the page's JS thread — `browser_page_info` reports them,
335
+ `browser_handle_dialog` clears them.
285
336
 
286
337
  ---
287
338
 
288
339
  ## Architecture
289
340
 
290
341
  ```
291
- pi agent → pi-browser-harness (TypeScript)
292
- │ CDP WebSocket
293
- ▼
294
- Chrome
342
+ pi agent
343
+ │ browser_* tools (in-process extension)
344
+ ▼
345
+ pi-browser-harness ──► daemon (Unix socket / named pipe) ──► Chrome
346
+ CDP WebSocket
295
347
  ```
296
348
 
297
- Temporary scripts run inside the harness process with full daemon and Node.js access.
349
+ A long-lived daemon owns the CDP connection, so it outlives individual pi sessions — which is why
350
+ you only see Chrome's "Allow Remote Debugging" prompt once. `browser_run_script` executes in the
351
+ harness process with direct daemon and Node access.
298
352
 
299
353
  ---
300
354
 
301
355
  ## Troubleshooting
302
356
 
303
357
  | Symptom | Fix |
304
- |---------|-----|
305
- | `DevToolsActivePort not found` | Open `chrome://inspect/#remote-debugging`, tick the checkbox, click Allow |
306
- | Connection fails after Chrome restart | Run `/browser-reload-daemon` |
307
- | Page seems loaded but content is missing | SPA — call `browser_snapshot` again, or `browser_execute_js` for a specific element |
308
- | JS dialog is blocking actions | `browser_page_info` will report it — use `browser_handle_dialog` |
309
- | Daemon not starting | Run `/browser-setup` to re-run guided setup |
310
- | Snapshot didn't return `@(x,y)` for a target | Element wasn't recognized as interactive. Fall back to `browser_execute_js` with `getBoundingClientRect()` |
358
+ |---|---|
359
+ | `DevToolsActivePort not found` | Open `chrome://inspect/#remote-debugging`, tick **Discover network targets**, click **Allow**. Or relaunch with `--remote-debugging-port=9222`. |
360
+ | `No browser instance running` | Chrome/Brave/Edge isn't open, or only helper processes remain. Open the browser, then `/browser-setup`. |
361
+ | `Browser harness not initialized` | Run `/browser-setup` — or let the agent call `browser_setup` itself. |
362
+ | `ref is stale` | The page re-rendered. Re-run `browser_snapshot` for fresh refs. |
363
+ | Page looks loaded but content is missing | SPA still rendering. Use `browser_wait_for({ selector })`, not a fixed sleep. |
364
+ | Click did nothing | Check `browser_console` for a thrown error, then `browser_network_requests` for a failed call. |
365
+ | Actions all hang | A JS dialog is blocking. `browser_page_info` reports it; `browser_handle_dialog` clears it. |
366
+ | No `[eN]` ref for your target | The element wasn't exposed as interactive. Fall back to a CSS `selector`, or `browser_execute_js` with `getBoundingClientRect()`. |
367
+ | Connected but the wrong browser responds | A stale `DevToolsActivePort` file. Quit every Chromium-family browser, reopen one, then `/browser-setup`. |
368
+ | Agent acts as the wrong account | No profile is pinned, so tabs open in whichever profile has focus. Run `/browser-profile` and pick one. |
369
+ | `couldn't open a window in "…" automatically` | The profile window couldn't be opened for you. Open that profile from the browser's profile menu, then retry — the harness never falls back to a different profile silently. |
311
370
 
312
371
  ---
313
372
 
314
- ## Contributing
373
+ ## Security
315
374
 
316
- See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, conventions, and PR process.
375
+ This extension drives your real browser. The agent can read any page you're signed into, submit
376
+ forms, and act inside authenticated sessions. `browser_run_script` is full code execution in the pi
377
+ process with unrestricted `require` — **review any script before running it.** Scripts are
378
+ path-restricted and timeout-bounded, but that is a blast-radius limit, not a sandbox.
317
379
 
318
380
  ---
319
381
 
320
- ## Security
321
-
322
- This extension drives your real Chrome. The agent can see open tabs, read page content, submit forms, and act inside authenticated sessions. `browser_run_script` evaluates JavaScript in the pi process with full `require` access — review any temporary scripts before executing them.
382
+ ## Contributing
323
383
 
324
- ---
384
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, conventions, and the PR process.
385
+ Release history lives in [CHANGELOG.md](CHANGELOG.md).
325
386
 
326
387
  ## License
327
388
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-browser-harness",
3
- "version": "0.8.3",
3
+ "version": "0.10.0",
4
4
  "description": "Browser control extension for pi — navigate, click, type, screenshot, and extract data from real Chrome via CDP",
5
5
  "keywords": [
6
6
  "pi-package",