cdpilot 0.9.6 → 0.9.8

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
@@ -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)
@@ -150,10 +150,11 @@ tools, or `enable` to add some (the server sends `tools/list_changed`). `--tools
150
150
  picks tools by name; `all` (default) is unchanged. Schema size of `tools/list`
151
151
  (tiktoken `o200k_base`): 45 tools = 5,769 tokens, `core` = 11 tools = 1,342 tokens; the four
152
152
  `browser_agent_*` tools add ~11% of schema (49 tools ≈ 6,400 tokens, scaled from the measurement).
153
+ The three `browser_canvas_*` tools are listed only while a live canvas is open (`cdpilot canvas open`), and never under `core`; `tools/list_changed` follows when the first canvas opens or the last one closes.
153
154
 
154
155
  ### Agent mode over MCP
155
156
 
156
- `browser_agent_observe` (`diff`), `browser_agent_act` (`ref`/`url`, `action`, `text`), `browser_agent_flow_save`
157
+ `browser_agent_observe` (`diff`, `full`), `browser_agent_act` (`ref`/`url`, `action`, `text`), `browser_agent_flow_save`
157
158
  (`name`, `last`) and `browser_agent_run` (`flow`, `vars`, `dry_run`) wrap the agent commands below, so a model
158
159
  on MCP works from numbered refs, gets diffs, and replays a learned flow with no page read at all. They are
159
160
  outside the `core` profile; `browser_tools enable` adds them. A drift or a missing variable comes back as
@@ -340,7 +341,7 @@ cdpilot network <url> --har trace.har # Export navigation traffic as HAR 1.2 JSO
340
341
  cdpilot debug [url] # Full diagnostic (console+network+perf+shot)
341
342
  cdpilot perf # Performance metrics
342
343
  cdpilot eval <js> # Execute JavaScript
343
- cdpilot eval-batch <json> # Run N JS expressions in 1 roundtrip (5-30x faster)
344
+ cdpilot eval-batch <json> # Run N JS expressions in one process and one CDP roundtrip instead of N
344
345
  ```
345
346
 
346
347
  #### Network monitoring & HAR export
@@ -472,6 +473,28 @@ cdpilot a11y summary # Quick summary
472
473
  cdpilot a11y find <role> # Find elements by ARIA role
473
474
  ```
474
475
 
476
+ ### Choosing a page view
477
+
478
+ Tokens are o200k_base counts of each command's stdout on Hacker News and the Wikipedia "Web browser" article
479
+ (median of 3 runs, rounded to hundreds, 2026-11-11; pages drift, so expect a few percent).
480
+
481
+ | View | What it gives | Can act on it | HN / Wikipedia |
482
+ |---|---|---|---|
483
+ | `content` | the page's plain text | no | 1,200 / 4,700 |
484
+ | `agent observe` | numbered `@refs` for actionable entries and headings/landmarks, plus a short text | yes (`agent act --ref`) | 4,300 / 8,300 |
485
+ | `agent observe --rows` | the same entries as arrays | yes | 2,900 / 5,200 |
486
+ | `agent observe --full` | every entry, nothing hidden | yes | 6,900 / 17,300 |
487
+ | `a11y-snapshot` | one `@ref` line per interactive or structural node | yes (`click-ref @N`) | 8,100 / 13,100 |
488
+ | `a11y` | the full tree with roles and names, no refs | no | 9,800 / 31,400 |
489
+ | `describe` | `a11y-snapshot` + page text, and it writes a screenshot file | yes (`click-ref @N`) | 8,800 / 13,600 |
490
+
491
+ To act on a page under a token budget, start with `agent observe`; `--rows` prints the same entries in about a
492
+ third fewer tokens (opt-in: `[ref, role, name(, value)]`). After a click or a fill, read the `agent act` result: it
493
+ already holds the change (142–496 tokens on Wikipedia, MDN and react.dev), and `agent observe --diff` right after
494
+ it returned 45–60. Use `a11y-snapshot` or
495
+ `observe --full` when you need a ref for every node, and `content` when you only read. The ranking is
496
+ page-dependent: on react.dev `a11y-snapshot` (1,200) is smaller than `content` (1,500).
497
+
475
498
  ### Agent mode: numbered refs and diffs (token budget)
476
499
 
477
500
  ```bash
