cdpilot 0.9.6 → 0.9.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -24
- package/bin/cdpilot.js +25 -3
- package/package.json +3 -3
- package/src/cdpilot.py +1068 -85
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)
|
|
@@ -340,7 +340,7 @@ cdpilot network <url> --har trace.har # Export navigation traffic as HAR 1.2 JSO
|
|
|
340
340
|
cdpilot debug [url] # Full diagnostic (console+network+perf+shot)
|
|
341
341
|
cdpilot perf # Performance metrics
|
|
342
342
|
cdpilot eval <js> # Execute JavaScript
|
|
343
|
-
cdpilot eval-batch <json> # Run N JS expressions in
|
|
343
|
+
cdpilot eval-batch <json> # Run N JS expressions in one process and one CDP roundtrip instead of N
|
|
344
344
|
```
|
|
345
345
|
|
|
346
346
|
#### Network monitoring & HAR export
|
|
@@ -506,8 +506,10 @@ Every `agent act` leaves a trail entry addressed by an *anchor*, the element's a
|
|
|
506
506
|
position among equals (`{"role":"button","name":"Archive","nth":0}`), never a `@ref` or a DOM node id, which
|
|
507
507
|
belong to one session. `agent flow save` writes the trail as a flow; `agent run` finds each anchor on the live
|
|
508
508
|
page (exact name, then case-insensitive, then a longer name containing it when that is unambiguous) and acts,
|
|
509
|
-
with no model in the loop. The result's `tokens_avoided` is
|
|
510
|
-
|
|
509
|
+
with no model in the loop. The result's `tokens_avoided` is an upper bound — a full observation before each step;
|
|
510
|
+
an agent that reads diffs reads less. On three real sites the model loop read
|
|
511
|
+
2,292–18,331 tokens, the `agent run` result 90–136 (HN 18,331 → 90, Wikipedia 15,474 → 136,
|
|
512
|
+
httpbin 2,292 → 119). When a site has a flow, `agent observe` lists it under `known_flows`.
|
|
511
513
|
|
|
512
514
|
**Drift stops, never guesses.** If a step's anchor is gone, or the step lands on another host than it did
|
|
513
515
|
when recorded, `agent run` exits 4 and prints that page's full observation (`status:"drift"`, `step`,
|
|
@@ -519,12 +521,15 @@ same fields show `«masked»` as their value in `observe` output. Masking goes b
|
|
|
519
521
|
secret typed into a field named otherwise is stored in clear, so review a flow before sharing it (flow files
|
|
520
522
|
and `agent-state.json` are 0600). A `{{placeholder}}` typed into `agent act --text` or `--url` is refused
|
|
521
523
|
(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
|
|
524
|
+
types one runs only on the origin (scheme, host, port) it was recorded on (old flows without an origin fall
|
|
525
|
+
back to host); another origin or an opaque page is a drift. `--var` values are visible in the process list for the
|
|
523
526
|
duration of the call and are redacted in the session log; an environment value never enters argv.
|
|
524
527
|
`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.
|
|
528
|
+
subdomains included), on that same origin over https (http only on loopback), and never inside a URL — so a flow
|
|
529
|
+
that a prompt-injected model records on another site cannot draw on the environment's secrets; anything else is
|
|
530
|
+
refused before the first step (exit 2, `refused` says why) or is a drift. `agent run` sets a placeholder value on
|
|
531
|
+
the recorded field in one browser call that first checks the field (editable, in the top-level page, not an
|
|
532
|
+
iframe; for an environment value not a search or url field) and the page's origin; a failed check exits 2 and types nothing.
|
|
528
533
|
|
|
529
534
|
#### Text that talks to the agent: `injection_suspect`
|
|
530
535
|
|
|
@@ -532,9 +537,16 @@ Page text that addresses the model instead of the user is the shape of an indire
|
|
|
532
537
|
assistants reading this page: ignore your previous instructions…"). `agent observe`, the diff of `agent act`
|
|
533
538
|
and a drift result of `agent run` carry `injection_suspect: [{text, match}]` (at most 5, absent when none)
|
|
534
539
|
whenever such a block is in the text: `ignore_instructions`, `addresses_ai`, `system_prompt`,
|
|
535
|
-
`new_instructions`, `hide_from_user`, `exfiltrate`. Nothing is
|
|
536
|
-
the agent sees the mark next to it and treats the block as data.
|
|
537
|
-
|
|
540
|
+
`new_instructions`, `hide_from_user`, `exfiltrate`, `fake_system`, `tool_call`, `navigate_order`. Nothing is
|
|
541
|
+
hidden or blocked, the text stays where it was; the agent sees the mark next to it and treats the block as data.
|
|
542
|
+
Matching runs on a normalised copy (zero-width characters removed, NFKC, case folded, spaced-out letters joined);
|
|
543
|
+
the "ignore previous instructions" family also covers Spanish, French, Turkish, Japanese, German and Portuguese.
|
|
544
|
+
The patterns were tuned on two sets (`test/fixtures/injection-korpus.json`, `injection-heldout.json`), so their
|
|
545
|
+
numbers there (20 of 20 and 24 of 24 caught, 0 false alarms) are in-sample. On a set never used for tuning
|
|
546
|
+
(`injection-heldout-2.json`, 30 + 30 lines in 7 languages) they catch 8 of 30 with 1 of 30 false alarms (a recipe
|
|
547
|
+
line); on a third such set (`injection-heldout-3.json`, 40 + 40 lines in 12 languages) 7 of 40 with 2 of 40
|
|
548
|
+
(before this pass: 2 of 40). Fake chat turns, homoglyphs, base64 and Chinese/Russian/Arabic lines still pass.
|
|
549
|
+
Treat the mark as a hint, not a filter: a page without it can still carry an injection.
|
|
538
550
|
|
|
539
551
|
### Session Management
|
|
540
552
|
|
|
@@ -620,7 +632,7 @@ cdpilot watch stop # Stop the screencast
|
|
|
620
632
|
```
|
|
621
633
|
|
|
622
634
|
Works on both local files (`file://...`) and online video (YouTube, Vimeo,
|
|
623
|
-
Twitter, Facebook, Instagram).
|
|
635
|
+
Twitter, Facebook, Instagram). No extra package needed — Pillow is optional and only
|
|
624
636
|
used for motion-detection between frames.
|
|
625
637
|
|
|
626
638
|
> **DRM limitation:** DRM-protected players (Netflix and similar) render as
|
|
@@ -641,7 +653,7 @@ cdpilot watch stop # Stop screencast
|
|
|
641
653
|
|
|
642
654
|
### Stealth & CAPTCHA
|
|
643
655
|
|
|
644
|
-
|
|
656
|
+
Self-contained anti-fingerprint layer (no extra packages) — patches `navigator.webdriver`,
|
|
645
657
|
`chrome.runtime`, plugins (proper `PluginArray` inheritance), WebGL
|
|
646
658
|
vendor/renderer, permissions, hardware concurrency, and the `Worker`
|
|
647
659
|
constructor. Injected via `Page.addScriptToEvaluateOnNewDocument` before
|
|
@@ -1033,12 +1045,14 @@ itself; it is passed on to every tool call / request it runs.
|
|
|
1033
1045
|
|
|
1034
1046
|
Every cdpilot call is a fresh process: node starts (~0.1 s), Python imports the
|
|
1035
1047
|
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
|
|
1048
|
+
runs — a browser paints a page in 20 ms, `cdpilot go` took about a second before
|
|
1049
|
+
0.9.6; now 0.5–0.8 s fresh and ~0.23–0.30 s with `CDPILOT_DAEMON=1`
|
|
1037
1050
|
(measurements: `.claude/docs/perf-taban-2026-11-08.md`). With
|
|
1038
1051
|
`CDPILOT_DAEMON=1` the launcher keeps one warm Python process per project and
|
|
1039
1052
|
environment and hands page commands to it over a Unix socket; the process keeps
|
|
1040
1053
|
the module and its event loop between calls, so the second and later calls skip
|
|
1041
|
-
the start-up
|
|
1054
|
+
the Python start-up and CDP session set-up; node's ~0.1 s launcher remains: a
|
|
1055
|
+
browser-free command ~270 → ~140 ms, `content`/`eval` ~360 → ~110 ms (M1 Max).
|
|
1042
1056
|
|
|
1043
1057
|
```bash
|
|
1044
1058
|
export CDPILOT_DAEMON=1
|
|
@@ -1071,7 +1085,9 @@ in its environment it runs each tool call in the server process the same way (th
|
|
|
1071
1085
|
same executor, no socket involved) instead of starting a Python process per
|
|
1072
1086
|
call — ~400 ms → ~10 ms before the CDP work. Lifecycle tools and a project
|
|
1073
1087
|
configuration that moved since the server started still run fresh; a call past
|
|
1074
|
-
30 s is cancelled at its next async step.
|
|
1088
|
+
30 s is cancelled at its next async step. A tool call stuck in synchronous code
|
|
1089
|
+
(not waiting on the browser) is not interrupted by the 30 s cancel, unlike the
|
|
1090
|
+
fresh path's kill. See "Agent mode over MCP" above.
|
|
1075
1091
|
|
|
1076
1092
|
### Session log
|
|
1077
1093
|
|
|
@@ -1118,7 +1134,7 @@ cdpilot record stop [--save login-flow.json]
|
|
|
1118
1134
|
cdpilot replay login-flow.json [--var k=v ...] [--delay real|none|<ms>] [--dry-run] [--continue]
|
|
1119
1135
|
```
|
|
1120
1136
|
|
|
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.
|
|
1137
|
+
Commands that run while recording is active are appended to `$CDPILOT_HOME/record/active.json`; secrets are replaced by `{{name}}` placeholders before a step is written to active.json. `CDPILOT_LOG=0` does not turn recording off, and steps run by `replay` are not recorded. Failed steps (`exit != 0`) are dropped at `record stop`. A value becomes a placeholder `{{name}}` cataloged under `vars` if the selector/label is secret-shaped (`#password`, `input[type=password]`, API key), or the value matches a token/bearer pattern or secret URL parameter. Other typed values (emails, search terms, anything the rules do not recognise) are stored verbatim, so review a flow before sharing it; `--save` files are created with mode 0600. In `replay`, `--delay real` reproduces recorded inter-step timing (capped at 10 s per gap), while `--delay none` or a fixed millisecond delay skips waiting. Variables are supplied via `--var k=v` or `CDPILOT_VAR_<NAME>`; missing variables halt replay before any step runs. A `--var` value fills a `{{name}}` anywhere in a step. A `CDPILOT_VAR_<NAME>` environment value fills only the value of an input step (`fill`, `type`, `smart-fill`, `smart-select`, `assert-value` — never its selector, a flag or a flag's value, never a step routed into a frame or typed with `--entropy=on`), and only on an https page whose host `CDPILOT_VAR_HOSTS` names (subdomains included; http only on localhost, 127.0.0.1, [::1] and *.localhost; another port only when the entry is written `host:port`; a URL with a backslash, a control character, whitespace, or `@` / `%` before its path is refused). The value reaches the step's process through its environment, never its command line. The page is checked right before the step and again inside the one browser call that sets the value; that call also refuses a target that is not a text, password, email, tel or number input, a textarea or a select of the top-level page (an iframe, a contenteditable element, a search or url field). A flow that takes any environment value may contain only these steps, which run no page script and print no page data: `go` / `open` (only to a page the rule above allows; the page the browser lands on is read back and checked too), `click`, `dblclick`, `hover`, `smart-click`, `submit`, `scroll-to`, `dismiss`, `keys` (no Ctrl/Alt/Meta), `fill`, `type`, `smart-fill`, `smart-select`, `wait`, `wait-for`, `wait-for-text`, `assert-count`, `assert-visible`, `assert-hidden`, and `assert-value` that takes the value itself. One `eval`, `run`, `batch`, `tools call` or any other step refuses the whole flow, as does an XPath selector or one that reacts to a typed value (`[value…]`, `@value`, `value=`, `:placeholder-shown`, `:valid`, `:checked` and the like, CSS comments and escapes included); in such a flow replay never shows a step's stdout or its duration. Once a value is typed, every later step runs only on the exact origin it was typed on (checked before each step), and any other page stops the replay with exit 2. The flow chooses the field, so a flow can still type the value into a field the allowed site shows or sends on (a comment box, a form): replay only flows you trust. A `google.com` entry covers every subdomain, so prefer exact hosts. A refusal exits 2 before the first step (JSON with `missing` / `refused`, names only) or stops the replay at that step with exit 2. `go`, `eval` and every other command take `--var` only. Replaying a flow runs its steps with your browser profile, so replay only flows you trust: a flow with `eval` can act on any page you are logged into.
|
|
1122
1138
|
|
|
1123
1139
|
### Use your own browser (human-in-the-loop)
|
|
1124
1140
|
|
|
@@ -1359,7 +1375,7 @@ print(result.stdout)
|
|
|
1359
1375
|
│ (CLI) │ Chrome DevTools │ (CDP mode) │
|
|
1360
1376
|
└─────────────┘ Protocol └──────────────┘
|
|
1361
1377
|
│ │
|
|
1362
|
-
│ Zero dependencies
|
|
1378
|
+
│ Zero npm dependencies │ Isolated profile
|
|
1363
1379
|
│ Pure HTTP + WebSocket │ Separate from your
|
|
1364
1380
|
│ single file, stdlib + websockets │ personal browser
|
|
1365
1381
|
└────────────────────────────────────────┘
|
|
@@ -1419,13 +1435,15 @@ The full adaptive layer is bench-neutral vs baseline (30/80 vs 30/80) for Stealt
|
|
|
1419
1435
|
|
|
1420
1436
|
| Feature | cdpilot | Puppeteer | Playwright | Selenium |
|
|
1421
1437
|
|---------|-----------|-----------|------------|----------|
|
|
1422
|
-
| Install size | **one Python file + a small Node launcher, no node_modules** |
|
|
1423
|
-
| Dependencies | **0 npm** (Python: `websockets`, auto-installed) |
|
|
1424
|
-
| Setup time | **
|
|
1438
|
+
| Install size | **one Python file + a small Node launcher, no node_modules** | puppeteer-core ~5.7MB + Chromium ~170-280MB | playwright-core ~12.8MB + browsers | selenium-webdriver ~21.8MB + drivers |
|
|
1439
|
+
| Dependencies | **0 npm** (Python: `websockets`, auto-installed) | 6 direct npm | 1 (playwright-core) direct npm | 4 direct npm |
|
|
1440
|
+
| Setup time | **one npx; needs Python 3.10+** | minutes | minutes | painful |
|
|
1425
1441
|
| AI-agent ready | **yes** | manual | manual | manual |
|
|
1426
1442
|
| Browser download | **no** | yes (Chromium) | yes (3 browsers) | no |
|
|
1427
1443
|
| CLI-first | **yes** | no (library) | no (library) | no |
|
|
1428
|
-
| MCP support | **yes** |
|
|
1444
|
+
| MCP support | **yes** | deprecated official server | yes (official @playwright/mcp) | no official server |
|
|
1445
|
+
|
|
1446
|
+
Third-party numbers from npm on 27.09.2026; sources on [cdpilot.ndr.ist/compare](https://cdpilot.ndr.ist/compare).
|
|
1429
1447
|
|
|
1430
1448
|
## Monetization / Pro (Coming Soon)
|
|
1431
1449
|
|
|
@@ -1471,7 +1489,7 @@ Expired cookies are filtered automatically on load.
|
|
|
1471
1489
|
|
|
1472
1490
|
## Roadmap
|
|
1473
1491
|
|
|
1474
|
-
|
|
1492
|
+
A browser MCP with 10 built-in test assertions. Here's what we've shipped and what's next:
|
|
1475
1493
|
|
|
1476
1494
|
### Shipped
|
|
1477
1495
|
|
|
@@ -1485,7 +1503,7 @@ The only browser MCP with built-in test assertions. Here's what we've shipped an
|
|
|
1485
1503
|
- [x] **Annotated screenshots** — @N badge overlays on interactive elements
|
|
1486
1504
|
- [x] **Auto-wait** — MutationObserver-based, 5s automatic element waiting
|
|
1487
1505
|
- [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
|
|
1506
|
+
- [x] **`eval-batch`** — run N JS expressions in one process and one CDP roundtrip instead of N
|
|
1489
1507
|
- [x] **`block`** — request blocking via `Network.setBlockedURLs` with built-in presets (images/fonts/ads/media); fewer bytes and faster loads on opt-in (measure on your own pages)
|
|
1490
1508
|
- [x] **`dismiss`** — heuristic auto-click for LLM chat sign-up walls (EN+TR pattern library, destructive-action guards)
|
|
1491
1509
|
- [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
|
|
|
@@ -204,7 +204,14 @@ function daemonRequest(sockPath, payload, onFrame) {
|
|
|
204
204
|
sock.destroy();
|
|
205
205
|
return;
|
|
206
206
|
}
|
|
207
|
-
if (msg.exit !== undefined) {
|
|
207
|
+
if (msg.exit !== undefined) {
|
|
208
|
+
// The last frame: the command ran and every output frame is through. Do not wait
|
|
209
|
+
// for the daemon to close the socket (K-0027: on Linux it held it open for 10 s).
|
|
210
|
+
exitCode = msg.exit;
|
|
211
|
+
done({ handled: true, code: exitCode });
|
|
212
|
+
sock.destroy();
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
208
215
|
onFrame(msg);
|
|
209
216
|
}
|
|
210
217
|
scanFrom = buf.length; // a frame longer than one chunk: do not rescan it
|
|
@@ -745,7 +752,7 @@ function resolveProjectConfig() {
|
|
|
745
752
|
|
|
746
753
|
function showHelp() {
|
|
747
754
|
console.log(`
|
|
748
|
-
cdpilot v${VERSION} —
|
|
755
|
+
cdpilot v${VERSION} — browser automation, zero npm dependencies
|
|
749
756
|
|
|
750
757
|
USAGE
|
|
751
758
|
cdpilot [--timeout <s>] <command> [args]
|
|
@@ -765,6 +772,8 @@ function showHelp() {
|
|
|
765
772
|
a fresh process when it is missing or busy; not with
|
|
766
773
|
--timeout; 'daemon status|stop'; idle exit after
|
|
767
774
|
CDPILOT_DAEMON_IDLE seconds, default 1800).
|
|
775
|
+
CDPILOT_VAR_HOSTS Hosts (comma-separated, host:port for another port)
|
|
776
|
+
an agent flow or replay step may type CDPILOT_VAR_* on
|
|
768
777
|
|
|
769
778
|
SETUP
|
|
770
779
|
setup Auto-detect browser, create isolated profile
|
|
@@ -913,6 +922,19 @@ function showHelp() {
|
|
|
913
922
|
stop-all Stop all browser instances
|
|
914
923
|
|
|
915
924
|
AI AGENT
|
|
925
|
+
agent observe [--diff]
|
|
926
|
+
Page state as numbered @refs (+text); --diff = only
|
|
927
|
+
changes since last
|
|
928
|
+
agent act --ref @N [--action click|type|fill|hover|submit] [--text X]
|
|
929
|
+
Acts, prints the diff
|
|
930
|
+
agent act --url URL
|
|
931
|
+
Navigates silently, prints the diff
|
|
932
|
+
agent flow save <name> [--last N] | list | show <name> | delete <name>
|
|
933
|
+
Save the acts since the last navigation as a replayable
|
|
934
|
+
flow
|
|
935
|
+
agent run <name> [--var k=v] [--dry-run]
|
|
936
|
+
Replay by anchors, no model; exit 4 on drift with the
|
|
937
|
+
page, 2 for a missing or refused variable
|
|
916
938
|
mcp Start MCP server (stdin/stdout JSON-RPC)
|
|
917
939
|
--tools core|all|a,b (env CDPILOT_MCP_TOOLS): smaller tool list
|
|
918
940
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cdpilot",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.7",
|
|
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",
|