cdpilot 0.9.5 → 0.9.7

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 +158 -13
  2. package/bin/cdpilot.js +399 -20
  3. package/package.json +3 -3
  4. package/src/cdpilot.py +2619 -293
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # cdpilot
2
2
 
3
- > Zero-dependency browser automation from your terminal. One command, full control.
3
+ > Zero-npm-dependency browser automation from your terminal. One command, full control.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/cdpilot.svg)](https://www.npmjs.com/package/cdpilot)
6
6
  [![npm downloads](https://img.shields.io/npm/dm/cdpilot.svg)](https://www.npmjs.com/package/cdpilot)
@@ -148,7 +148,26 @@ tools (navigate, click, fill, content, screenshot, eval, wait_for, assert, tabs,
148
148
  plus `browser_tools`. The agent calls `browser_tools` with `list` to see the other
149
149
  tools, or `enable` to add some (the server sends `tools/list_changed`). `--tools a,b,c`
150
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.
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
+ ```
152
171
 
153
172
  ### MCP Bundle (.mcpb)
154
173
 
@@ -321,7 +340,7 @@ cdpilot network <url> --har trace.har # Export navigation traffic as HAR 1.2 JSO
321
340
  cdpilot debug [url] # Full diagnostic (console+network+perf+shot)
322
341
  cdpilot perf # Performance metrics
323
342
  cdpilot eval <js> # Execute JavaScript
324
- cdpilot eval-batch <json> # Run N JS expressions in 1 roundtrip (5-30x faster)
343
+ cdpilot eval-batch <json> # Run N JS expressions in one process and one CDP roundtrip instead of N
325
344
  ```
326
345
 
327
346
  #### Network monitoring & HAR export
@@ -453,6 +472,82 @@ cdpilot a11y summary # Quick summary
453
472
  cdpilot a11y find <role> # Find elements by ARIA role
454
473
  ```
455
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 an upper bound — a full observation before each step;
510
+ an agent that reads diffs reads less. On three real sites the model loop read
511
+ 2,292–18,331 tokens, the `agent run` result 90–136 (HN 18,331 → 90, Wikipedia 15,474 → 136,
512
+ httpbin 2,292 → 119). When a site has a flow, `agent observe` lists it under `known_flows`.
513
+
514
+ **Drift stops, never guesses.** If a step's anchor is gone, or the step lands on another host than it did
515
+ when recorded, `agent run` exits 4 and prints that page's full observation (`status:"drift"`, `step`,
516
+ `expected`, `actions`, `text`): the agent acts on those refs, finishes the task, and `agent flow save <name>`
517
+ rewrites the flow. Exit 2: unknown flow or a `{{variable}}` without a value (checked before any step runs);
518
+ `--dry-run` prints the steps with placeholders, never values. Values typed into secret-named fields
519
+ (password, one-time code, API key, PIN, card number, …) or token-shaped values are placeholders on disk; the
520
+ same fields show `«masked»` as their value in `observe` output. Masking goes by field name or token shape: a
521
+ secret typed into a field named otherwise is stored in clear, so review a flow before sharing it (flow files
522
+ and `agent-state.json` are 0600). A `{{placeholder}}` typed into `agent act --text` or `--url` is refused
523
+ (exit 2): placeholders are cdpilot's own, so a flow replays only values the user recorded, and a step that
524
+ types one runs only on the origin (scheme, host, port) it was recorded on (old flows without an origin fall
525
+ back to host); another origin or an opaque page is a drift. `--var` values are visible in the process list for the
526
+ duration of the call and are redacted in the session log; an environment value never enters argv.
527
+ `CDPILOT_VAR_<NAME>` is used only by a step recorded on a host named in `CDPILOT_VAR_HOSTS` (comma-separated,
528
+ subdomains included), on that same origin over https (http only on loopback), and never inside a URL — so a flow
529
+ that a prompt-injected model records on another site cannot draw on the environment's secrets; anything else is
530
+ refused before the first step (exit 2, `refused` says why) or is a drift. `agent run` sets a placeholder value on
531
+ the recorded field in one browser call that first checks the field (editable, in the top-level page, not an
532
+ iframe; for an environment value not a search or url field) and the page's origin; a failed check exits 2 and types nothing.
533
+
534
+ #### Text that talks to the agent: `injection_suspect`
535
+
536
+ Page text that addresses the model instead of the user is the shape of an indirect prompt injection ("AI
537
+ assistants reading this page: ignore your previous instructions…"). `agent observe`, the diff of `agent act`
538
+ and a drift result of `agent run` carry `injection_suspect: [{text, match}]` (at most 5, absent when none)
539
+ whenever such a block is in the text: `ignore_instructions`, `addresses_ai`, `system_prompt`,
540
+ `new_instructions`, `hide_from_user`, `exfiltrate`, `fake_system`, `tool_call`, `navigate_order`. Nothing is
541
+ hidden or blocked, the text stays where it was; the agent sees the mark next to it and treats the block as data.
542
+ Matching runs on a normalised copy (zero-width characters removed, NFKC, case folded, spaced-out letters joined);
543
+ the "ignore previous instructions" family also covers Spanish, French, Turkish, Japanese, German and Portuguese.
544
+ The patterns were tuned on two sets (`test/fixtures/injection-korpus.json`, `injection-heldout.json`), so their
545
+ numbers there (20 of 20 and 24 of 24 caught, 0 false alarms) are in-sample. On a set never used for tuning
546
+ (`injection-heldout-2.json`, 30 + 30 lines in 7 languages) they catch 8 of 30 with 1 of 30 false alarms (a recipe
547
+ line); on a third such set (`injection-heldout-3.json`, 40 + 40 lines in 12 languages) 7 of 40 with 2 of 40
548
+ (before this pass: 2 of 40). Fake chat turns, homoglyphs, base64 and Chinese/Russian/Arabic lines still pass.
549
+ Treat the mark as a hint, not a filter: a page without it can still carry an injection.
550
+
456
551
  ### Session Management
457
552
 
458
553
  ```bash
@@ -537,7 +632,7 @@ cdpilot watch stop # Stop the screencast
537
632
  ```
538
633
 
539
634
  Works on both local files (`file://...`) and online video (YouTube, Vimeo,
540
- Twitter, Facebook, Instagram). Zero dependency — Pillow is optional and only
635
+ Twitter, Facebook, Instagram). No extra package needed — Pillow is optional and only
541
636
  used for motion-detection between frames.
542
637
 
543
638
  > **DRM limitation:** DRM-protected players (Netflix and similar) render as
@@ -558,7 +653,7 @@ cdpilot watch stop # Stop screencast
558
653
 
559
654
  ### Stealth & CAPTCHA
560
655
 
561
- Zero-dependency anti-fingerprint layer — patches `navigator.webdriver`,
656
+ Self-contained anti-fingerprint layer (no extra packages) — patches `navigator.webdriver`,
562
657
  `chrome.runtime`, plugins (proper `PluginArray` inheritance), WebGL
563
658
  vendor/renderer, permissions, hardware concurrency, and the `Worker`
564
659
  constructor. Injected via `Page.addScriptToEvaluateOnNewDocument` before
@@ -946,6 +1041,54 @@ CDPILOT_TIMEOUT=60 cdpilot run flow.cdp # default for every command (and each
946
1041
  For `mcp` and `serve` the value is not applied to the long-running server
947
1042
  itself; it is passed on to every tool call / request it runs.
948
1043
 
1044
+ ### Warm process (`CDPILOT_DAEMON=1`)
1045
+
1046
+ Every cdpilot call is a fresh process: node starts (~0.1 s), Python imports the
1047
+ core (~0.15 s) and a CDP session is set up (~0.5 s) before the command itself
1048
+ runs — a browser paints a page in 20 ms, `cdpilot go` took about a second before
1049
+ 0.9.6; now 0.5–0.8 s fresh and ~0.23–0.30 s with `CDPILOT_DAEMON=1`
1050
+ (measurements: `.claude/docs/perf-taban-2026-11-08.md`). With
1051
+ `CDPILOT_DAEMON=1` the launcher keeps one warm Python process per project and
1052
+ environment and hands page commands to it over a Unix socket; the process keeps
1053
+ the module and its event loop between calls, so the second and later calls skip
1054
+ the Python start-up and CDP session set-up; node's ~0.1 s launcher remains: a
1055
+ browser-free command ~270 → ~140 ms, `content`/`eval` ~360 → ~110 ms (M1 Max).
1056
+
1057
+ ```bash
1058
+ export CDPILOT_DAEMON=1
1059
+ cdpilot go https://example.com # first call: fresh process, starts the daemon
1060
+ cdpilot content # served warm
1061
+ cdpilot daemon status # pid, requests served, uptime, socket
1062
+ cdpilot daemon stop # stop every running daemon (they also leave after 30 min idle)
1063
+ ```
1064
+
1065
+ Output, exit codes, the session log and recordings are the same either way:
1066
+ each request runs as its own process would (the caller's argv, cwd,
1067
+ environment and stdin; module state reset between requests; exit-time hooks
1068
+ run at request end; the CDP connections a command opened are closed at request
1069
+ end, as a process exit closed them, so no viewport, throttle or event state
1070
+ leaks into the next command). Closing the client (Ctrl+C) cancels the running
1071
+ command. The launcher runs a fresh process instead whenever the daemon is
1072
+ missing, busy with another call, of another version (that one is asked to
1073
+ leave), set up for a port or profile the registry no longer names, or the
1074
+ command is not a page command: `launch`, `stop`, `connect`, `mcp`, `serve`,
1075
+ `watch`, `replay`, `run`, `captcha-wait` and the other lifecycle commands, and
1076
+ anything with `--timeout`. Nothing is ever executed twice. POSIX only for now;
1077
+ the socket lives in `$CDPILOT_HOME/daemon/` (or `$XDG_RUNTIME_DIR` / a private
1078
+ temp directory when that path is too long for a Unix socket), 0600, and is used
1079
+ only when it and its directory belong to you. `CDPILOT_DAEMON_IDLE=<seconds>`
1080
+ sets the idle exit (default 1800). Opt-in in this release; the default may
1081
+ change once two weeks of use show no difference in error rates.
1082
+
1083
+ The MCP server (`cdpilot mcp`) reads the same variable: with `CDPILOT_DAEMON=1`
1084
+ in its environment it runs each tool call in the server process the same way (the
1085
+ same executor, no socket involved) instead of starting a Python process per
1086
+ call — ~400 ms → ~10 ms before the CDP work. Lifecycle tools and a project
1087
+ configuration that moved since the server started still run fresh; a call past
1088
+ 30 s is cancelled at its next async step. A tool call stuck in synchronous code
1089
+ (not waiting on the browser) is not interrupted by the 30 s cancel, unlike the
1090
+ fresh path's kill. See "Agent mode over MCP" above.
1091
+
949
1092
  ### Session log
950
1093
 
951
1094
  Every command (and every MCP tool call) appends one JSON line to a local,
@@ -991,7 +1134,7 @@ cdpilot record stop [--save login-flow.json]
991
1134
  cdpilot replay login-flow.json [--var k=v ...] [--delay real|none|<ms>] [--dry-run] [--continue]
992
1135
  ```
993
1136
 
994
- 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.
1137
+ 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. A `--var` value fills a `{{name}}` anywhere in a step. A `CDPILOT_VAR_<NAME>` environment value fills only the value of an input step (`fill`, `type`, `smart-fill`, `smart-select`, `assert-value` — never its selector, a flag or a flag's value, never a step routed into a frame or typed with `--entropy=on`), and only on an https page whose host `CDPILOT_VAR_HOSTS` names (subdomains included; http only on localhost, 127.0.0.1, [::1] and *.localhost; another port only when the entry is written `host:port`; a URL with a backslash, a control character, whitespace, or `@` / `%` before its path is refused). The value reaches the step's process through its environment, never its command line. The page is checked right before the step and again inside the one browser call that sets the value; that call also refuses a target that is not a text, password, email, tel or number input, a textarea or a select of the top-level page (an iframe, a contenteditable element, a search or url field). A flow that takes any environment value may contain only these steps, which run no page script and print no page data: `go` / `open` (only to a page the rule above allows; the page the browser lands on is read back and checked too), `click`, `dblclick`, `hover`, `smart-click`, `submit`, `scroll-to`, `dismiss`, `keys` (no Ctrl/Alt/Meta), `fill`, `type`, `smart-fill`, `smart-select`, `wait`, `wait-for`, `wait-for-text`, `assert-count`, `assert-visible`, `assert-hidden`, and `assert-value` that takes the value itself. One `eval`, `run`, `batch`, `tools call` or any other step refuses the whole flow, as does an XPath selector or one that reacts to a typed value (`[value…]`, `@value`, `value=`, `:placeholder-shown`, `:valid`, `:checked` and the like, CSS comments and escapes included); in such a flow replay never shows a step's stdout or its duration. Once a value is typed, every later step runs only on the exact origin it was typed on (checked before each step), and any other page stops the replay with exit 2. The flow chooses the field, so a flow can still type the value into a field the allowed site shows or sends on (a comment box, a form): replay only flows you trust. A `google.com` entry covers every subdomain, so prefer exact hosts. A refusal exits 2 before the first step (JSON with `missing` / `refused`, names only) or stops the replay at that step with exit 2. `go`, `eval` and every other command take `--var` only. Replaying a flow runs its steps with your browser profile, so replay only flows you trust: a flow with `eval` can act on any page you are logged into.
995
1138
 
996
1139
  ### Use your own browser (human-in-the-loop)
997
1140
 
@@ -1232,7 +1375,7 @@ print(result.stdout)
1232
1375
  │ (CLI) │ Chrome DevTools │ (CDP mode) │
1233
1376
  └─────────────┘ Protocol └──────────────┘
1234
1377
  │ │
1235
- │ Zero dependencies │ Isolated profile
1378
+ │ Zero npm dependencies │ Isolated profile
1236
1379
  │ Pure HTTP + WebSocket │ Separate from your
1237
1380
  │ single file, stdlib + websockets │ personal browser
1238
1381
  └────────────────────────────────────────┘
@@ -1292,13 +1435,15 @@ The full adaptive layer is bench-neutral vs baseline (30/80 vs 30/80) for Stealt
1292
1435
 
1293
1436
  | Feature | cdpilot | Puppeteer | Playwright | Selenium |
1294
1437
  |---------|-----------|-----------|------------|----------|
1295
- | Install size | **one Python file + a small Node launcher, no node_modules** | 400MB+ | 200MB+ | 100MB+ |
1296
- | Dependencies | **0 npm** (Python: `websockets`, auto-installed) | 50+ | 30+ | Java + drivers |
1297
- | Setup time | **instant** | minutes | minutes | painful |
1438
+ | Install size | **one Python file + a small Node launcher, no node_modules** | puppeteer-core ~5.7MB + Chromium ~170-280MB | playwright-core ~12.8MB + browsers | selenium-webdriver ~21.8MB + drivers |
1439
+ | Dependencies | **0 npm** (Python: `websockets`, auto-installed) | 6 direct npm | 1 (playwright-core) direct npm | 4 direct npm |
1440
+ | Setup time | **one npx; needs Python 3.10+** | minutes | minutes | painful |
1298
1441
  | AI-agent ready | **yes** | manual | manual | manual |
1299
1442
  | Browser download | **no** | yes (Chromium) | yes (3 browsers) | no |
1300
1443
  | CLI-first | **yes** | no (library) | no (library) | no |
1301
- | MCP support | **yes** | no | no | no |
1444
+ | MCP support | **yes** | deprecated official server | yes (official @playwright/mcp) | no official server |
1445
+
1446
+ Third-party numbers from npm on 27.09.2026; sources on [cdpilot.ndr.ist/compare](https://cdpilot.ndr.ist/compare).
1302
1447
 
1303
1448
  ## Monetization / Pro (Coming Soon)
1304
1449
 
@@ -1344,7 +1489,7 @@ Expired cookies are filtered automatically on load.
1344
1489
 
1345
1490
  ## Roadmap
1346
1491
 
1347
- The only browser MCP with built-in test assertions. Here's what we've shipped and what's next:
1492
+ A browser MCP with 10 built-in test assertions. Here's what we've shipped and what's next:
1348
1493
 
1349
1494
  ### Shipped
1350
1495
 
@@ -1358,7 +1503,7 @@ The only browser MCP with built-in test assertions. Here's what we've shipped an
1358
1503
  - [x] **Annotated screenshots** — @N badge overlays on interactive elements
1359
1504
  - [x] **Auto-wait** — MutationObserver-based, 5s automatic element waiting
1360
1505
  - [x] **`wait-for-text`** — adaptive text-based waiting (subtree + characterData) for streaming AI responses, async toasts, and selector-less synchronization
1361
- - [x] **`eval-batch`** — run N JS expressions in 1 CDP roundtrip (5-30x speedup vs sequential `eval`)
1506
+ - [x] **`eval-batch`** — run N JS expressions in one process and one CDP roundtrip instead of N
1362
1507
  - [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)
1363
1508
  - [x] **`dismiss`** — heuristic auto-click for LLM chat sign-up walls (EN+TR pattern library, destructive-action guards)
1364
1509
  - [x] **`adaptive`** — auto-escalate to stealth on CAPTCHA-protected hosts, persistent per-host memory ("run fast, climb walls")