cdpilot 0.9.1 → 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
@@ -124,7 +124,7 @@ See [CHANGELOG.md](CHANGELOG.md) for the full list with rationale.
124
124
  ### Navigation & Content
125
125
 
126
126
  ```bash
127
- cdpilot go <url> # Navigate to URL
127
+ cdpilot go <url> # Navigate to URL (alias: open <url>)
128
128
  cdpilot content # Get page text content
129
129
  cdpilot html # Get page HTML
130
130
  cdpilot shot [file] # Take screenshot (PNG)
@@ -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
  ```
@@ -537,17 +608,32 @@ cdpilot profile warm # age the profile for reCAPTCHA v3 score
537
608
  which nudges reCAPTCHA v3's behavioral score upward over time. Slow by
538
609
  design — run it ahead of a session, not inline.
539
610
 
540
- Verified against public bot-detection panels:
541
- - **bot.sannysoft.com:** 24/24 PASS (WebDriver, Chrome obj, Plugins as PluginArray, WebGL, PHANTOM_*, HEADCHR_*, SELENIUM_DRIVER)
542
- - **bot.incolumitas.com** intoli: 6/6 OK — new-tests: 6/7 OK (one FAIL = pure CDP presence, cannot be JS-patched)
543
- - **nowsecure.nl** (Cloudflare full challenge): passed
544
- - **arh.antoinevastel.com/areyouheadless:** "You are not Chrome headless"
611
+ Public bot-detection panels, measured 2026-09-27, **headless** Brave (Chrome 154)
612
+ ([method and raw results](.claude/docs/stealth-panel-olcumu-2026-09-27.md)):
613
+
614
+ | `mode` | bot.sannysoft.com (31 rows) | incolumitas intoli | incolumitas fpscanner | incolumitas new-tests |
615
+ |---|---|---|---|---|
616
+ | `regular` (no patches, default) | 28 pass / 3 fail | 5/6 | 17 ok / 3 fail / 1 warn | all ok |
617
+ | `stealth` | 31 pass | 6/6 | 19 ok / 1 fail / 1 warn | all ok |
618
+ | `undetected` | 31 pass | 6/6 | 19 ok / 1 fail / 1 warn | all ok |
619
+
620
+ - The remaining fpscanner fail is `'webdriver' in navigator`, which is true in every modern
621
+ Chrome; faking it away would make cdpilot the odd one out.
622
+ - `regular`'s fails all come from the `HeadlessChrome` user agent. `stealth`/`undetected`
623
+ rewrite it for the page load that `go` starts; a later, separate command in the same page
624
+ can still read `HeadlessChrome`.
625
+ - new-tests: every key is ok; `connectionRTT` is reported as unknown in all tiers.
626
+ - nowsecure.nl and areyouheadless could not be measured (a fixed always-interactive
627
+ Turnstile test key; HTTP 502).
628
+
629
+ Earlier figures on this page (sannysoft 24/24, intoli 6/6) were measured on v0.4.x in April
630
+ 2026; they are replaced by the table above.
545
631
 
546
632
  ### Reliability
547
633
 
548
634
  ```bash
549
635
  cdpilot browser [name|auto] # workload-aware browser selection
550
- cdpilot health # JSON: alive, port, tabs, browser, today's crashes
636
+ cdpilot health # JSON: alive, port, tabs, browser, today's crashes, idle close
551
637
  ```
552
638
 
553
639
  `cdpilot health` is designed for shell watchdogs:
@@ -559,6 +645,89 @@ until cdpilot health >/dev/null; do cdpilot launch; sleep 2; done
559
645
  Surfaces today's Brave crash count from `~/Library/Logs/DiagnosticReports/`
560
646
  on macOS — spot degradation before your automation silently stalls.
561
647
 
648
+ **Auto-launch.** A page command (`go`, `content`, `click`, `shot`, …) that finds
649
+ the browser not running starts it the same way `cdpilot launch` does (same
650
+ profile, same headless/visible setting), then carries on, printing one line to
651
+ stderr: `cdpilot: browser was not running — launched it (CDPILOT_NO_AUTOLAUNCH=1 to disable)`.
652
+ Lifecycle, status and configuration commands (`launch`, `stop`, `close`,
653
+ `close-tab`, `stop-all`, `project-stop`, `session-close`, `status`, `health`,
654
+ `tabs`, `sessions`, `projects`, `headless`, `proxy`, `browser`, `extensions`,
655
+ `mcp`, `serve`, …) never launch. `CDPILOT_NO_AUTOLAUNCH=1` restores the old
656
+ "CDP connection error. Is the browser running?" error and exit code 1.
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
+
685
+ **Timeouts.** Any command takes `--timeout <seconds>`, before or after the
686
+ command name, or a default from `CDPILOT_TIMEOUT` (the flag wins; `0`
687
+ disables). It bounds the whole command's wall-clock: on expiry cdpilot prints
688
+ `cdpilot: timed out after <N>s (<command>)` to stderr, kills the child processes
689
+ it started and exits with code **124** (like GNU `timeout`). A browser that was
690
+ already up and registered is left running.
691
+
692
+ ```bash
693
+ cdpilot --timeout 10 click "#submit" # before the command...
694
+ cdpilot shot page.png --timeout 30 # ...or after it
695
+ CDPILOT_TIMEOUT=60 cdpilot run flow.cdp # default for every command (and each script line)
696
+ ```
697
+
698
+ For `mcp` and `serve` the value is not applied to the long-running server
699
+ itself; it is passed on to every tool call / request it runs.
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
+
562
731
  ### Scaling & Workstation Use
