cdpilot 0.9.4 → 0.9.6

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.
Files changed (4) hide show
  1. package/README.md +188 -17
  2. package/bin/cdpilot.js +376 -18
  3. package/package.json +1 -1
  4. package/src/cdpilot.py +3659 -294
package/README.md CHANGED
@@ -28,14 +28,14 @@ No config files. No boilerplate. Just `npx` and go.
28
28
  AI agents and developers need browser control that **just works**:
29
29
 
30
30
  - **Zero config** — `npx cdpilot launch` starts an isolated browser session
31
- - **Zero dependency** — No Puppeteer, no Playwright, no Selenium. Pure CDP over HTTP
31
+ - **Zero npm dependencies** — No Puppeteer, no Playwright, no Selenium. Pure CDP over HTTP/WebSocket; the Python side needs one package, `websockets` (the setup wizard installs it)
32
32
  - **70+ commands** — Navigate, click, type, screenshot, network, console, accessibility, video understanding, progressive anti-bot resilience, and more
33
33
  - **AI-agent friendly** — Designed for Claude, GPT, Gemini, and any LLM tool-use workflow
34
34
  - **Isolated sessions** — Your personal browser stays untouched. cdpilot runs in its own profile
35
35
  - **Visual feedback** — Green glow overlay, cursor visualization, click ripples, and keystroke display keep you informed during automation
36
36
  - **Multi-project isolation** — Each project gets its own browser instance and port automatically, no conflicts
37
37
  - **AI control warning** — Red toast notification appears when you hover during active automation
38
- - **Privacy-first** — Everything runs locally. No data leaves your machine
38
+ - **Privacy-first** — Everything runs locally; nothing leaves your machine unless you opt in (CAPTCHA solver keys, `tls-check`, proxies)
39
39
 
40
40
  ### Browser Selection (Workload-Aware Auto-Pick)
41
41
 
@@ -141,6 +141,34 @@ that teaches Claude Code when and how to reach for cdpilot's commands. See
141
141
  [`.claude-plugin/marketplace.json`](.claude-plugin/marketplace.json) and
142
142
  [`plugins/cdpilot/`](plugins/cdpilot/).
143
143
 
144
+ ### Smaller MCP tool list
145
+
146
+ `cdpilot mcp --tools core` (or `CDPILOT_MCP_TOOLS=core`; the flag wins) lists 10 core
147
+ tools (navigate, click, fill, content, screenshot, eval, wait_for, assert, tabs, connect)
148
+ plus `browser_tools`. The agent calls `browser_tools` with `list` to see the other
149
+ tools, or `enable` to add some (the server sends `tools/list_changed`). `--tools a,b,c`
150
+ picks tools by name; `all` (default) is unchanged. Schema size of `tools/list`
151
+ (tiktoken `o200k_base`): 45 tools = 5,769 tokens, `core` = 11 tools = 1,342 tokens; the four
152
+ `browser_agent_*` tools add ~11% of schema (49 tools ≈ 6,400 tokens, scaled from the measurement).
153
+
154
+ ### Agent mode over MCP
155
+
156
+ `browser_agent_observe` (`diff`), `browser_agent_act` (`ref`/`url`, `action`, `text`), `browser_agent_flow_save`
157
+ (`name`, `last`) and `browser_agent_run` (`flow`, `vars`, `dry_run`) wrap the agent commands below, so a model
158
+ on MCP works from numbered refs, gets diffs, and replays a learned flow with no page read at all. They are
159
+ outside the `core` profile; `browser_tools enable` adds them. A drift or a missing variable comes back as
160
+ `isError` with the command's JSON (`status:"drift"` carries the page's observation).
161
+
162
+ Set `CDPILOT_DAEMON=1` in the MCP server's environment and every tool call runs in the server process
163
+ instead of a fresh Python process: ~10 ms before the CDP work instead of ~400 ms (`browser_eval` /
164
+ `browser_tabs` / `browser_content` medians 11 / 7 / 9 ms against 412 / 390 / 416 ms, M1 Max under load).
165
+ Same rules as the warm process below — same output, exit codes and session log; lifecycle tools
166
+ (`browser_launch`, `browser_connect`) still run fresh; a call past 30 s is cancelled.
167
+
168
+ ```json
169
+ { "mcpServers": { "cdpilot": { "command": "npx", "args": ["cdpilot", "mcp"], "env": { "CDPILOT_DAEMON": "1" } } } }
170
+ ```
171
+
144
172
  ### MCP Bundle (.mcpb)