@@ -480,9 +503,16 @@ cdpilot agent act --ref @3 --action click
480
503
  cdpilot agent act --ref @4 --action fill --text "cdpilot"
481
504
  cdpilot agent act --url https://example.com/ # navigates silently, prints the diff
482
505
  cdpilot agent observe --diff # only what changed since the last observe/act
506
+ cdpilot agent observe --full # every entry, nothing hidden (also on act and run)
507
+ cdpilot agent observe --rows # entries as [ref, role, name, value?] arrays
483
508
  cdpilot agent stats | agent reset
484
509
  ```
485
510
 
511
+ The agent commands (`agent observe`, `agent act`, `agent run`, `agent flow list|show`) print compact
512
+ UTF-8 JSON when stdout is not a terminal (an agent, a pipe, an MCP tool call) and indented JSON in a
513
+ terminal. `CDPILOT_JSON=pretty|compact` overrides this. Scripts that grep line by line should use `jq`
514
+ or set `CDPILOT_JSON=pretty`.
515
+
486
516
  `observe` turns the accessibility tree into a numbered list of things an agent can act on (buttons, links,
487
517
  fields with their current value, unique text anchors) plus a short text summary; no screenshot, no HTML.
488
518
  Every `act` and `observe --diff` returns only the change: `changed.added` (full descriptors, so the agent can act
@@ -491,6 +521,18 @@ when it changed (`text_changed`). Refs are stable while the element exists (keye
491
521
  position); a navigation starts a new set. `token_estimate` is `len(json)/4`; `saved_vs_full` compares the diff
492
522
  with a full observe of the same page. State lives in `~/.cdpilot/projects/<id>/agent-state.json`.
493
523
 
524
+ **What the printed list leaves out.** By default `actions` keeps every interactive entry, headings, regions
525
+ and navigation landmarks, and text that is clickable although the accessibility tree calls it text (a
526
+ `<div onclick>`: `cursor:pointer` starts there, or Chrome marks it clickable, and no link or button sits
527
+ above it). It leaves out plain text, table cells and list markers, entries whose name is only the sum of
528
+ their own kept children, and cuts names over 80 characters to 79 + `…` (output only). The text is still in
529
+ `text`. Only the printed list is thinned: refs, saved flows (`agent run` anchors on the full list) and
530
+ `act --ref` of a hidden ref are unchanged. One extra `DOMSnapshot.captureSnapshot` call (2 s limit) decides
531
+ "clickable"; if it fails nothing is hidden. `--full` (CLI `observe`/`act`/`run`; over MCP only `browser_agent_observe` takes `full: true`,
532
+ `browser_agent_act` and `browser_agent_run` have no `full`) prints the unfiltered list. `--rows` (or `CDPILOT_OBSERVE_FORMAT=rows`) prints `actions` and `changed.added` as arrays
533
+ `[ref, role, name]`, with the field value as a 4th element only for entries that have one; objects stay
534
+ the default.
535
+
494
536
  #### Learned flows: drive a site once, replay it with no model
495
537
 
496
538
  ```bash
@@ -506,8 +548,11 @@ Every `agent act` leaves a trail entry addressed by an *anchor*, the element's a
506
548
  position among equals (`{"role":"button","name":"Archive","nth":0}`), never a `@ref` or a DOM node id, which
507
549
  belong to one session. `agent flow save` writes the trail as a flow; `agent run` finds each anchor on the live
