cdpilot 0.9.7 → 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
@@ -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
@@ -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
@@ -507,9 +549,10 @@ position among equals (`{"role":"button","name":"Archive","nth":0}`), never a `@
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
551
  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`.
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`.
513
556
 
514
557
  **Drift stops, never guesses.** If a step's anchor is gone, or the step lands on another host than it did
515
558
  when recorded, `agent run` exits 4 and prints that page's full observation (`status:"drift"`, `step`,
@@ -537,16 +580,69 @@ Page text that addresses the model instead of the user is the shape of an indire
537
580
  assistants reading this page: ignore your previous instructions…"). `agent observe`, the diff of `agent act`
538
581
  and a drift result of `agent run` carry `injection_suspect: [{text, match}]` (at most 5, absent when none)
539
582
  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
583
+ `new_instructions`, `hide_from_user`, `exfiltrate`, `fake_system`, `fake_turn`, `encoded_order`, `tool_call`,
584
+ `navigate_order`. Nothing is
541
585
  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.
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.
550
646
 
551
647
  ### Session Management
552
648
 
@@ -1169,7 +1265,7 @@ google-chrome --remote-debugging-port=9450 --user-data-dir="$HOME/cdpilot-chrome
1169
1265
  cdpilot connect 9450 # or: cdpilot connect ws://127.0.0.1:9450/devtools/browser/<id>
1170
1266
  cdpilot connect --auto # or: find it via a DevToolsActivePort file
1171
1267
  cdpilot go https://example.com/dashboard
1172
- cdpilot disconnect # forget it; the browser keeps running
1268
+ cdpilot disconnect # close cdpilot's own tab, forget it; the browser keeps running
1173
1269
  ```
1174
1270
 
1175
1271
  Since Chrome 136, `--remote-debugging-port` is ignored on the default profile,
@@ -1221,7 +1317,8 @@ and exits with code 2 and the start command above instead of half-working.
1221
1317
  - `connect` refuses when this project already has a browser cdpilot launched
1222
1318
  ("stop it first (`cdpilot stop`) or use another project"), and only
1223
1319
  `127.0.0.1` / `localhost` is accepted (anything else exits 2). `disconnect`
1224
- 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.
1225
1322
  - cdpilot treats a browser as its own only with proof: the browser id it
1226
1323
  recorded at `launch` (or, for older entries, a live recorded pid that holds
1227
1324
  the debug port). A busy port is not proof, so a leftover registry entry never
@@ -1364,6 +1461,8 @@ print(result.stdout)
1364
1461
  | `CDPILOT_TIMEOUT` | unset | Default `--timeout` in seconds for every command (flag wins, `0` disables); expiry exits 124 |
1365
1462
  | `CDPILOT_NO_AUTOLAUNCH` | `0` | `1` = page commands fail with the old "Is the browser running?" error instead of launching the browser |
1366
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 |
1367
1466
  | `CDPILOT_LOG` | `1` | `0` = do not write the session log (`cdpilot log`) |
1368
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) |
1369
1468
 
package/bin/cdpilot.js CHANGED
@@ -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';
@@ -772,6 +773,11 @@ function showHelp() {
772
773
  a fresh process when it is missing or busy; not with
773
774
  --timeout; 'daemon status|stop'; idle exit after
774
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)
775
781
  CDPILOT_VAR_HOSTS Hosts (comma-separated, host:port for another port)
776
782
  an agent flow or replay step may type CDPILOT_VAR_* on
777
783
 
@@ -922,19 +928,24 @@ function showHelp() {
922
928
  stop-all Stop all browser instances
923
929
 
924
930
  AI AGENT
925
- agent observe [--diff]
931
+ agent observe [--diff] [--full] [--rows]
926
932
  Page state as numbered @refs (+text); --diff = only
927
- changes since last
933
+ changes since last; --full = every entry (default
934
+ hides text that cannot be acted on); --rows =
935
+ [ref, role, name, value?] arrays
928
936
  agent act --ref @N [--action click|type|fill|hover|submit] [--text X]
929
- Acts, prints the diff
937
+ Acts, prints the diff (also --full, --rows)
930
938
  agent act --url URL
931
939
  Navigates silently, prints the diff
932
940
  agent flow save <name> [--last N] | list | show <name> | delete <name>
933
941
  Save the acts since the last navigation as a replayable
934
942
  flow
935
- agent run <name> [--var k=v] [--dry-run]
943
+ agent run <name> [--var k=v] [--dry-run] [--full] [--rows]
936
944
  Replay by anchors, no model; exit 4 on drift with the
937
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)
938
949
  mcp Start MCP server (stdin/stdout JSON-RPC)
939
950
  --tools core|all|a,b (env CDPILOT_MCP_TOOLS): smaller tool list
940
951
 
@@ -1199,6 +1210,25 @@ function spawnPython(python, args, env) {
1199
1210
  process.exit(1);
1200
1211
  });
1201
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
+
1202
1232
  // A child killed by a signal reports code === null. `code || 0` turned that
1203
1233
  // into success, so an OOM-killed or SIGKILLed run looked fine to scripts
1204
1234
  // and agents. Use the shell convention 128 + signal number instead.
@@ -1235,7 +1265,7 @@ if (cmd === 'status') {
1235
1265
  const sockPath = daemonSockPath(daemonKey(env, python));
1236
1266
  // env: the daemon applies the caller's environment to the request (proxy
1237
1267
  // and solver keys, PATH, TZ … are read when a command runs, not at import).
1238
- const payload = { argv: args, cwd: process.cwd(), env };
1268
+ const payload = { argv: args, cwd: process.cwd(), env, tty: process.stdout.isTTY === true };
1239
1269
  const stdin = daemonStdin(cmd, args);
1240
1270
  if (stdin !== undefined) payload.stdin = stdin;
1241
1271
  daemonRequest(sockPath, payload, (m) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cdpilot",
3
- "version": "0.9.7",
3
+ "version": "0.9.8",
4
4
  "mcpName": "io.github.mehmetnadir/cdpilot",
5
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": {