pi-browser-harness 0.4.0 โ†’ 0.6.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 CHANGED
@@ -2,6 +2,33 @@
2
2
 
3
3
  All notable changes to pi-browser-harness will be documented in this file.
4
4
 
5
+ ## 0.6.0 โ€” 2026-06-21
6
+
7
+ ### Added
8
+
9
+ - **Per-tab isolation for multi-agent safety.** Each agent's browser session now operates in a dedicated Chrome window with tab ownership tracking. Tabs opened via `browser_open_urls` and `browser_new_tab` are automatically registered in the ownership registry and tagged with a ๐ŸŸข prefix in the document title for user visibility. `browser_list_tabs` scoped to `"owned"` (default) only shows the current session's tabs.
10
+ - **Unix socket daemon with auto-reconnect.** The browser daemon now binds to a Unix-domain socket (`pi-browser.sock`) in the harness temp directory instead of a TCP port. The transport layer reconnects automatically when Chrome restarts โ€” no manual `/browser-reload-daemon` needed.
11
+ - **On-demand browser initialization.** Chrome is no longer launched eagerly at harness startup. The harness attaches lazily on the first browser tool call, reducing resource usage when the agent isn't using the browser.
12
+ - **`browser_setup` as agent-callable tool.** The setup tool can now be called programmatically by agents, not just via slash command. Idempotent โ€” safe to call when already connected.
13
+
14
+ ### Fixed
15
+
16
+ - **Chrome detection on macOS** now matches by process-name substring instead of exact binary path comparison, fixing false negatives when Chrome is running from different installation paths (e.g. `/Applications/Google Chrome.app/` vs user-local copies).
17
+
18
+ ### Added
19
+
20
+ - **`browser_console`** โ€” new tool. Reads JS errors and console messages from the active tab via two CDP sources merged into one buffer: `Runtime.consoleAPICalled` (page `console.*` calls and uncaught exceptions) and `Log.entryAdded` (browser-level entries โ€” CSP violations, mixed content, deprecations, network errors). Filters: `levels` (log/info/warn/error/debug), `textPattern` (substring; wrap in slashes for regex), `sinceMs`, `limit` (default 50, cap 500). Each record carries a monotonic `seq`; the response includes `nextCursor` so callers can pass `sinceSeq` to see only what's new since the previous drain โ€” the cursor pattern that makes "what did this action cause?" answerable in one call. Buffer is page-scoped (cleared on tab switch) and bounded at 500 records; `bufferOverflowed` flag reports drops since the last drain. Stack traces (top 3 frames) are preserved for error/warn records. Per-arg cap of 2 KB prevents a single `console.log(hugeBlob)` from blowing the buffer.
21
+ - **`Log` CDP domain enabled** alongside Page/DOM/Runtime/Network/Accessibility on every attach. (`Runtime` was already enabled, so `Runtime.consoleAPICalled` was reachable; this adds the missing domain for browser-level entries.)
22
+ - **`browser_console` ships with a custom `renderResult`** following the established pattern โ€” collapsed: header counts (`3 errors ยท 2 warnings ยท 47 logs`) + last 5 rows. Expanded: full list with stack traces inline as code fences. Appends `keyHint("app.tools.expand", ...)` so the binding label adapts to user remaps.
23
+
24
+ ### Internal
25
+
26
+ - New pure module `src/cdp/console-buffer.ts` mirrors the `network-buffer.ts` pattern (insertion-ordered Map, FIFO eviction at capacity 500, overflow flag reset per drain). Wired into the existing single-consumer event loop in `src/cdp/session.ts`.
27
+
28
+ ### Known follow-ups
29
+
30
+ - No tests added in this release. Verification is manual against real Chrome.
31
+
5
32
  ## 0.4.0 โ€” 2026-05-06
6
33
 
7
34
  ### Added
package/README.md CHANGED
@@ -2,7 +2,29 @@
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. Navigate, click, type, screenshot, and extract data from your real Chrome โ€” all driven through natural language via CDP.
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.
6
+
7
+ ---
8
+
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.
6
28
 
7
29
  ---
8
30
 
@@ -29,84 +51,144 @@ pi install npm:pi-browser-harness
29
51
 
30
52
  ---
31
53
 
54
+ ## Tool hierarchy โ€” read this first
55
+
56
+ ```
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)
70
+ ```
71
+
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.
73
+
74
+ ---
75
+
32
76
  ## Tools
33
77
 
78
+ ### Page inspection (use these by default)
79
+
34
80
  | Tool | Purpose |
35
81
  |------|---------|