508
550
  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`.
551
+ with no model in the loop. The result's `tokens_avoided` is an upper bound — a full observation before each step;
552
+ an agent that reads diffs reads less. On three real sites (measured on 2026-11-10, before `observe` hid text that
553
+ cannot be acted on) the model loop read 2,292–18,331 tokens, the `agent run` result 90–136 (HN 18,331 → 90,
554
+ Wikipedia 15,474 → 136, httpbin 2,292 → 119). A default `observe` now prints about 4,300 tokens on HN and 8,300
555
+ on Wikipedia, still far above the `agent run` result. When a site has a flow, `agent observe` lists it under `known_flows`.
511
556
 
512
557
  **Drift stops, never guesses.** If a step's anchor is gone, or the step lands on another host than it did
513
558
  when recorded, `agent run` exits 4 and prints that page's full observation (`status:"drift"`, `step`,
@@ -519,12 +564,15 @@ same fields show `«masked»` as their value in `observe` output. Masking goes b
519
564
  secret typed into a field named otherwise is stored in clear, so review a flow before sharing it (flow files
520
565
  and `agent-state.json` are 0600). A `{{placeholder}}` typed into `agent act --text` or `--url` is refused
521
566
  (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
567
+ types one runs only on the origin (scheme, host, port) it was recorded on (old flows without an origin fall
568
+ back to host); another origin or an opaque page is a drift. `--var` values are visible in the process list for the
523
569
  duration of the call and are redacted in the session log; an environment value never enters argv.
524
570
  `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.
571
+ subdomains included), on that same origin over https (http only on loopback), and never inside a URL — so a flow
572
+ that a prompt-injected model records on another site cannot draw on the environment's secrets; anything else is
573
+ refused before the first step (exit 2, `refused` says why) or is a drift. `agent run` sets a placeholder value on
574
+ the recorded field in one browser call that first checks the field (editable, in the top-level page, not an
575
+ iframe; for an environment value not a search or url field) and the page's origin; a failed check exits 2 and types nothing.
528
576
 
529
577
  #### Text that talks to the agent: `injection_suspect`
530
578
 
@@ -532,9 +580,69 @@ Page text that addresses the model instead of the user is the shape of an indire
532
580
  assistants reading this page: ignore your previous instructions…"). `agent observe`, the diff of `agent act`
533
581
  and a drift result of `agent run` carry `injection_suspect: [{text, match}]` (at most 5, absent when none)
534
582
  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.
