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 +144 -27
- package/bin/cdpilot.js +57 -5
- package/package.json +3 -3
- package/src/cdpilot.py +4868 -300
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
|
[](https://www.npmjs.com/package/cdpilot)
|
|
6
6
|
[](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
|
|
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
|
|
510
|
-
|
|
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
|
|
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
|
|
526
|
-
records on another site cannot draw on the environment's secrets; anything else is
|
|
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
|
|
536
|
-
|
|
537
|
-
|
|
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).
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
|
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** |
|
|
1423
|
-
| Dependencies | **0 npm** (Python: `websockets`, auto-installed) |
|
|
1424
|
-
| Setup time | **
|
|
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** |
|
|
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
|
-
|
|
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
|
|
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 —
|
|
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) {
|
|
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} —
|
|
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.
|
|
3
|
+
"version": "0.9.8",
|
|
4
4
|
"mcpName": "io.github.mehmetnadir/cdpilot",
|
|
5
|
-
"description": "
|
|
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-
|
|
23
|
+
"zero-npm-dependencies",
|
|
24
24
|
"headless-browser",
|
|
25
25
|
"puppeteer-alternative",
|
|
26
26
|
"playwright-alternative",
|