36
- | `browser_screenshot` | Capture a PNG screenshot of the current page |
37
- | `browser_click` | Click at viewport coordinates |
38
- | `browser_type` | Type text into the focused element |
39
- | `browser_press_key` | Press a key (Enter, Tab, Escape, arrows, etc.) with optional modifiers |
40
- | `browser_scroll` | Scroll the page by delta pixels |
41
- | `browser_navigate` | Navigate to a URL |
42
- | `browser_new_tab` | Open a new tab, optionally navigate |
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. |
87
+
88
+ ### Visual (last resort)
89
+
90
+ | Tool | Purpose |
91
+ |------|---------|
92
+ | `browser_screenshot` | Capture PNG/JPEG. Use only when you need to verify visual rendering. |
93
+
94
+ ### Navigation
95
+
96
+ | Tool | Purpose |
97
+ |------|---------|
98
+ | `browser_navigate` / `browser_new_tab` | Navigate or open a tab |
43
99
  | `browser_open_urls` | Open multiple URLs in parallel tabs |
44
100
  | `browser_go_back` / `browser_go_forward` / `browser_reload` | History navigation |
45
- | `browser_page_info` | Get URL, title, viewport, scroll position, or dialog state |
46
- | `browser_list_tabs` / `browser_current_tab` / `browser_switch_tab` | Tab discovery and switching |
47
- | `browser_execute_js` | Run JavaScript in the page and return the result |
48
- | `browser_http_get` | Direct HTTP GET outside the browser โ€” much faster for APIs |
101
+ | `browser_list_tabs` / `browser_current_tab` / `browser_switch_tab` / `browser_close_tab` | Tab management (only tabs this session opened) |
102
+
103
+ ### Interaction
104
+
105
+ | 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 |
111
+
112
+ ### Utility
113
+
114
+ | Tool | Purpose |
115
+ |------|---------|
49
116
  | `browser_wait` / `browser_wait_for_load` | Sleep, or wait for `readyState === 'complete'` |
50
117
  | `browser_handle_dialog` | Accept or dismiss `alert` / `confirm` / `prompt` |
51
118
  | `browser_run_script` | Execute a temporary script with daemon and Node.js access |
52
119
 
53
- ---
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.
54
121
 
55
- ## Core Patterns
122
+ ---
56
123
 
57
- The golden rule: **screenshot โ†’ act โ†’ screenshot โ†’ verify.** Never assume โ€” always look.
124
+ ## Core patterns
58
125
 
59
- ### Navigation
126
+ ### Page inspection
60
127
 
61
128
  ```
62
- browser_new_tab({ url: "https://example.com" })
63
- browser_wait_for_load()
64
- browser_screenshot()
129
+ browser_snapshot()
130
+ # โ†’ AX outline with @(x,y) per button/link/input
65
131
  ```
66
132
 
67
- ### Form filling
133
+ ### Form filling (no screenshots)
68
134
 
69
135
  ```
70
- browser_screenshot() # see the form
71
- browser_click({ x: 400, y: 200 }) # click the search input
72
- browser_type({ text: "query" }) # type
73
- browser_press_key({ key: "Enter" }) # submit
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" })
74
140
  browser_wait_for_load()
75
- browser_screenshot() # verify
141
+ browser_snapshot() # verify next state
76
142
  ```
77
143
 
78
144
  ### Data extraction
79
145
 
80
- ```
146
+ ```js
147
+ // One value
81
148
  browser_execute_js({ expression: "document.querySelector('.price').innerText" })
82
149
 
150
+ // Direct API call outside the browser
83
151
  browser_http_get({ url: "https://api.github.com/repos/amankumarsingh77/pi-browser-harness" })
84
152
 
85
153
  // Structured arrays
86
- browser_execute_js({ expression: `Array.from(document.querySelectorAll('.result')).map(el => ({
87
- title: el.querySelector('h3')?.textContent,
88
- link: el.querySelector('a')?.href,
89
- }))` })
154
+ browser_execute_js({ expression: `JSON.stringify(
155
+ Array.from(document.querySelectorAll('.result')).map(el => ({
156
+ title: el.querySelector('h3')?.textContent,
157
+ link: el.querySelector('a')?.href,
158
+ }))
159
+ )` })
160
+ ```
161
+
162
+ ### Network debugging
163
+
164
+ ```
165
+ browser_navigate({ url: "https://app.example.com/feed" })
166
+ browser_wait_for_load()
167
+ browser_network_requests({
168
+ urlPattern: "/api/",
169
+ statusFilter: { min: 400 },
170
+ includeResponseBodies: true
171
+ })
90
172
  ```
91
173
 
92
174
  ### Research workflow
93
175
 
94
176
  ```
95
177
  browser_navigate({ url: "https://google.com/search?q=..." })
96
- // read search results
97
178
  browser_open_urls({ urls: ["url1", "url2", "url3"] })
98
179
  browser_list_tabs()