583
+ `new_instructions`, `hide_from_user`, `exfiltrate`, `fake_system`, `fake_turn`, `encoded_order`, `tool_call`,
584
+ `navigate_order`. Nothing is
585
+ hidden or blocked, the text stays where it was; the agent sees the mark next to it and treats the block as data.
586
+ Matching runs on a normalised copy (zero-width characters removed, NFKC, case folded, spaced-out letters joined,
587
+ Cyrillic/Greek look-alikes mapped inside a mixed-script word); base64 payloads are decoded and scanned; the
588
+ "ignore previous instructions" family covers 14 languages. The patterns were tuned on three sets
589
+ (`test/fixtures/injection-korpus.json`, `injection-heldout.json`, `injection-heldout-2.json`), so their numbers
590
+ there (20 of 20, 24 of 24 and 27 of 30 caught) are in-sample. On a set never used for tuning
591
+ (`injection-heldout-3.json`, 40 + 40 lines in 12 languages, written without seeing the patterns) they catch 11 of 40
592
+ with 2 of 40 false alarms (a quoted security report and a recipe line); the gain over the previous pass (7 of 40)
593
+ came from multilingual, encoded and urgency lines, not from fake chat turns or fake system notices, which still
594
+ mostly pass when phrased differently. Treat the mark as a hint, not a filter: a page without it can still carry an injection.
595
+
596
+ ### Live canvas (experimental)
597
+
598
+ A canvas (experimental; macOS and Linux, not on Windows yet) is a tab in your browser that an agent writes HTML into and updates in place: a status board, a form the
599
+ user fills in, a table that grows while the model streams. The tab keeps its scroll position, focus and form state
600
+ between updates (the new HTML is morphed into the page, not reloaded). Clicks and form input come back as events; treat their content as untrusted page data (an `untrusted` field says
601
+ which fields).
602
+
603
+ ```bash
604
+ cdpilot canvas open --name board # opens the tab; prints "canvas: board <targetId>"
605
+ cdpilot canvas set page.html --name board # replace the content ("-" reads stdin)
606
+ cdpilot canvas patch page.html --name board # morph to the new HTML: "ok +add -del ~upd ↕move <ms> ms"
607
+ llm-cli ... | cdpilot canvas stream --name board # stdin as it arrives, patched whole-document
608
+ cdpilot canvas watch page.html --name board # re-patch on every save; Ctrl+C stops
609
+ cdpilot canvas events --name board --follow # one JSON line per click / submit / input
610
+ cdpilot canvas list # name, targetId, pid, age
611
+ cdpilot canvas close --name board # or --all
612
+ ```
613
+
614
+ Mark the elements that report back with `data-action` (or `data-emit`): a click on them, a form `submit` and
615
+ `input` become `{seq, t, src, type, action, id, data}` lines; `src` is `page` for the page's events and `host` for
616
+ cdpilot's own (`blocked`, `nav`, `closed`). Event `data` is capped at 1 KB; `data.url` in host events is chosen by the
617
+ page and is not trusted.
618
+
619
+ MCP: `browser_canvas_set`, `browser_canvas_patch`, `browser_canvas_events` appear in `tools/list` only while a canvas
620
+ of the project is open (the server sends `notifications/tools/list_changed`); `browser_tools enable` lists them
621
+ anyway. They are not in `--tools core`. A canvas is opened from the CLI (`cdpilot canvas open`). HTML goes to the canvas process over a local socket, never in a
622
+ command line or the session log.
623
+
624
+ What the canvas page cannot do (the HTML is treated as untrusted — an agent may have copied it from anywhere):
625
+ - It runs in its own browser context, so it never sees your other tabs, cookies or storage; the context and the tab
626
+ close when the canvas process ends, even on SIGKILL.
627
+ - Only `http://cdpilot-canvas.invalid/<name>/` is served to it, with a CSP (`default-src 'none'`, inline script and
628
+ style only, `data:`/`blob:` images) and `sandbox allow-scripts`. Every other navigation gets an empty 204 and a
629
+ `blocked` event; every subresource request is refused (`BlockedByClient`). `--allow-net <host>` at `open` allows
630
+ https requests to that exact host on port 443 and nothing else.
631
+ - The canvas runs in a browser context whose proxy is a local port that never answers (`--allow-net` hosts bypass
632
+ it), so a request that slips past cdpilot's interception goes nowhere (speculation-rules prefetch did, measured
633
+ before this was added). Pop-ups the page opens in that context are closed.
634
+ - WebRTC constructors are removed before the page's scripts run, in its frames too (CSP does not cover WebRTC).
635
+ - The document is written only after cdpilot has proven it is the canvas document (frame URL and execution context);
636
+ if the context changes, the write is refused and the page reopened. Navigation history is cleared after each load.
637
+ - A leak test runs every vector we know (fetch, img, script, iframe, WebSocket, prefetch, ping, window.open, form,
638
+ meta refresh, `blob:` navigation, `history.back()`, speculation rules, pop-ups from derived documents, WebRTC
639
+ STUN/TURN in the page and in srcdoc frames) against an unguarded and a guarded canvas:
640
+ each vector must reach the test listener unguarded (or is reported as not measurable in that environment) and show 0 requests guarded. Not measurable there: `data:`
641
+ navigation (Chrome blocks it itself), preconnect and DNS prefetch (the canvas's off-the-record context does not
642
+ send these hints; the canvas also sends `X-DNS-Prefetch-Control: off`).
643
+
644
+ Not supported on Windows yet (the canvas process uses a Unix socket). Paint latency is not published until it is
645
+ measured on an idle machine.
538
646
 
539
647
  ### Session Management
540
648
 
@@ -620,7 +728,7 @@ cdpilot watch stop # Stop the screencast
620
728
  ```
621
729
 
622
730
  Works on both local files (`file://...`) and online video (YouTube, Vimeo,
623
- Twitter, Facebook, Instagram). Zero dependency — Pillow is optional and only
731
+ Twitter, Facebook, Instagram). No extra package needed — Pillow is optional and only
624
732
  used for motion-detection between frames.
625
733
 
626
734
  > **DRM limitation:** DRM-protected players (Netflix and similar) render as
@@ -641,7 +749,7 @@ cdpilot watch stop # Stop screencast
641
749
 
642
750
  ### Stealth & CAPTCHA
643
751
 
644
- Zero-dependency anti-fingerprint layer — patches `navigator.webdriver`,
752
+ Self-contained anti-fingerprint layer (no extra packages) — patches `navigator.webdriver`,
645
753
  `chrome.runtime`, plugins (proper `PluginArray` inheritance), WebGL
