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 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 (`&nbsp;` 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'}\n`);
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 Start browser with CDP enabled
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.2",
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": {