99
180
  browser_switch_tab({ targetId: "..." })
100
- browser_screenshot()
181
+ browser_wait_for_load()
182
+ browser_snapshot()
101
183
  browser_execute_js({ expression: "document.querySelector('.content').innerText" })
102
184
  ```
103
185
 
104
- ### Scrolling
186
+ ### Visual verification (only when pixels matter)
105
187
 
106
188
  ```
107
- browser_screenshot()
108
- browser_scroll({ deltaY: -500 })
109
- browser_screenshot()
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
110
192
  ```
111
193
 
112
194
  ### Keyboard modifiers
@@ -118,55 +200,65 @@ browser_screenshot()
118
200
  | Meta / Cmd | 4 |
119
201
  | Shift | 8 |
120
202
 
121
- Combine with bitwise OR:
122
203
  ```
123
204
  browser_press_key({ key: "c", modifiers: 2 }) // Ctrl+C
124
205
  browser_press_key({ key: "v", modifiers: 4 }) // Cmd+V
125
206
  browser_press_key({ key: "T", modifiers: 10 }) // Ctrl+Shift+T
126
207
  ```
127
208
 
128
- ### SPAs (React, Vue, etc.)
209
+ ### Dialogs
129
210
 
130
- `browser_wait_for_load` only checks `readyState`. Poll for elements after:
211
+ JS dialogs freeze the page. Check `browser_page_info` first โ€” if it reports a dialog, handle it before anything else:
131
212
 
132
213
  ```
133
- browser_execute_js({ expression: "!!document.querySelector('.loaded-content')" })
214
+ browser_handle_dialog({ action: "accept" }) // confirm
215
+ browser_handle_dialog({ action: "dismiss" }) // cancel
216
+ browser_handle_dialog({ action: "accept", promptText: "hello" }) // prompt
134
217
  ```
135
218
 
136
- ### Dialogs
219
+ ---
220
+
221
+ ## Parallel execution
137
222
 
138
- JS dialogs (`alert` / `confirm` / `prompt`) freeze the page. Check `browser_page_info` first โ€” if it reports a dialog, handle it before anything else:
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.
139
224
 
140
225
  ```
141
- browser_handle_dialog({ action: "accept" }) // confirm
142
- browser_handle_dialog({ action: "dismiss" }) // cancel
143
- browser_handle_dialog({ action: "accept", promptText: "hello" }) // prompt
226
+ # All three run concurrently
227
+ browser_snapshot()
228
+ browser_network_requests({ sinceMs: 5000 })
229
+ browser_http_get({ url: "..." })
144
230
  ```
145
231
 
146
232
  ---
147
233
 
148
- ## Temporary Scripts
234
+ ## Tab ownership
149
235
 
150
- When built-in tools aren't enough, write a script to disk and run it:
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.
237
+
238
+ ---
239
+
240
+ ## Temporary scripts
241
+
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.
151
243
 
152
244
  ```js
153
- // 1. Write the script
154
245
  write("/tmp/scrape-pages.js", `
155
246
  const results = [];
156
247
  for (const url of params.urls) {
157
- await daemon.cdp("Page.navigate", { url });
248
+ await daemon.session().call("Page.navigate", { url });
158
249
  await new Promise(r => setTimeout(r, 2000));
159
- const title = await daemon.evaluateJS("document.title");
250
+ const title = await daemon.evaluateJs("document.title");
160
251
  results.push({ url, title });
161
252
  }
162
253
  return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
163
254
  `)
164
255
 
165
- // 2. Execute it
166
256
  browser_run_script({ path: "/tmp/scrape-pages.js", params: { urls: [...] } })
167
257
  ```
168
258
 
169
- **Script bindings:** `params`, `daemon`, `require`, `signal`, `onUpdate`, `ctx`, `console`, `fetch`, `JSON`, `Buffer`, `setTimeout`, `clearTimeout`.
259
+ **Bindings:** `params`, `daemon`, `require`, `signal`, `onUpdate`, `ctx`, `console`, `fetch`, `JSON`, `Buffer`, `setTimeout`, `clearTimeout`.
260
+
261
+ `daemon` exposes: `evaluateJs`, `pageInfo`, `listTabs`, `switchTab`, `newTab`, `current`, and `session(targetId?)` for raw CDP via `session.call` / `session.callOnTarget` / `session.callBrowser` / `session.takeDialog`.
170
262
 
171
263
  Scripts are written to disk โ€” auditable and re-runnable.
172
264
 
@@ -186,8 +278,9 @@ Scripts are written to disk โ€” auditable and re-runnable.
186
278
 
187
279
  - Don't launch your own browser โ€” you're connected to the user's real Chrome
188
280
  - Don't type credentials โ€” if you hit an auth wall, stop and ask
189
- - Don't assume a page action worked โ€” screenshot to verify
190
- - Don't write pixel coordinates into documentation โ€” describe selectors and text instead
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
191
284
  - Don't ignore dialogs โ€” check `browser_page_info` first
192
285
 
193
286
  ---
@@ -211,9 +304,10 @@ Temporary scripts run inside the harness process with full daemon and Node.js ac
211
304
  |---------|-----|
212
305
  | `DevToolsActivePort not found` | Open `chrome://inspect/#remote-debugging`, tick the checkbox, click Allow |