646
754
  vendor/renderer, permissions, hardware concurrency, and the `Worker`
647
755
  constructor. Injected via `Page.addScriptToEvaluateOnNewDocument` before
@@ -1033,12 +1141,14 @@ itself; it is passed on to every tool call / request it runs.
1033
1141
 
1034
1142
  Every cdpilot call is a fresh process: node starts (~0.1 s), Python imports the
1035
1143
  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
1144
+ runs — a browser paints a page in 20 ms, `cdpilot go` took about a second before
1145
+ 0.9.6; now 0.5–0.8 s fresh and ~0.23–0.30 s with `CDPILOT_DAEMON=1`
1037
1146
  (measurements: `.claude/docs/perf-taban-2026-11-08.md`). With
1038
1147
  `CDPILOT_DAEMON=1` the launcher keeps one warm Python process per project and
1039
1148
  environment and hands page commands to it over a Unix socket; the process keeps
1040
1149
  the module and its event loop between calls, so the second and later calls skip
1041
- the start-up cost.
1150
+ the Python start-up and CDP session set-up; node's ~0.1 s launcher remains: a
1151
+ browser-free command ~270 → ~140 ms, `content`/`eval` ~360 → ~110 ms (M1 Max).
1042
1152
 
1043
1153
  ```bash
1044
1154
  export CDPILOT_DAEMON=1
@@ -1071,7 +1181,9 @@ in its environment it runs each tool call in the server process the same way (th
1071
1181
  same executor, no socket involved) instead of starting a Python process per
1072
1182
  call — ~400 ms → ~10 ms before the CDP work. Lifecycle tools and a project
1073
1183
  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.
1184
+ 30 s is cancelled at its next async step. A tool call stuck in synchronous code
1185
+ (not waiting on the browser) is not interrupted by the 30 s cancel, unlike the
1186
+ fresh path's kill. See "Agent mode over MCP" above.
1075
1187
 
1076
1188
  ### Session log
1077
1189
 
@@ -1118,7 +1230,7 @@ cdpilot record stop [--save login-flow.json]
1118
1230
  cdpilot replay login-flow.json [--var k=v ...] [--delay real|none|<ms>] [--dry-run] [--continue]
1119
1231
  ```
1120
1232
 
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.
1233
+ 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.
1122
1234
 
1123
1235
  ### Use your own browser (human-in-the-loop)
1124
1236
 
@@ -1153,7 +1265,7 @@ google-chrome --remote-debugging-port=9450 --user-data-dir="$HOME/cdpilot-chrome
1153
1265
  cdpilot connect 9450 # or: cdpilot connect ws://127.0.0.1:9450/devtools/browser/<id>
1154
1266
  cdpilot connect --auto # or: find it via a DevToolsActivePort file
1155
1267
  cdpilot go https://example.com/dashboard
