cdpilot 0.9.2 → 0.9.3
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 +134 -3
- package/bin/cdpilot.js +52 -2
- package/package.json +1 -1
- package/src/cdpilot.py +2196 -47
package/README.md
CHANGED
|
@@ -144,6 +144,76 @@ cdpilot scroll-to <selector> # Scroll element into view
|
|
|
144
144
|
cdpilot drag <from> <to> # Drag and drop
|
|
145
145
|
```
|
|
146
146
|
|
|
147
|
+
### Element targeting inside iframes
|
|
148
|
+
|
|
149
|
+
Card forms (Stripe/iyzico-style), embedded login widgets and the reCAPTCHA
|
|
150
|
+
checkbox live inside `<iframe>`s. Name the frame chain before the element
|
|
151
|
+
selector with `>>>`, or pass `--frame`:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
# Payment-style page: the card fields are inside <iframe id="card" title="Secure card payment">
|
|
155
|
+
cdpilot fill "iframe#card >>> input[name=cardnumber]" "4242424242424242"
|
|
156
|
+
cdpilot type --frame "#card" "input[name=cvc]" "123"
|
|
157
|
+
cdpilot click "iframe[title='Secure card payment'] >>> button#pay-btn"
|
|
158
|
+
|
|
159
|
+
# Nested frames: outermost first
|
|
160
|
+
cdpilot click "iframe.checkout >>> iframe.card >>> button"
|
|
161
|
+
|
|
162
|
+
# --frame <selector|index|url-substring>, nestable with >>>
|
|
163
|
+
cdpilot click --frame 0 "button.submit" # first iframe (same order as `frame list`)
|
|
164
|
+
cdpilot fill --frame "url=js.stripe.com" "input[name=cardnumber]" "4242424242424242"
|
|
165
|
+
cdpilot frame list --frame "#card" # iframes inside that frame
|
|
166
|
+
cdpilot frame eval --frame "#card" "document.title" # runs in the frame's own page context
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
- Works for `click`, `fill`, `type`, `submit`, `hover`, `dblclick`, `rightclick`,
|
|
170
|
+
`smart-click`, `smart-fill`, `smart-select` and `frame list|eval|shadow`.
|
|
171
|
+
- A frame hop is a CSS selector, a 0-based index, a `name`/`id`, or
|
|
172
|
+
`url=<src substring>`. `--frame` also takes a bare `src` substring.
|
|
173
|
+
`>>>` inside quotes or `[...]` is literal. A selector that matches an
|
|
174
|
+
element *wrapping* an iframe (Stripe's `#card-element`) enters the iframe
|
|
175
|
+
inside it and says so on stderr:
|
|
176
|
+
`note: '#card-element' is not an iframe; using the iframe inside it`.
|
|
177
|
+
- Selectors and texts that already contain `>>>` keep working: when the part
|
|
178
|
+
before the first `>>>` finds no iframe in the page, or a segment is empty
|
|
179
|
+
(`click "Next >>>"`), the whole string is used as written, exactly as
|
|
180
|
+
before. If that fails too, one stderr line explains:
|
|
181
|
+
`note: 'Home' matched no iframe; used the selector as written`.
|
|
182
|
+
`--frame` never falls back: a frame it cannot find is an error (exit 1).
|
|
183
|
+
- For `smart-click` / `smart-fill` / `smart-select` the part before the first
|
|
184
|
+
`>>>` is a frame only if it looks like a selector (it has `#`, `.` or `[`,
|
|
185
|
+
or starts with an `iframe`/`frame` tag) or is `url=…`; words stay text.
|
|
186
|
+
`smart-click "Main >>> Settings"` clicks the page's "Main >>> Settings"
|
|
187
|
+
link even when a `<main>` element holds an iframe. For a frame index or a
|
|
188
|
+
bare name use `--frame`.
|
|
189
|
+
- Same-origin **and cross-origin** frames work, including out-of-process
|
|
190
|
+
iframes (site isolation): cdpilot resolves each hop over CDP
|
|
191
|
+
(`DOM.describeNode` → frame id → the frame's execution context, or
|
|
192
|
+
`Target.attachToTarget` with a flat session for an out-of-process frame),
|
|
193
|
+
never through `contentDocument`, which the browser blocks cross-origin.
|
|
194
|
+
- Real mouse input (`hover`, `dblclick`, `rightclick`, `--entropy=on` clicks)
|
|
195
|
+
is dispatched at page coordinates: the frame's offset in the page is added
|
|
196
|
+
automatically, so the browser hit-tests into the right frame.
|
|
197
|
+
- `smart-click` / `smart-fill` / `smart-select` look in the page first and
|
|
198
|
+
compare texts with whitespace collapsed (` ` and line breaks count as
|
|
199
|
+
one space). An enabled page element whose text or label contains the whole
|
|
200
|
+
query is used. If the page's only such elements are disabled, the command
|
|
201
|
+
fails as before (`no enabled element matches …`) without looking in frames.
|
|
202
|
+
Otherwise the visible frames are searched breadth-first for a whole-query
|
|
203
|
+
match (at most 20 frames, and 2 s for all of the search's CDP calls) and the
|
|
204
|
+
match is reported on stderr:
|
|
205
|
+
`smart-click: matched inside frame iframe#card (https://…)`. The search only
|
|
206
|
+
looks (no click, no typing); the command then acts once, in the chosen
|
|
207
|
+
frame. With no frame match, the page's partial (word) match is used as
|
|
208
|
+
before. When the 2 s run out:
|
|
209
|
+
`smart-click: frame search stopped after 2s (3 of 7 frames)`; a frame that
|
|
210
|
+
had not answered by then has not been touched.
|
|
211
|
+
- `smart-fill` and `smart-select` search automatically only frames of the
|
|
212
|
+
page's own origin, so a typed value never lands in a third-party frame (an
|
|
213
|
+
ad, chat or payment widget) by accident; reach a cross-origin frame with
|
|
214
|
+
`>>>` or `--frame`. `smart-click` searches all visible frames.
|
|
215
|
+
- `frame list` without `--frame` prints the same list as before.
|
|
216
|
+
|
|
147
217
|
### Debugging
|
|
148
218
|
|
|
149
219
|
```bash
|
|
@@ -308,7 +378,8 @@ cdpilot storage # localStorage contents
|
|
|
308
378
|
cdpilot upload <sel> <file> # Upload file to input
|
|
309
379
|
cdpilot multi-eval <js> # Execute JS in all tabs
|
|
310
380
|
cdpilot headless [on|off] # Toggle headless mode
|
|
311
|
-
cdpilot frame list # List iframes
|
|
381
|
+
cdpilot frame list # List iframes (index = --frame <index>)
|
|
382
|
+
cdpilot frame eval --frame <f> <js> # Run JS inside an iframe (cross-origin too)
|
|
312
383
|
cdpilot dialog auto-accept # Auto-accept dialogs
|
|
313
384
|
cdpilot permission grant geo # Grant geolocation
|
|
314
385
|
```
|
|
@@ -562,7 +633,7 @@ Earlier figures on this page (sannysoft 24/24, intoli 6/6) were measured on v0.4
|
|
|
562
633
|
|
|
563
634
|
```bash
|
|
564
635
|
cdpilot browser [name|auto] # workload-aware browser selection
|
|
565
|
-
cdpilot health # JSON: alive, port, tabs, browser, today's crashes
|
|
636
|
+
cdpilot health # JSON: alive, port, tabs, browser, today's crashes, idle close
|
|
566
637
|
```
|
|
567
638
|
|
|
568
639
|
`cdpilot health` is designed for shell watchdogs:
|
|
@@ -584,6 +655,33 @@ Lifecycle, status and configuration commands (`launch`, `stop`, `close`,
|
|
|
584
655
|
`mcp`, `serve`, …) never launch. `CDPILOT_NO_AUTOLAUNCH=1` restores the old
|
|
585
656
|
"CDP connection error. Is the browser running?" error and exit code 1.
|
|
586
657
|
|
|
658
|
+
**Idle auto-close.** A browser that a page command auto-launched, or that the
|
|
659
|
+
MCP server launched (`browser_launch` included), closes itself after
|
|
660
|
+
**15 minutes** without a cdpilot command or a visible page change, so a
|
|
661
|
+
finished agent task does not leave it holding memory. An explicit CLI
|
|
662
|
+
`cdpilot launch` stays open (you may be browsing in it by hand) unless you ask:
|
|
663
|
+
`cdpilot launch --idle-close 30` or `CDPILOT_IDLE_CLOSE=30`. The env var also
|
|
664
|
+
sets the auto-launch delay (read when the browser starts; fractions allowed);
|
|
665
|
+
`0` turns idle close off. A browser you started yourself, or one cdpilot merely
|
|
666
|
+
attached to, is never closed.
|
|
667
|
+
|
|
668
|
+
What counts as use: any cdpilot command except the read-only checks (`status`,
|
|
669
|
+
`health`, `projects`, `version` — a `cdpilot health` watchdog loop does not keep
|
|
670
|
+
the browser alive), MCP tool calls, `serve` requests, a CDP client still
|
|
671
|
+
attached to a page (a long-running command, `watch`, Playwright via
|
|
672
|
+
`connectOverCDP`), and any change in the open pages' URLs or in the set of
|
|
673
|
+
tabs (someone navigating, a tab opened or closed). Title changes do not count,
|
|
674
|
+
so a page that rewrites its own title (a clock, an unread counter) cannot keep
|
|
675
|
+
the browser alive. `cdpilot status` and
|
|
676
|
+
`cdpilot health` show `idle close in 12m` or `idle close off`.
|
|
677
|
+
|
|
678
|
+
How: every command stamps `~/.cdpilot/projects/<id>/last-activity`; the launch
|
|
679
|
+
starts a small detached watcher (one per port, no console window on Windows)
|
|
680
|
+
that checks every ≤30 s, stops the browser like `cdpilot stop`, marks it
|
|
681
|
+
stopped in the registry and exits — it also exits as soon as the browser is
|
|
682
|
+
gone for any other reason. Browsers started by `serve --api` are managed by the
|
|
683
|
+
server and have no idle close.
|
|
684
|
+
|
|
587
685
|
**Timeouts.** Any command takes `--timeout <seconds>`, before or after the
|
|
588
686
|
command name, or a default from `CDPILOT_TIMEOUT` (the flag wins; `0`
|
|
589
687
|
disables). It bounds the whole command's wall-clock: on expiry cdpilot prints
|
|
@@ -600,6 +698,36 @@ CDPILOT_TIMEOUT=60 cdpilot run flow.cdp # default for every command (and each
|
|
|
600
698
|
For `mcp` and `serve` the value is not applied to the long-running server
|
|
601
699
|
itself; it is passed on to every tool call / request it runs.
|
|
602
700
|
|
|
701
|
+
### Session log
|
|
702
|
+
|
|
703
|
+
Every command (and every MCP tool call) appends one JSON line to a local,
|
|
704
|
+
per-project log, so when a browser task is done there is a record of what was
|
|
705
|
+
done and found. Nothing leaves the machine.
|
|
706
|
+
|
|
707
|
+
```bash
|
|
708
|
+
cdpilot log # today's commands: time, exit, command, url, result
|
|
709
|
+
cdpilot log --md # Markdown report: pages visited, actions, errors, files produced
|
|
710
|
+
cdpilot log --json # raw lines (one JSON object per command)
|
|
711
|
+
cdpilot log --days 3 # include the last 3 days
|
|
712
|
+
cdpilot log --path # where the files are
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
Each line has `ts`, `cmd`, `args`, `exit`, `duration_ms`, the page `url` and
|
|
716
|
+
`title` after the command (when the command already had them), a ~200-char
|
|
717
|
+
`summary` of its output, the `error` line, and `files` it wrote (screenshots,
|
|
718
|
+
PDFs). Files live in `~/.cdpilot/projects/<project-id>/log/<YYYY-MM-DD>.jsonl`.
|
|
719
|
+
|
|
720
|
+
Redaction happens before anything is written: values given to `fill`, `type`,
|
|
721
|
+
`smart-fill`, `smart-select`, `assert-value` and `dialog prompt` become
|
|
722
|
+
`«redacted:N chars»`; so do values of password/token/key/secret/cookie/auth
|
|
723
|
+
flags and headers, token-shaped arguments, and URL query/fragment values whose
|
|
724
|
+
names contain token, key, secret, password, auth, code or session. `cookies`
|
|
725
|
+
and `storage` output is never logged; `eval` source is, with string literals
|
|
726
|
+
over 40 chars cut and secret-looking ones replaced. Logging is best effort: it
|
|
727
|
+
never changes a command's output or exit code, and a failed write costs one
|
|
728
|
+
stderr line. `CDPILOT_LOG=0` turns it off; `CDPILOT_LOG_DAYS` (default 14)
|
|
729
|
+
sets how many days are kept. The MCP server exposes it as `browser_log`.
|
|
730
|
+
|
|
603
731
|
### Scaling & Workstation Use
|
|
604
732
|
|
|
605
733
|
```bash
|
|
@@ -663,6 +791,9 @@ print(result.stdout)
|
|
|
663
791
|
| `CDPILOT_OFFSCREEN` | `0` | Headed but render off-screen — no window steals focus |
|
|
664
792
|
| `CDPILOT_TIMEOUT` | unset | Default `--timeout` in seconds for every command (flag wins, `0` disables); expiry exits 124 |
|
|
665
793
|
| `CDPILOT_NO_AUTOLAUNCH` | `0` | `1` = page commands fail with the old "Is the browser running?" error instead of launching the browser |
|
|
794
|
+
| `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 |
|
|
795
|
+
| `CDPILOT_LOG` | `1` | `0` = do not write the session log (`cdpilot log`) |
|
|
796
|
+
| `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) |
|
|
666
797
|
|
|
667
798
|
## How It Works
|
|
668
799
|
|
|
@@ -823,10 +954,10 @@ The only browser MCP with built-in test assertions. Here's what we've shipped an
|
|
|
823
954
|
- [x] **Data extraction** (`extract`) — structured DOM data in text, JSON, or list format
|
|
824
955
|
- [x] **Page observation** (`observe`) — list all interactive elements with available actions
|
|
825
956
|
- [x] **Script runner** (`run`) — execute `.cdp` script files with pass/fail reporting
|
|
957
|
+
- [x] **iframe targeting** — `"iframe#card >>> input"` / `--frame` for element commands, same-origin and cross-origin (out-of-process) frames; smart commands search frames automatically
|
|
826
958
|
|
|
827
959
|
### Coming Soon
|
|
828
960
|
|
|
829
|
-
- [ ] **iframe** support — interact with elements inside iframes (Shadow DOM traversal already shipped in smart commands)
|
|
830
961
|
- [ ] **Multi-instance pool** (`CDPILOT_POOL_SIZE`) — N independent browser processes with least-loaded dispatch
|
|
831
962
|
- [ ] **Session recording & replay** — record browser sessions and replay them deterministically
|
|
832
963
|
- [ ] **Stealth mode** *(Pro)* — human-like mouse/typing, anti-fingerprint, CAPTCHA solving
|
package/bin/cdpilot.js
CHANGED
|
@@ -242,7 +242,8 @@ function runStatus() {
|
|
|
242
242
|
console.log(` ✓ Connected`);
|
|
243
243
|
console.log(` Browser: ${info.Browser || 'Unknown'}`);
|
|
244
244
|
console.log(` Protocol: ${info['Protocol-Version'] || 'Unknown'}`);
|
|
245
|
-
console.log(` WebSocket: ${info.webSocketDebuggerUrl || 'N/A'}
|
|
245
|
+
console.log(` WebSocket: ${info.webSocketDebuggerUrl || 'N/A'}`);
|
|
246
|
+
console.log(` ${idleCloseLabel(port)}\n`);
|
|
246
247
|
} catch {
|
|
247
248
|
console.log(' ✓ CDP responding but version info unavailable\n');
|
|
248
249
|
}
|
|
@@ -262,6 +263,33 @@ function runStatus() {
|
|
|
262
263
|
}
|
|
263
264
|
}
|
|
264
265
|
|
|
266
|
+
// Idle auto-close countdown, from the files src/cdpilot.py keeps (see
|
|
267
|
+
// _idle_status there): CDPILOT_HOME/idle/<port>.json names the watcher and its
|
|
268
|
+
// minutes, CDPILOT_HOME/projects/<id>/last-activity the last command.
|
|
269
|
+
function idleCloseLabel(port) {
|
|
270
|
+
const home = process.env.CDPILOT_HOME || path.join(os.homedir(), '.cdpilot');
|
|
271
|
+
try {
|
|
272
|
+
const st = JSON.parse(fs.readFileSync(path.join(home, 'idle', `${port}.json`), 'utf-8'));
|
|
273
|
+
const minutes = Number(st.minutes) || 0;
|
|
274
|
+
if (minutes <= 0 || !st.pid) return 'idle close off';
|
|
275
|
+
try {
|
|
276
|
+
process.kill(st.pid, 0); // existence probe; safe on Windows in Node
|
|
277
|
+
} catch (e) {
|
|
278
|
+
if (e.code !== 'EPERM') return 'idle close off';
|
|
279
|
+
}
|
|
280
|
+
let last = Number(st.started) || Date.now() / 1000;
|
|
281
|
+
try {
|
|
282
|
+
const file = path.join(home, 'projects', String(st.project_id), 'last-activity');
|
|
283
|
+
const t = parseFloat(fs.readFileSync(file, 'utf-8'));
|
|
284
|
+
if (t > last) last = t;
|
|
285
|
+
} catch {}
|
|
286
|
+
const left = Math.max(0, Math.floor(minutes * 60 - (Date.now() / 1000 - last)));
|
|
287
|
+
return `idle close in ${Math.ceil(left / 60)}m`;
|
|
288
|
+
} catch {
|
|
289
|
+
return 'idle close off';
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
265
293
|
// ── Version ──
|
|
266
294
|
|
|
267
295
|
function showVersion() {
|
|
@@ -329,10 +357,14 @@ function showHelp() {
|
|
|
329
357
|
(the flag wins, 0 disables).
|
|
330
358
|
Page commands start the browser if it is not running
|
|
331
359
|
(set CDPILOT_NO_AUTOLAUNCH=1 to get an error instead).
|
|
360
|
+
An auto-launched (or MCP-launched) browser closes after 15 min without a
|
|
361
|
+
cdpilot command or page change (CDPILOT_IDLE_CLOSE=<minutes>,
|
|
362
|
+
0 = never); an explicit launch stays open unless asked (below).
|
|
332
363
|
|
|
333
364
|
SETUP
|
|
334
365
|
setup Auto-detect browser, create isolated profile
|
|
335
|
-
launch
|
|
366
|
+
launch [--idle-close <min>] Start browser with CDP enabled (--idle-close or
|
|
367
|
+
CDPILOT_IDLE_CLOSE: close it after <min> idle minutes)
|
|
336
368
|
status Check browser connection
|
|
337
369
|
stop [--smart] Stop browser (--smart = close owned tabs, quit if empty)
|
|
338
370
|
close [--force|--keep] Smart close: close cdpilot's tabs; quit browser only
|
|
@@ -352,6 +384,14 @@ function showHelp() {
|
|
|
352
384
|
submit <form> Submit form
|
|
353
385
|
hover <sel> Hover element
|
|
354
386
|
keys <combo> Keyboard shortcut
|
|
387
|
+
Inside an iframe: click "iframe#card >>> input[name=cardnumber]"
|
|
388
|
+
(nest: "iframe.a >>> iframe.b >>> button") or
|
|
389
|
+
--frame <selector|index|url-substring>; same-origin and
|
|
390
|
+
cross-origin frames. With no match in the page,
|
|
391
|
+
smart-click searches all frames, smart-fill and
|
|
392
|
+
smart-select only frames of the page's origin.
|
|
393
|
+
frame list|eval [--frame <f>]
|
|
394
|
+
List iframes / run JS inside a frame
|
|
355
395
|
|
|
356
396
|
DEBUGGING
|
|
357
397
|
console [url] Capture console logs
|
|
@@ -430,6 +470,16 @@ function showHelp() {
|
|
|
430
470
|
AI AGENT
|
|
431
471
|
mcp Start MCP server (stdin/stdout JSON-RPC)
|
|
432
472
|
|
|
473
|
+
SESSION LOG (always on, local, redacted)
|
|
474
|
+
log Today's commands for this project: time, exit, command,
|
|
475
|
+
url, result
|
|
476
|
+
log --md Markdown report (pages visited, actions, errors, files
|
|
477
|
+
produced) to paste into an issue or PR
|
|
478
|
+
log --json Raw JSON lines · log --days N · log --path (log directory)
|
|
479
|
+
Typed values, secret-looking args and token/key/secret URL
|
|
480
|
+
params are redacted. CDPILOT_LOG=0 turns it off;
|
|
481
|
+
CDPILOT_LOG_DAYS (default 14) sets how many days are kept.
|
|
482
|
+
|
|
433
483
|
WATCH (continuous screencast for AI video understanding)
|
|
434
484
|
watch start <url> Begin JPEG screencast at N fps to a disk ring buffer
|
|
435
485
|
(default 10fps, 5min retention, 100MB cap). Background
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cdpilot",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.3",
|
|
4
4
|
"mcpName": "io.github.mehmetnadir/cdpilot",
|
|
5
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.",
|
|
6
6
|
"bin": {
|