213
306
  | Connection fails after Chrome restart | Run `/browser-reload-daemon` |
214
- | Page seems loaded but content is missing | SPA โ€” poll with `browser_execute_js` for the element you need |
215
- | A JS dialog is blocking actions | `browser_page_info` will report it โ€” use `browser_handle_dialog` |
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` |
216
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()` |
217
311
 
218
312
  ---
219
313
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-browser-harness",
3
- "version": "0.4.0",
3
+ "version": "0.6.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",
@@ -52,6 +52,7 @@
52
52
  "@mariozechner/pi-coding-agent": "*",
53
53
  "@mariozechner/pi-tui": "*",
54
54
  "@types/ws": "^8.18.1",
55
+ "tsx": "^4.22.4",
55
56
  "typebox": "*",
56
57
  "typescript": "^5.0.0"
57
58
  },
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: pi-browser-harness
3
- description: Direct browser control via CDP. Use when the user wants to automate, scrape, test, or interact with web pages. Connects to the user's already-running Chrome. Default to browser_snapshot for understanding pages and browser_execute_js for surgical reads โ€” browser_screenshot is for visual verification only.
3
+ description: Direct browser control via CDP. Use when the user wants to automate, scrape, test, or interact with web pages. Connects to the user's already-running Chrome. Default to browser_snapshot for understanding pages and browser_execute_js for surgical reads โ€” browser_screenshot is for visual verification only.
4
4
  ---
5
5
 
6
6
  # pi-browser-harness
@@ -21,16 +21,39 @@ What do you need to know?
21
21
  โ”œโ”€ Network behavior on the current page?
22
22
  โ”‚ โ†’ browser_network_requests
23
23
  โ”‚
24
+ โ”œโ”€ JS errors / why did nothing happen after an action?
25
+ โ”‚ โ†’ browser_console (DIAGNOSTIC โ€” only when something looks broken)
26
+ โ”‚
24
27
  โ””โ”€ Visual rendering (layout / colors / chart drew correctly)?
25
28
  โ†’ browser_screenshot (LAST RESORT โ€” pixels only)
26
29
  ```
27
30
 
28
31
  Pass `@(x,y)` from `browser_snapshot` straight to `browser_click`. No screenshot round-trip.
29
32
 
33
+ ## Connection Setup
34
+
35
+ Browser control is **on-demand** โ€” the daemon does NOT start automatically.
36
+ If you try a browser tool and get a `not_connected` error, tell the user to
37
+ run `/browser-setup` first. This opens the daemon and connects to Chrome.
38
+ Once initialized, all subsequent sessions reuse the same connection silently.
39
+
40
+ **Before calling any browser tool**, the runtime checks for the daemon socket
41
+ at `/tmp/pi-browser-daemon.sock`. If the socket is missing (user hasn't run
42
+ `/browser-setup`), you get: `"Browser harness not initialized. Run /browser-setup first"`.
43
+
44
+ **Do not ask the user.** Call `browser_setup` directly โ€” it spawns the daemon,
45
+ connects to Chrome, and opens a test tab. The user sees a single "Allow Remote
46
+ Debugging" prompt the first time. After that, all sessions reuse the same connection.
47
+ `browser_setup` is idempotent โ€” safe to call even when already connected.
48
+
30
49
  ## Connection
31
50
 
32
51
  You're attached to the user's real Chrome โ€” never launch your own. If auth is required, stop and ask the user. If `browser_page_info` returns a dialog, handle it first with `browser_handle_dialog`.
33
52
 
53
+ ## Diagnosing a "nothing happened" moment
54
+
55
+ When an action runs but the page didn't change, capture `browser_console`'s `nextCursor` *before* the action, take the action, then call `browser_console({ sinceSeq: <cursor> })` after โ€” this isolates what your action caused from what was already there. Pair with `browser_network_requests({ sinceMs: 5000 })` to see if an API call fired and failed. The console buffer is page-scoped: it clears on tab switch, capacity 500.
56
+
34
57
  ## Temporary scripts
35
58
 
36
59
  When a workflow repeats 3+ times or needs Node.js APIs, write a script to disk and run it with `browser_run_script`. Scripts get a `daemon` binding for direct CDP access โ€” much faster than chaining tool calls.