1156
- cdpilot disconnect # forget it; the browser keeps running
1268
+ cdpilot disconnect # close cdpilot's own tab, forget it; the browser keeps running
1157
1269
  ```
1158
1270
 
1159
1271
  Since Chrome 136, `--remote-debugging-port` is ignored on the default profile,
@@ -1205,7 +1317,8 @@ and exits with code 2 and the start command above instead of half-working.
1205
1317
  - `connect` refuses when this project already has a browser cdpilot launched
1206
1318
  ("stop it first (`cdpilot stop`) or use another project"), and only
1207
1319
  `127.0.0.1` / `localhost` is accepted (anything else exits 2). `disconnect`
1208
- also stops this project's `watch` daemon.
1320
+ closes cdpilot's own tab in that browser (never yours, never the last one)
1321
+ and stops this project's `watch` daemon.
1209
1322
  - cdpilot treats a browser as its own only with proof: the browser id it
1210
1323
  recorded at `launch` (or, for older entries, a live recorded pid that holds
1211
1324
  the debug port). A busy port is not proof, so a leftover registry entry never
@@ -1348,6 +1461,8 @@ print(result.stdout)
1348
1461
  | `CDPILOT_TIMEOUT` | unset | Default `--timeout` in seconds for every command (flag wins, `0` disables); expiry exits 124 |
1349
1462
  | `CDPILOT_NO_AUTOLAUNCH` | `0` | `1` = page commands fail with the old "Is the browser running?" error instead of launching the browser |
1350
1463
  | `CDPILOT_IDLE_CLOSE` | `15` | Minutes without a cdpilot command or page change before a browser cdpilot launched closes itself (fractions allowed, `0` = never; read at launch). Default applies to auto-launch and MCP launches; an explicit CLI `launch` is off unless this is set or `--idle-close <min>` is given |
1464
+ | `CDPILOT_JSON` | auto | `pretty` = indented, `compact` = one line; default: agent command JSON is indented in a terminal and compact UTF-8 otherwise |
1465
+ | `CDPILOT_OBSERVE_FORMAT` | unset | `rows` = `agent observe/act/run` print `actions` as `[ref, role, name(, value)]` arrays (same as `--rows`); MCP has no per-call switch |
1351
1466
  | `CDPILOT_LOG` | `1` | `0` = do not write the session log (`cdpilot log`) |
1352
1467
  | `CDPILOT_LOG_DAYS` | `14` | Days of session log to keep; older day files are deleted on the first write of a day (`0` keeps all) |
1353
1468
 
@@ -1359,7 +1474,7 @@ print(result.stdout)
1359
1474
  │ (CLI) │ Chrome DevTools │ (CDP mode) │
1360
1475
  └─────────────┘ Protocol └──────────────┘
1361
1476
  │ │
1362
- │ Zero dependencies │ Isolated profile
1477
+ │ Zero npm dependencies │ Isolated profile
1363
1478
  │ Pure HTTP + WebSocket │ Separate from your
1364
1479
  │ single file, stdlib + websockets │ personal browser
1365
1480
  └────────────────────────────────────────┘
@@ -1419,13 +1534,15 @@ The full adaptive layer is bench-neutral vs baseline (30/80 vs 30/80) for Stealt
1419
1534
 
1420
1535
  | Feature | cdpilot | Puppeteer | Playwright | Selenium |
1421
1536
  |---------|-----------|-----------|------------|----------|
1422
- | Install size | **one Python file + a small Node launcher, no node_modules** | 400MB+ | 200MB+ | 100MB+ |
1423
- | Dependencies | **0 npm** (Python: `websockets`, auto-installed) | 50+ | 30+ | Java + drivers |
1424
- | Setup time | **instant** | minutes | minutes | painful |
1537
+ | 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 |
1538
+ | Dependencies | **0 npm** (Python: `websockets`, auto-installed) | 6 direct npm | 1 (playwright-core) direct npm | 4 direct npm |
1539
+ | Setup time | **one npx; needs Python 3.10+** | minutes | minutes | painful |
1425
1540
  | AI-agent ready | **yes** | manual | manual | manual |
1426
1541
  | Browser download | **no** | yes (Chromium) | yes (3 browsers) | no |
1427
1542
  | CLI-first | **yes** | no (library) | no (library) | no |
1428
- | MCP support | **yes** | no | no | no |
1543
+ | MCP support | **yes** | deprecated official server | yes (official @playwright/mcp) | no official server |
1544
+
1545
+ Third-party numbers from npm on 27.09.2026; sources on [cdpilot.ndr.ist/compare](https://cdpilot.ndr.ist/compare).
1429
1546
 
1430
1547
  ## Monetization / Pro (Coming Soon)
1431
1548
 
@@ -1471,7 +1588,7 @@ Expired cookies are filtered automatically on load.
1471
1588
 
1472
1589
  ## Roadmap
1473
1590
 
1474
- The only browser MCP with built-in test assertions. Here's what we've shipped and what's next:
1591
+ A browser MCP with 10 built-in test assertions. Here's what we've shipped and what's next:
1475
1592
 
1476
1593
  ### Shipped
1477
1594
 
@@ -1485,7 +1602,7 @@ The only browser MCP with built-in test assertions. Here's what we've shipped an
1485
1602
  - [x] **Annotated screenshots** — @N badge overlays on interactive elements
1486
1603
  - [x] **Auto-wait** — MutationObserver-based, 5s automatic element waiting
1487
1604
  - [x] **`wait-for-text`** — adaptive text-based waiting (subtree + characterData) for streaming AI responses, async toasts, and selector-less synchronization
1488
- - [x] **`eval-batch`** — run N JS expressions in 1 CDP roundtrip (5-30x speedup vs sequential `eval`)
1605
+ - [x] **`eval-batch`** — run N JS expressions in one process and one CDP roundtrip instead of N
1489
1606
  - [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)
1490
1607
  - [x] **`dismiss`** — heuristic auto-click for LLM chat sign-up walls (EN+TR pattern library, destructive-action guards)
1491
1608
  - [x] **`adaptive`** — auto-escalate to stealth on CAPTCHA-protected hosts, persistent per-host memory ("run fast, climb walls")
package/bin/cdpilot.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * cdpilot — Zero-dependency browser automation CLI
4
+ * cdpilot — Browser automation CLI, zero npm dependencies
5
5
  * Entry point: detects Python, finds browser, delegates to cdpilot.py
6
6
  */
7
7
 
@@ -90,7 +90,8 @@ const DAEMON_SPAWN_CMDS = new Set([
90
90
  'tls-proxy', 'captcha-wait', 'help', '--help', '-h']);
91
91
  // "cmd sub" pairs that run fresh while the command itself is served warm
92
92
  // (agent observe/act are page steps; agent twitter logs in and posts).
93
- const DAEMON_SPAWN_SUBCMDS = new Set(['agent twitter']);
93
+ const DAEMON_SPAWN_SUBCMDS = new Set(['agent twitter', 'canvas open', 'canvas stream',
94
+ 'canvas watch', 'canvas events']);
94
95
 
95
96
  function daemonEnabled() {
96
97
  return process.env.CDPILOT_DAEMON === '1' && process.platform !== 'win32';
@@ -204,7 +205,14 @@ function daemonRequest(sockPath, payload, onFrame) {
204
205
  sock.destroy();
205
206
  return;
206
207
  }
207
- if (msg.exit !== undefined) { exitCode = msg.exit; continue; }
208
+ if (msg.exit !== undefined) {
209
+ // The last frame: the command ran and every output frame is through. Do not wait
210
+ // for the daemon to close the socket (K-0027: on Linux it held it open for 10 s).
211
+ exitCode = msg.exit;
212
+ done({ handled: true, code: exitCode });
213
+ sock.destroy();
214
+ return;
215
+ }
208
216
  onFrame(msg);
209
217
  }
210
218
  scanFrom = buf.length; // a frame longer than one chunk: do not rescan it
@@ -745,7 +753,7 @@ function resolveProjectConfig() {
745
753
 
746
754
  function showHelp() {
747
755
  console.log(`