563
732
 
564
733
  ```bash
@@ -620,6 +789,11 @@ print(result.stdout)
620
789
  | `BROWSER_SESSION` | Auto | Session identifier |
621
790
  | `CDPILOT_MODE` | `regular` | Stealth tier override (`regular`/`stealth`/`undetected`) |
622
791
  | `CDPILOT_OFFSCREEN` | `0` | Headed but render off-screen — no window steals focus |
792
+ | `CDPILOT_TIMEOUT` | unset | Default `--timeout` in seconds for every command (flag wins, `0` disables); expiry exits 124 |
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) |
623
797
 
624
798
  ## How It Works
625
799
 
@@ -780,10 +954,10 @@ The only browser MCP with built-in test assertions. Here's what we've shipped an
780
954
  - [x] **Data extraction** (`extract`) — structured DOM data in text, JSON, or list format
781
955
  - [x] **Page observation** (`observe`) — list all interactive elements with available actions
782
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
783
958
 
784
959
  ### Coming Soon
785
960
 
786
- - [ ] **iframe** support — interact with elements inside iframes (Shadow DOM traversal already shipped in smart commands)
787
961
  - [ ] **Multi-instance pool** (`CDPILOT_POOL_SIZE`) — N independent browser processes with least-loaded dispatch
788
962
  - [ ] **Session recording & replay** — record browser sessions and replay them deterministically
789
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() {
@@ -321,18 +349,29 @@ function showHelp() {
321
349
  cdpilot v${VERSION} — Zero-dependency browser automation
322
350
 
323
351
  USAGE
324
- cdpilot <command> [args]
352
+ cdpilot [--timeout <s>] <command> [args]
353
+
354
+ GLOBAL OPTIONS
355
+ --timeout <s> Abort the command after <s> seconds (exit 124). Before or
356
+ after the command; env CDPILOT_TIMEOUT sets a default
357
+ (the flag wins, 0 disables).
358
+ Page commands start the browser if it is not running
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).
325
363
 
326
364
  SETUP
327
365
  setup Auto-detect browser, create isolated profile
328
- 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)
329
368
  status Check browser connection
330
369
  stop [--smart] Stop browser (--smart = close owned tabs, quit if empty)
331
370
  close [--force|--keep] Smart close: close cdpilot's tabs; quit browser only
332
371
  if no user tabs remain (--force quits anyway, --keep never quits)
333
372
 
334
373
  NAVIGATION
335
- go <url> Navigate to URL
374
+ go <url> Navigate to URL (alias: open)
336
375
  content Get page text content
337
376
  html Get page HTML
338
377
  shot [file] Take screenshot
@@ -345,6 +384,14 @@ function showHelp() {
345
384
  submit <form> Submit form
346
385
  hover <sel> Hover element
347
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
348
395
 
349
396
  DEBUGGING
350
397
  console [url] Capture console logs
@@ -423,6 +470,16 @@ function showHelp() {
423
470
  AI AGENT
424
471
  mcp Start MCP server (stdin/stdout JSON-RPC)
425
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
+
426
483
  WATCH (continuous screencast for AI video understanding)
427
484
  watch start <url> Begin JPEG screencast at N fps to a disk ring buffer
428
485
  (default 10fps, 5min retention, 100MB cap). Background
@@ -590,8 +647,23 @@ if (process.argv.includes('--internal-test-runner')) {
590
647
  return; // runAll() is async, this exits via process.exit
591
648
  }
592
649
 
650
+ // Global flags may precede the command name (`cdpilot --timeout 10 click "#x"`).
651
+ // src/cdpilot.py parses and validates them (it is also run directly, without
652
+ // this launcher); here they only have to be skipped to find the command that
653
+ // the launcher answers itself (help / --version / setup / status). args are
654
+ // passed to Python unchanged.
655
+ function commandIndex(argv) {
656
+ let i = 0;
657
+ while (i < argv.length) {
658
+ if (argv[i] === '--timeout') { i += 2; continue; }
659
+ if (argv[i].startsWith('--timeout=')) { i += 1; continue; }
660
+ break;
661
+ }
662
+ return i;
663
+ }
664
+
593
665
  const args = process.argv.slice(2);
594
- const cmd = args[0];
666
+ const cmd = args[commandIndex(args)];
595
667
 
596
668
  if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
597
669
  showHelp();
@@ -650,7 +722,14 @@ if (cmd === 'status') {
650
722
  env,
651
723
  });
652
724
 
653
- child.on('close', (code) => {
654
- process.exit(code || 0);
725
+ // A child killed by a signal reports code === null. `code || 0` turned that
726
+ // into success, so an OOM-killed or SIGKILLed run looked fine to scripts
727
+ // and agents. Use the shell convention 128 + signal number instead.
728
+ child.on('close', (code, signal) => {
729
+ if (code === null && signal) {
730
+ console.error(`cdpilot: python was terminated by ${signal}`);
731
+ process.exit(128 + (os.constants.signals[signal] || 0));
732
+ }
733
+ process.exit(code ?? 0);
655
734
  });
656
735
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cdpilot",
3
- "version": "0.9.1",
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": {