145
173
 
146
174
  For MCP clients that support one-click local server installation (Claude
@@ -248,11 +276,19 @@ cdpilot frame eval --frame "#card" "document.title" # runs in the frame's
248
276
  inside it and says so on stderr:
249
277
  `note: '#card-element' is not an iframe; using the iframe inside it`.
250
278
  - Selectors and texts that already contain `>>>` keep working: when the part
251
- before the first `>>>` finds no iframe in the page, or a segment is empty
279
+ before the first `>>>` finds no iframe or shadow host in the page, or a segment is empty
252
280
  (`click "Next >>>"`), the whole string is used as written, exactly as
253
281
  before. If that fails too, one stderr line explains:
254
- `note: 'Home' matched no iframe; used the selector as written`.
282
+ `note: 'Home' matched no iframe or shadow host; used the selector as written`.
255
283
  `--frame` never falls back: a frame it cannot find is an error (exit 1).
284
+ - **Shadow DOM support**: `>>>` hops also pierce open shadow roots (`host.shadowRoot.querySelector(next)`).
285
+ Chains mix freely with iframes: `cdpilot click "iframe#pay >>> my-card >>> button.inner"` or
286
+ `cdpilot click "my-app >>> my-panel >>> input"`. A closed shadow root is unreachable by design
287
+ and fails with `'X' has a closed shadow root; cdpilot cannot reach inside it`.
288
+ When a plain CSS selector matches nothing at document level, cdpilot automatically falls back
289
+ to a depth-first search across all open shadow roots, printing one stderr note:
290
+ `note: '<sel>' found inside the shadow root of <host tag#id>`. Explicit `>>>` hops do not print this note.
291
+ `wait` and `wait-for` poll shadow roots every 200 ms alongside the document MutationObserver.
256
292
  - For `smart-click` / `smart-fill` / `smart-select` the part before the first
257
293
  `>>>` is a frame only if it looks like a selector (it has `#`, `.` or `[`,
258
294
  or starts with an `iframe`/`frame` tag) or is `url=…`; words stay text.
@@ -298,14 +334,22 @@ cdpilot frame eval --frame "#card" "document.title" # runs in the frame's
298
334
  ### Debugging
299
335
 
300
336
  ```bash
301
- cdpilot console [url] # Capture console logs
302
- cdpilot network [url] # Monitor network requests
303
- cdpilot debug [url] # Full diagnostic (console+network+perf+shot)
304
- cdpilot perf # Performance metrics
305
- cdpilot eval <js> # Execute JavaScript
306
- cdpilot eval-batch <json> # Run N JS expressions in 1 roundtrip (5-30x faster)
337
+ cdpilot console [url] # Capture console logs
338
+ cdpilot network [url] # Monitor network requests
339
+ cdpilot network <url> --har trace.har # Export navigation traffic as HAR 1.2 JSON (--har - for stdout)
340
+ cdpilot debug [url] # Full diagnostic (console+network+perf+shot)
341
+ cdpilot perf # Performance metrics
342
+ cdpilot eval <js> # Execute JavaScript
343
+ cdpilot eval-batch <json> # Run N JS expressions in 1 roundtrip (5-30x faster)
307
344
  ```
308
345
 
346
+ #### Network monitoring & HAR export
347
+
348
+ `cdpilot network <url> --har <file>` exports all HTTP requests of a navigation as standard HAR 1.2 JSON (`--har -` writes to stdout).
349
+ - **Included:** URLs, methods, headers, status codes, query strings, and CDP timing breakdowns (DNS, connect, SSL, send, wait, receive).
350
+ - **Omitted:** Response bodies are not fetched or stored (avoids memory bloat and round-trip overhead); content.size reflects transfer length; bodySize is -1.
351
+ - **Masking:** Sensitive header values matching security patterns (`Cookie`, `Set-Cookie`, `Authorization`, `X-Api-Key`, token, secret) are replaced with `"«masked»"`. URLs are masked the same way as the session log: userinfo passwords and query/fragment parameters named like secrets (`token`, `key`, `code`, `session`, …) in `request.url`, `queryString`, `redirectURL` and URL-bearing header values (`Location`, `Referer`, `Link`); a `data:` URL keeps only its media type.
352
+
309
353
  ```bash
310
354
  # Example: read 4 DOM values in a single CDP roundtrip instead of 4
311
355
  cdpilot eval-batch '["document.title","location.href","document.links.length","document.images.length"]'
@@ -428,6 +472,70 @@ cdpilot a11y summary # Quick summary
428
472
  cdpilot a11y find <role> # Find elements by ARIA role
429
473
  ```
430
474
 
475
+ ### Agent mode: numbered refs and diffs (token budget)
476
+
477
+ ```bash
478
+ cdpilot agent observe # {url, title, actions:[{ref:"@1", role, name, value?}], text, token_estimate}
479
+ cdpilot agent act --ref @3 --action click
480
+ cdpilot agent act --ref @4 --action fill --text "cdpilot"
481
+ cdpilot agent act --url https://example.com/ # navigates silently, prints the diff
482
+ cdpilot agent observe --diff # only what changed since the last observe/act
483
+ cdpilot agent stats | agent reset
484
+ ```
485
+
486
+ `observe` turns the accessibility tree into a numbered list of things an agent can act on (buttons, links,
487
+ fields with their current value, unique text anchors) plus a short text summary; no screenshot, no HTML.
488
+ Every `act` and `observe --diff` returns only the change: `changed.added` (full descriptors, so the agent can act
489
+ on a new element without another observe), `changed.removed` (refs), `changed.value_changed`, and `text` only
490
+ when it changed (`text_changed`). Refs are stable while the element exists (keyed by the DOM node, not by
491
+ position); a navigation starts a new set. `token_estimate` is `len(json)/4`; `saved_vs_full` compares the diff
492
+ with a full observe of the same page. State lives in `~/.cdpilot/projects/<id>/agent-state.json`.
493
+
494
+ #### Learned flows: drive a site once, replay it with no model
495
+
496
+ ```bash
497
+ cdpilot agent observe && cdpilot agent act --ref @3 --action click && cdpilot agent act --ref @4 --action fill --text "cdpilot"
498
+ cdpilot agent flow save inbox-triage # the acts since the last navigation → ~/.cdpilot/projects/<id>/flows/inbox_triage.json
499
+ cdpilot agent run inbox-triage # replays by anchors; prints one JSON: {status:"ok", steps, url, title, ms, tokens_avoided}
500
+ cdpilot agent run login --var password=… # a typed secret was saved as {{password}}; --var supplies it, or CDPILOT_VAR_PASSWORD
501
+ # with CDPILOT_VAR_HOSTS=example.com (the host the value was recorded on)
502
+ cdpilot agent flow list | show <name> | delete <name>
503
+ ```
504
+
505
+ Every `agent act` leaves a trail entry addressed by an *anchor*, the element's accessibility role, name and
506
+ position among equals (`{"role":"button","name":"Archive","nth":0}`), never a `@ref` or a DOM node id, which
507
+ belong to one session. `agent flow save` writes the trail as a flow; `agent run` finds each anchor on the live
508
+ page (exact name, then case-insensitive, then a longer name containing it when that is unambiguous) and acts,
509
+ with no model in the loop. The result's `tokens_avoided` is what a model would have read observing the page
510
+ before each step. When a site has a flow, `agent observe` lists it under `known_flows`.
511
+
512
+ **Drift stops, never guesses.** If a step's anchor is gone, or the step lands on another host than it did
513
+ when recorded, `agent run` exits 4 and prints that page's full observation (`status:"drift"`, `step`,
514
+ `expected`, `actions`, `text`): the agent acts on those refs, finishes the task, and `agent flow save <name>`
515
+ rewrites the flow. Exit 2: unknown flow or a `{{variable}}` without a value (checked before any step runs);
516
+ `--dry-run` prints the steps with placeholders, never values. Values typed into secret-named fields
517
+ (password, one-time code, API key, PIN, card number, …) or token-shaped values are placeholders on disk; the
518
+ same fields show `«masked»` as their value in `observe` output. Masking goes by field name or token shape: a
519
+ secret typed into a field named otherwise is stored in clear, so review a flow before sharing it (flow files
520
+ and `agent-state.json` are 0600). A `{{placeholder}}` typed into `agent act --text` or `--url` is refused
521
+ (exit 2): placeholders are cdpilot's own, so a flow replays only values the user recorded, and a step that
522
+ types one runs only on the host it was recorded on. `--var` values are visible in the process list for the
523
+ duration of the call and are redacted in the session log; an environment value never enters argv.
524
+ `CDPILOT_VAR_<NAME>` is used only by a step recorded on a host named in `CDPILOT_VAR_HOSTS` (comma-separated,
525
+ subdomains included), on that same origin, and never inside a URL — so a flow that a prompt-injected model
526
+ records on another site cannot draw on the environment's secrets; anything else is refused before the first
527
+ step (exit 2, `refused` says why) or is a drift.
528
+
529
+ #### Text that talks to the agent: `injection_suspect`
530
+
531
+ Page text that addresses the model instead of the user is the shape of an indirect prompt injection ("AI
532
+ assistants reading this page: ignore your previous instructions…"). `agent observe`, the diff of `agent act`
533
+ and a drift result of `agent run` carry `injection_suspect: [{text, match}]` (at most 5, absent when none)
534
+ whenever such a block is in the text: `ignore_instructions`, `addresses_ai`, `system_prompt`,
535
+ `new_instructions`, `hide_from_user`, `exfiltrate`. Nothing is hidden or blocked, the text stays where it was;
536
+ the agent sees the mark next to it and treats the block as data. The patterns are narrow on purpose: a page
537
+ *about* AI agents or system prompts is not flagged.
538
+
431
539
  ### Session Management
432
540
 
433
541
  ```bash
@@ -709,7 +817,7 @@ is dropped, never signalled.
709
817
 
710
818
  **Idle close** still works while the signer runs: the signer is attached to every page, so instead
711
819
  of "a page is attached" cdpilot counts the CDP clients other than the signer (by socket owner:
712
- `netstat` on macOS and Windows, `ss` on Linux), and a `watch` daemon or a Playwright session keeps
820
+ `netstat` with an `lsof` fallback on macOS, `netstat` on Windows, `ss` on Linux), and a `watch` daemon or a Playwright session keeps
713
821
  the browser open. On Linux, `ss -tnp` shows only your own processes' sockets, so a client running
714
822
  as another user is not counted and idle close may close the browser under it.
715
823
 
@@ -921,6 +1029,50 @@ CDPILOT_TIMEOUT=60 cdpilot run flow.cdp # default for every command (and each
921
1029
  For `mcp` and `serve` the value is not applied to the long-running server
922
1030
  itself; it is passed on to every tool call / request it runs.
923
1031
 
1032
+ ### Warm process (`CDPILOT_DAEMON=1`)
1033
+
1034
+ Every cdpilot call is a fresh process: node starts (~0.1 s), Python imports the
1035
+ core (~0.15 s) and a CDP session is set up (~0.5 s) before the command itself
1036
+ runs — a browser paints a page in 20 ms, `cdpilot go` took about a second
1037
+ (measurements: `.claude/docs/perf-taban-2026-11-08.md`). With
1038
+ `CDPILOT_DAEMON=1` the launcher keeps one warm Python process per project and
1039
+ environment and hands page commands to it over a Unix socket; the process keeps
1040
+ the module and its event loop between calls, so the second and later calls skip
1041
+ the start-up cost.
1042
+
1043
+ ```bash
1044
+ export CDPILOT_DAEMON=1
1045
+ cdpilot go https://example.com # first call: fresh process, starts the daemon
1046
+ cdpilot content # served warm
1047
+ cdpilot daemon status # pid, requests served, uptime, socket
1048
+ cdpilot daemon stop # stop every running daemon (they also leave after 30 min idle)
1049
+ ```
1050
+
1051
+ Output, exit codes, the session log and recordings are the same either way:
1052
+ each request runs as its own process would (the caller's argv, cwd,
1053
+ environment and stdin; module state reset between requests; exit-time hooks
1054
+ run at request end; the CDP connections a command opened are closed at request
1055
+ end, as a process exit closed them, so no viewport, throttle or event state
1056
+ leaks into the next command). Closing the client (Ctrl+C) cancels the running
1057
+ command. The launcher runs a fresh process instead whenever the daemon is
1058
+ missing, busy with another call, of another version (that one is asked to
1059
+ leave), set up for a port or profile the registry no longer names, or the
1060
+ command is not a page command: `launch`, `stop`, `connect`, `mcp`, `serve`,
1061
+ `watch`, `replay`, `run`, `captcha-wait` and the other lifecycle commands, and
1062
+ anything with `--timeout`. Nothing is ever executed twice. POSIX only for now;
1063
+ the socket lives in `$CDPILOT_HOME/daemon/` (or `$XDG_RUNTIME_DIR` / a private
1064
+ temp directory when that path is too long for a Unix socket), 0600, and is used
1065
+ only when it and its directory belong to you. `CDPILOT_DAEMON_IDLE=<seconds>`
1066
+ sets the idle exit (default 1800). Opt-in in this release; the default may
1067
+ change once two weeks of use show no difference in error rates.
1068
+
1069
+ The MCP server (`cdpilot mcp`) reads the same variable: with `CDPILOT_DAEMON=1`
1070
+ in its environment it runs each tool call in the server process the same way (the
1071
+ same executor, no socket involved) instead of starting a Python process per
1072
+ call — ~400 ms → ~10 ms before the CDP work. Lifecycle tools and a project
1073
+ configuration that moved since the server started still run fresh; a call past
1074
+ 30 s is cancelled at its next async step. See "Agent mode over MCP" above.
1075
+
924
1076
  ### Session log
925
1077
 
926
1078
  Every command (and every MCP tool call) appends one JSON line to a local,
@@ -951,6 +1103,23 @@ never changes a command's output or exit code, and a failed write costs one
951
1103
  stderr line. `CDPILOT_LOG=0` turns it off; `CDPILOT_LOG_DAYS` (default 14)
952
1104
  sets how many days are kept. The MCP server exposes it as `browser_log`.
953
1105
 
1106
+ ### Record & replay
1107
+
1108
+ Record sequences of CLI commands into a portable flow JSON file and replay them with variable substitution:
1109
+
1110
+ ```bash
1111
+ cdpilot record start [--name <n>]
1112
+ cdpilot record status
1113
+ cdpilot record cancel
1114
+ cdpilot go https://example.com
1115
+ cdpilot click "#login"
1116
+ cdpilot fill "#email" test@test.com
1117
+ cdpilot record stop [--save login-flow.json]
1118
+ cdpilot replay login-flow.json [--var k=v ...] [--delay real|none|<ms>] [--dry-run] [--continue]
1119
+ ```
1120
+
1121
+ Commands that run while recording is active are appended to `$CDPILOT_HOME/record/active.json`; secrets are replaced by `{{name}}` placeholders before a step is written to active.json. `CDPILOT_LOG=0` does not turn recording off, and steps run by `replay` are not recorded. Failed steps (`exit != 0`) are dropped at `record stop`. A value becomes a placeholder `{{name}}` cataloged under `vars` if the selector/label is secret-shaped (`#password`, `input[type=password]`, API key), or the value matches a token/bearer pattern or secret URL parameter. Other typed values (emails, search terms, anything the rules do not recognise) are stored verbatim, so review a flow before sharing it; `--save` files are created with mode 0600. In `replay`, `--delay real` reproduces recorded inter-step timing (capped at 10 s per gap), while `--delay none` or a fixed millisecond delay skips waiting. Variables are supplied via `--var k=v` or `CDPILOT_VAR_<NAME>`; missing variables halt replay before any step runs.
1122
+
954
1123
  ### Use your own browser (human-in-the-loop)
955
1124
 
956
1125
  An agent hits a CAPTCHA or a login wall it cannot get past. A person solves it,
@@ -1082,7 +1251,9 @@ cdpilot launch --no-webmcp # turn it off again (applies at the next start)
1082
1251
  Chrome reads feature flags only at startup: a browser that is already running
1083
1252
  keeps its flags until `cdpilot stop`.
1084
1253
  - `tools list` shows the tools of the page and of its same-origin iframes
1085
- (with the frame URL), marks form tools and whether the form submits itself
1254
+ (with the frame URL; every readable frame is asked for its own tools, so a
1255
+ frame tool missing from the top document's `getTools()` is still listed and
1256
+ runs in its frame), marks form tools and whether the form submits itself
1086
1257
  (`toolautosubmit`), and shows `title` and the `readOnlyHint` /
1087
1258
  `consequentialHint` / `untrustedContentHint` annotations. Nothing is injected
1088
1259
  into the page: the list is the browser's registry at that moment, so it
@@ -1230,9 +1401,9 @@ print(result.stdout)
1230
1401
  | v0.6.0 | + captcha solver + cookies-auto (regression) | 15 / 80 | 18.75% |
1231
1402
  | **v0.8.0** | **Full — cookies safe-host scoped + per-task wipe (no proxy, no TLS fork)** | **29 / 80** | **36.25%** |
1232
1403
  | v0.7.0 (slot) | + named proxy pools | _depends on user proxy_ | — |
1233
- | v0.8.0 (slot) | + TLS-aware launcher (camoufox/undetected-chrome) | _depends on browser choice_ | — |
1404
+ | v0.8.0 (slot) | TLS-aware launcher — withdrawn: camoufox / undetected-chrome do not speak CDP (CHANGELOG 0.8.0) | — | — |
1234
1405
 
1235
- **What cdpilot does not do:** `cdpilot` is an avoidance engine, not a CAPTCHA solver. We prioritize structural stealth (JS fingerprinting, behavioral entropy) to prevent challenges from appearing. When a CAPTCHA blocks progress and cannot be bypassed, the task fails — that is the honest definition of our success rate. PerimeterX (2/18), GeeTest (0/4) and Akamai (1/6) are known weaknesses; v0.7+ (residential proxy) and v0.8+ (TLS fingerprint correction via camoufox) target these directly.
1406
+ **What cdpilot does not do:** `cdpilot` is an avoidance engine first; the solver plugins above (`cdpilot captcha`, bring your own key, off by default) are a fallback, not the strategy. We prioritize structural stealth (JS fingerprinting, behavioral entropy) to prevent challenges from appearing. When a CAPTCHA blocks progress and cannot be avoided, the task fails — that is the honest definition of our success rate. PerimeterX (2/18), GeeTest (0/4) and Akamai (1/6) are known weaknesses; named proxy pools (v0.7) help where the block is IP-based, while TLS-level fingerprinting cannot be fixed from inside a CDP-driven browser and remains open.
1236
1407
 
1237
1408
  ## Recommended Configuration
1238
1409
 
@@ -1271,7 +1442,7 @@ Future paid offerings:
1271
1442
  - **No arbitrary file access** — MCP screenshot filenames are sanitized and restricted to the screenshots directory. Path traversal is blocked.
1272
1443
  - **Safe CSS selectors** — All selectors passed to `querySelector` are JSON-escaped to prevent injection.
1273
1444
  - **No network exposure** — CDP listens on `127.0.0.1` only. Remote connections are not possible by default.
1274
- - **No dependencies** — Zero npm/Python runtime dependencies means zero supply-chain attack surface.
1445
+ - **One dependency** — zero npm packages; the Python side needs only `websockets`. A one-package supply chain is small enough to audit.
1275
1446
 
1276
1447
  Found a vulnerability? Please email the maintainer directly instead of opening a public issue.
1277
1448
 
@@ -1309,13 +1480,13 @@ The only browser MCP with built-in test assertions. Here's what we've shipped an
1309
1480
  - [x] **10 built-in test assertions** — assert, assert-url, assert-title, assert-count, assert-value, assert-attr, assert-visible/hidden, wait-for, check (batch), screenshot-diff
1310
1481
  - [x] **`test` runner + `trace` viewer** — `cdpilot test [--watch]` runs `*.cdpt.js` files; `cdpilot trace open <run>` opens a time-travel trace viewer for a run
1311
1482
  - [x] **Accessibility tree snapshot** (`a11y-snapshot`) — structured text with @ref handles the agent can act on directly, no vision model needed; 1.4–42× smaller than the page's raw HTML on the four pages we measured (2026-09-27). Not always cheaper than a screenshot: link-heavy pages produce more text than a small screenshot costs
1312
- - [x] **Token-efficient screenshots** — element-level crop (13x smaller), JPEG quality control, format selection
1483
+ - [x] **Token-efficient screenshots** — element-level crop, JPEG quality control, format selection
1313
1484
  - [x] **Vision fallback** (`describe`) — a11y + screenshot + text in one call
1314
1485
  - [x] **Annotated screenshots** — @N badge overlays on interactive elements
1315
1486
  - [x] **Auto-wait** — MutationObserver-based, 5s automatic element waiting
1316
1487
  - [x] **`wait-for-text`** — adaptive text-based waiting (subtree + characterData) for streaming AI responses, async toasts, and selector-less synchronization
1317
1488
  - [x] **`eval-batch`** — run N JS expressions in 1 CDP roundtrip (5-30x speedup vs sequential `eval`)
1318
- - [x] **`block`** — request blocking via `Network.setBlockedURLs` with built-in presets (images/fonts/ads/media), 3-10x faster page loads on opt-in
1489
+ - [x] **`block`** — request blocking via `Network.setBlockedURLs` with built-in presets (images/fonts/ads/media); fewer bytes and faster loads on opt-in (measure on your own pages)
1319
1490
  - [x] **`dismiss`** — heuristic auto-click for LLM chat sign-up walls (EN+TR pattern library, destructive-action guards)
1320
1491
  - [x] **`adaptive`** — auto-escalate to stealth on CAPTCHA-protected hosts, persistent per-host memory ("run fast, climb walls")
1321
1492
  - [x] **`cookies save/load`** — export/import cookies as JSON (replay CF/DataDome clearance across runs)