748
- cdpilot v${VERSION} — Zero-dependency browser automation
756
+ cdpilot v${VERSION} — browser automation, zero npm dependencies
749
757
 
750
758
  USAGE
751
759
  cdpilot [--timeout <s>] <command> [args]
@@ -765,6 +773,13 @@ function showHelp() {
765
773
  a fresh process when it is missing or busy; not with
766
774
  --timeout; 'daemon status|stop'; idle exit after
767
775
  CDPILOT_DAEMON_IDLE seconds, default 1800).
776
+ CDPILOT_JSON pretty|compact: agent command JSON (default: indented on a
777
+ terminal, compact UTF-8 when stdout is not a terminal)
778
+ CDPILOT_OBSERVE_FORMAT
779
+ rows: agent observe/act/run print actions as
780
+ [ref, role, name, value?] arrays (same as --rows)
781
+ CDPILOT_VAR_HOSTS Hosts (comma-separated, host:port for another port)
782
+ an agent flow or replay step may type CDPILOT_VAR_* on
768
783
 
769
784
  SETUP
770
785
  setup Auto-detect browser, create isolated profile
@@ -913,6 +928,24 @@ function showHelp() {
913
928
  stop-all Stop all browser instances
914
929
 
915
930
  AI AGENT
931
+ agent observe [--diff] [--full] [--rows]
932
+ Page state as numbered @refs (+text); --diff = only
933
+ changes since last; --full = every entry (default
934
+ hides text that cannot be acted on); --rows =
935
+ [ref, role, name, value?] arrays
936
+ agent act --ref @N [--action click|type|fill|hover|submit] [--text X]
937
+ Acts, prints the diff (also --full, --rows)
938
+ agent act --url URL
939
+ Navigates silently, prints the diff
940
+ agent flow save <name> [--last N] | list | show <name> | delete <name>
941
+ Save the acts since the last navigation as a replayable
942
+ flow
943
+ agent run <name> [--var k=v] [--dry-run] [--full] [--rows]
944
+ Replay by anchors, no model; exit 4 on drift with the
945
+ page, 2 for a missing or refused variable
946
+ canvas open|set|patch|stream|watch|events|list|close [--name N] [--allow-net host]
947
+ Live canvas tab an agent writes HTML into (experimental,
948
+ not on Windows)
916
949
  mcp Start MCP server (stdin/stdout JSON-RPC)
917
950
  --tools core|all|a,b (env CDPILOT_MCP_TOOLS): smaller tool list
918
951
 
@@ -1177,6 +1210,25 @@ function spawnPython(python, args, env) {
1177
1210
  process.exit(1);
1178
1211
  });
1179
1212
 
1213
+ // A signal sent to the launcher alone (an agent harness, `kill <pid>`) used
1214
+ // to end the launcher and leave the Python child running (`canvas events
1215
+ // --follow` kept polling). Forward it. A terminal Ctrl+C or hangup reaches
1216
+ // the child itself through the process group, so SIGINT and SIGHUP go on
1217
+ // only if the child still runs 300 ms later (a second SIGINT would cut its
1218
+ // cleanup short). On Windows the console signals the child; nothing is sent.
1219
+ for (const sig of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
1220
+ process.on(sig, () => {
1221
+ if (process.platform === 'win32') return;
1222
+ const forward = () => {
1223
+ if (child.exitCode === null && child.signalCode === null) {
1224
+ try { child.kill(sig); } catch { /* already gone */ }
1225
+ }
1226
+ };
1227
+ if (sig === 'SIGTERM') forward();
1228
+ else setTimeout(forward, 300).unref();
1229
+ });
1230
+ }
1231
+
1180
1232
  // A child killed by a signal reports code === null. `code || 0` turned that
1181
1233
  // into success, so an OOM-killed or SIGKILLed run looked fine to scripts
1182
1234
  // and agents. Use the shell convention 128 + signal number instead.
@@ -1213,7 +1265,7 @@ if (cmd === 'status') {
1213
1265
  const sockPath = daemonSockPath(daemonKey(env, python));
1214
1266
  // env: the daemon applies the caller's environment to the request (proxy
1215
1267
  // and solver keys, PATH, TZ … are read when a command runs, not at import).
1216
- const payload = { argv: args, cwd: process.cwd(), env };
1268
+ const payload = { argv: args, cwd: process.cwd(), env, tty: process.stdout.isTTY === true };
1217
1269
  const stdin = daemonStdin(cmd, args);
1218
1270
  if (stdin !== undefined) payload.stdin = stdin;
1219
1271
  daemonRequest(sockPath, payload, (m) => {
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "cdpilot",
3
- "version": "0.9.6",
3
+ "version": "0.9.8",
4
4
  "mcpName": "io.github.mehmetnadir/cdpilot",
5
- "description": "Zero-dependency browser automation CLI. Raw CDP, 70+ commands, friction ladder, 3-tier stealth, CAPTCHA + press-and-hold solvers, proxy pools, MCP server. No Puppeteer/Playwright/Selenium.",
5
+ "description": "Browser automation CLI with zero npm dependencies (Python: websockets). Raw CDP, 70+ commands, friction ladder, 3-tier stealth, CAPTCHA + press-and-hold solvers, proxy pools, MCP server. No Puppeteer/Playwright/Selenium.",
6
6
  "bin": {
7
7
  "cdpilot": "./bin/cdpilot.js",
8
8
  "bctl": "./bin/cdpilot.js"
@@ -20,7 +20,7 @@
20
20
  "mcp",
21
21
  "ai-agent",
22
22
  "web-scraping",
23
- "zero-dependency",
23
+ "zero-npm-dependencies",
24
24
  "headless-browser",
25
25
  "puppeteer-alternative",
26
26
  "playwright-alternative",