cdpilot 0.5.0 → 0.9.0

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.
Files changed (4) hide show
  1. package/README.md +306 -9
  2. package/bin/cdpilot.js +225 -11
  3. package/package.json +47 -9
  4. package/src/cdpilot.py +8654 -2116
package/bin/cdpilot.js CHANGED
@@ -69,11 +69,20 @@ function findBrowser() {
69
69
  // ── Python Detection ──
70
70
 
71
71
  function findPython() {
72
- for (const cmd of ['python3', 'python']) {
72
+ // cdpilot.py uses PEP 604 unions (dict | None) → requires Python 3.10+.
73
+ // Prefer version-specific + Homebrew interpreters so a stale default python3
74
+ // (e.g. macOS 3.9) is skipped instead of selected and crashing at runtime.
75
+ const candidates = [
76
+ process.env.CDPILOT_PYTHON,
77
+ 'python3.13', 'python3.12', 'python3.11', 'python3.10',
78
+ '/opt/homebrew/bin/python3', '/usr/local/bin/python3',
79
+ 'python3', 'python',
80
+ ].filter(Boolean);
81
+ for (const cmd of candidates) {
73
82
  try {
74
83
  const ver = execSync(`${cmd} --version 2>&1`, { stdio: 'pipe' }).toString().trim();
75
84
  const match = ver.match(/(\d+)\.(\d+)/);
76
- if (match && parseInt(match[1]) >= 3 && parseInt(match[2]) >= 8) {
85
+ if (match && (parseInt(match[1]) > 3 || (parseInt(match[1]) === 3 && parseInt(match[2]) >= 10))) {
77
86
  return cmd;
78
87
  }
79
88
  } catch {}
@@ -81,6 +90,21 @@ function findPython() {
81
90
  return null;
82
91
  }
83
92
 
93
+ // ── Python websockets Detection ──
94
+
95
+ function findWebsockets() {
96
+ // Import via the interpreter cdpilot actually selects — pip metadata can
97
+ // point at a different Python than findPython() resolves.
98
+ const python = findPython();
99
+ if (!python) return null;
100
+ try {
101
+ const out = execSync(`${python} -c "import websockets; print(getattr(websockets, '__version__', 'installed'))"`, { stdio: 'pipe' }).toString().trim();
102
+ return out || 'installed';
103
+ } catch {
104
+ return null;
105
+ }
106
+ }
107
+
84
108
  // ── Setup Command ──
85
109
 
86
110
  function runSetup() {
@@ -88,11 +112,12 @@ function runSetup() {
88
112
  const config = resolveProjectConfig();
89
113
 
90
114
  console.log('\n cdpilot setup\n');
91
- console.log(` Browser: ${browser || '❌ Not found'}`);
92
- console.log(` Profile: ${config.profileDir}`);
93
- console.log(` CDP Port: ${config.port === '0' ? 'auto' : config.port}`);
94
- console.log(` Project: ${config.projectId || 'manual mode'}`);
95
- console.log(` Python: ${findPython() || '❌ Not found'}`);
115
+ console.log(` Browser: ${browser || '❌ Not found'}`);
116
+ console.log(` Profile: ${config.profileDir}`);
117
+ console.log(` CDP Port: ${config.port === '0' ? 'auto' : config.port}`);
118
+ console.log(` Project: ${config.projectId || 'manual mode'}`);
119
+ console.log(` Python: ${findPython() || '❌ Not found'}`);
120
+ console.log(` websockets: ${findWebsockets() || '❌ Not found'}`);
96
121
 
97
122
  if (!browser) {
98
123
  console.log('\n ❌ No compatible browser found.');
@@ -102,11 +127,17 @@ function runSetup() {
102
127
  }
103
128
 
104
129
  if (!findPython()) {
105
- console.log('\n ❌ Python 3.8+ not found.');
130
+ console.log('\n ❌ Python 3.10+ not found.');
106
131
  console.log(' Install: https://www.python.org/downloads/\n');
107
132
  process.exit(1);
108
133
  }
109
134
 
135
+ if (!findWebsockets()) {
136
+ console.log('\n ❌ Python websockets not found.');
137
+ console.log(' Install: pip install websockets\n');
138
+ process.exit(1);
139
+ }
140
+
110
141
  // Create profile directory
111
142
  if (!fs.existsSync(config.profileDir)) {
112
143
  fs.mkdirSync(config.profileDir, { recursive: true });
@@ -147,7 +178,7 @@ function preflight() {
147
178
  const ver = execSync(`${python} --version 2>&1`, { stdio: 'pipe' }).toString().trim();
148
179
  console.log(` ✓ ${ver}`);
149
180
  } else {
150
- console.log(' ✗ Python 3.8+ not found');
181
+ console.log(' ✗ Python 3.10+ not found');
151
182
  console.log(' → Install: https://www.python.org/downloads/\n');
152
183
  process.exit(1);
153
184
  }
@@ -296,7 +327,9 @@ function showHelp() {
296
327
  setup Auto-detect browser, create isolated profile
297
328
  launch Start browser with CDP enabled
298
329
  status Check browser connection
299
- stop Stop browser
330
+ stop [--smart] Stop browser (--smart = close owned tabs, quit if empty)
331
+ close [--force|--keep] Smart close: close cdpilot's tabs; quit browser only
332
+ if no user tabs remain (--force quits anyway, --keep never quits)
300
333
 
301
334
  NAVIGATION
302
335
  go <url> Navigate to URL
@@ -347,9 +380,29 @@ function showHelp() {
347
380
  (Address a context's tab in subsequent commands via CDPILOT_TARGET=<tgt-id>)
348
381
 
349
382
  STEALTH & CAPTCHA
350
- stealth [on|off] Toggle anti-fingerprint patches (opt-in)
383
+ mode [regular|stealth|undetected]
384
+ Three-tier stealth (crawl4ai-style). regular = no patch
385
+ (cleanest, default); stealth = light patch (webdriver/
386
+ chrome.runtime/permissions); undetected = full patch
387
+ (+ plugins + WebGL + Worker). Adaptive auto-escalates.
388
+ stealth [on|off] Legacy binary toggle (on -> undetected tier)
351
389
  captcha-check Detect CAPTCHA on active page (JSON output)
352
390
  captcha-wait [s] Pause until user solves CAPTCHA (default 300s)
391
+ captcha-solve [--provider P]
392
+ Solve Amazon classic image CAPTCHA (opt-in). amazon-local
393
+ (optional amazoncaptcha lib) or BYOK capsolver/2captcha.
394
+ Auto-routes PerimeterX 'Press & Hold' to press-hold.
395
+ press-hold [selector]
396
+ Solve a PerimeterX/HUMAN 'Press & Hold' challenge with a
397
+ humanized press->hold(jitter)->release gesture (no token,
398
+ no provider). Auto-finds #px-captcha if no selector given.
399
+ friction Detect highest anti-bot rung (none/rate_limited/
400
+ soft_captcha/login_wall/otp_sms/hard_block) + policy.
401
+ rate_limit auto-backoff in 'go'; login/OTP/block = human
402
+ handoff (no autonomous bypass). Env: CDPILOT_FRICTION_BACKOFF,
403
+ CDPILOT_FRICTION_MAX_RETRY.
404
+ profile warm [--minutes N]
405
+ Age cookies/history on safe sites to boost reCAPTCHA v3 score.
353
406
  adaptive [on|off] Auto-escalate to stealth on hosts that show CAPTCHA.
354
407
  Remembers per-host. Use 'adaptive forget <host>' to reset.
355
408
  cookies save <file> [<dom>]
@@ -370,12 +423,173 @@ function showHelp() {
370
423
  AI AGENT
371
424
  mcp Start MCP server (stdin/stdout JSON-RPC)
372
425
 
426
+ WATCH (continuous screencast for AI video understanding)
427
+ watch start <url> Begin JPEG screencast at N fps to a disk ring buffer
428
+ (default 10fps, 5min retention, 100MB cap). Background
429
+ daemon — command returns immediately.
430
+ watch query --at MM:SS --window 5s
431
+ Return JSON list of frame paths around a video time.
432
+ watch query --last 5s | --since-last
433
+ Recent frames or everything new since the last query.
434
+ watch status Daemon state, frame count, disk usage.
435
+ watch stop Stop daemon + clean up frames (--keep-frames to retain).
436
+ watch ask "<q>" Tiny NL parser: extracts time window from a question.
437
+
373
438
  More: https://github.com/mehmetnadir/cdpilot#commands
374
439
  `);
375
440
  }
376
441
 
442
+ // ── Internal Test Runner ──
443
+
444
+ function runInternalTestRunner(testFile, traceDir, traceMode, grepPattern) {
445
+ if (traceDir) {
446
+ fs.mkdirSync(path.join(traceDir, 'screenshots'), { recursive: true });
447
+ fs.mkdirSync(path.join(traceDir, 'a11y'), { recursive: true });
448
+ }
449
+
450
+ const metaPath = traceDir ? path.join(traceDir, 'meta.json') : null;
451
+ const stepsPath = traceDir && traceMode !== 'off' ? path.join(traceDir, 'steps.jsonl') : null;
452
+
453
+ const meta = { name: path.basename(testFile), started_at: new Date().toISOString(), status: 'running', tests: [] };
454
+ if (metaPath) fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2));
455
+
456
+ const testQueue = [];
457
+ global.test = (name, fn) => testQueue.push({ name, fn });
458
+
459
+ try {
460
+ require(path.resolve(testFile));
461
+ } catch (err) {
462
+ const out = { passed: 0, failed: 1, skipped: 0, tests: [{ name: testFile, status: 'failed', duration_ms: 0, error: err.message }] };
463
+ process.stdout.write(JSON.stringify(out) + '\n');
464
+ process.exit(1);
465
+ }
466
+
467
+ const results = { passed: 0, failed: 0, skipped: 0, tests: [] };
468
+ const cdpPort = process.env.CDP_PORT || '9222';
469
+ const SCRIPT = path.join(__dirname, '..', 'src', 'cdpilot.py');
470
+ const python = findPython() || 'python3';
471
+ let stepIdx = 0;
472
+
473
+ const makeT = () => {
474
+ const runCmd = (cmd, ...cargs) => {
475
+ const padded = String(stepIdx).padStart(3, '0');
476
+ const step = { action: cmd + ' ' + cargs.join(' '), ts_ms: Date.now(), duration_ms: 0, error: null };
477
+ const t0 = Date.now();
478
+ try {
479
+ const quoted = cargs.map(a => JSON.stringify(String(a))).join(' ');
480
+ execSync(`${python} ${SCRIPT} ${cmd} ${quoted}`, {
481
+ stdio: 'pipe',
482
+ env: { ...process.env, CDP_PORT: cdpPort },
483
+ timeout: 30000,
484
+ });
485
+ step.duration_ms = Date.now() - t0;
486
+ if (stepsPath) fs.appendFileSync(stepsPath, JSON.stringify(step) + '\n');
487
+ // Screenshot after each step (best-effort — no browser = skipped)
488
+ if (traceDir && traceMode !== 'off') {
489
+ try {
490
+ const shotPath = path.join(traceDir, 'screenshots', `step-${padded}.png`);
491
+ execSync(`${python} ${SCRIPT} shot ${shotPath}`, { stdio: 'pipe', env: { ...process.env, CDP_PORT: cdpPort }, timeout: 10000 });
492
+ } catch (_) { /* no browser is OK in unit-style tests */ }
493
+ }
494
+ stepIdx++;
495
+ } catch (err) {
496
+ step.duration_ms = Date.now() - t0;
497
+ step.error = err.stderr ? err.stderr.toString().trim() : err.message;
498
+ if (stepsPath) fs.appendFileSync(stepsPath, JSON.stringify(step) + '\n');
499
+ stepIdx++;
500
+ throw new Error(`${cmd} failed: ${step.error}`);
501
+ }
502
+ };
503
+
504
+ const t = {
505
+ goto: (url) => runCmd('go', url),
506
+ click: (sel) => runCmd('click', sel),
507
+ fill: (sel, val) => runCmd('fill', sel, val),
508
+ type: (sel, val) => runCmd('type', sel, val),
509
+ hover: (sel) => runCmd('hover', sel),
510
+ screenshot: (p) => runCmd('shot', p),
511
+ eval: (js) => {
512
+ try {
513
+ const out = execSync(`${python} ${SCRIPT} eval ${JSON.stringify(js)}`, { stdio: 'pipe', env: { ...process.env, CDP_PORT: cdpPort }, timeout: 10000 });
514
+ return out.toString().trim();
515
+ } catch (e) { throw new Error('eval failed: ' + e.message); }
516
+ },
517
+ a11y: () => runCmd('a11y-snapshot'),
518
+ };
519
+
520
+ t.expect = (textOrSel) => runCmd('assert', textOrSel);
521
+ t.expect.url = (expected) => runCmd('assert-url', expected);
522
+ t.expect.visible = (sel) => runCmd('assert-visible', sel);
523
+ t.expect.hidden = (sel) => runCmd('assert-hidden', sel);
524
+
525
+ return t;
526
+ };
527
+
528
+ // Run tests sequentially (parallel is managed at the Python level across files)
529
+ const runAll = async () => {
530
+ for (const tst of testQueue) {
531
+ if (grepPattern && !tst.name.match(new RegExp(grepPattern, 'i'))) {
532
+ results.skipped++;
533
+ continue;
534
+ }
535
+ const t0 = Date.now();
536
+ const rec = { name: tst.name, status: 'passed', duration_ms: 0, error: null };
537
+ try {
538
+ await tst.fn(makeT());
539
+ rec.status = 'passed';
540
+ results.passed++;
541
+ } catch (err) {
542
+ rec.status = 'failed';
543
+ rec.error = err.message;
544
+ results.failed++;
545
+ }
546
+ rec.duration_ms = Date.now() - t0;
547
+ results.tests.push(rec);
548
+ if (metaPath) {
549
+ meta.tests.push(rec);
550
+ meta.status = 'running';
551
+ fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2));
552
+ }
553
+ }
554
+
555
+ // Finalize meta
556
+ if (metaPath) {
557
+ meta.status = results.failed > 0 ? 'failed' : 'passed';
558
+ meta.passed = results.passed;
559
+ meta.failed = results.failed;
560
+ meta.skipped = results.skipped;
561
+ meta.ended_at = new Date().toISOString();
562
+ fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2));
563
+ }
564
+
565
+ process.stdout.write(JSON.stringify(results) + '\n');
566
+ process.exit(results.failed > 0 ? 1 : 0);
567
+ };
568
+
569
+ runAll().catch(err => {
570
+ process.stderr.write('Test runner error: ' + err.message + '\n');
571
+ process.exit(1);
572
+ });
573
+ }
574
+
377
575
  // ── Main ──
378
576
 
577
+ // Internal test runner mode — intercept before normal CLI dispatch
578
+ if (process.argv.includes('--internal-test-runner')) {
579
+ const idx = process.argv.indexOf('--internal-test-runner');
580
+ const testFile = process.argv[idx + 1];
581
+ const traceDirArg = process.argv.find(a => a.startsWith('--trace-dir='));
582
+ const traceArg = process.argv.find(a => a.startsWith('--trace='));
583
+ const grepArg = process.argv.find(a => a.startsWith('--grep='));
584
+ runInternalTestRunner(
585
+ testFile,
586
+ traceDirArg ? traceDirArg.split('=').slice(1).join('=') : null,
587
+ traceArg ? traceArg.split('=')[1] : 'default',
588
+ grepArg ? grepArg.split('=').slice(1).join('=') : null,
589
+ );
590
+ return; // runAll() is async, this exits via process.exit
591
+ }
592
+
379
593
  const args = process.argv.slice(2);
380
594
  const cmd = args[0];
381
595
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cdpilot",
3
- "version": "0.5.0",
4
- "description": "Zero-dependency browser automation from your terminal. One command, full control.",
3
+ "version": "0.9.0",
4
+ "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.",
5
5
  "bin": {
6
6
  "cdpilot": "./bin/cdpilot.js",
7
7
  "bctl": "./bin/cdpilot.js"
@@ -11,12 +11,45 @@
11
11
  "start": "node bin/cdpilot.js"
12
12
  },
13
13
  "keywords": [
14
- "browser-automation", "cdp", "chrome-devtools-protocol", "cli",
15
- "mcp", "ai-agent", "web-scraping", "zero-dependency",
16
- "headless-browser", "puppeteer-alternative", "playwright-alternative",
17
- "screenshot", "pdf", "automation", "devtools", "browser",
18
- "model-context-protocol", "claude", "brave", "testing",
19
- "web-testing", "accessibility", "a11y"
14
+ "browser-automation",
15
+ "cdp",
16
+ "chrome-devtools-protocol",
17
+ "cli",
18
+ "mcp",
19
+ "ai-agent",
20
+ "web-scraping",
21
+ "zero-dependency",
22
+ "headless-browser",
23
+ "puppeteer-alternative",
24
+ "playwright-alternative",
25
+ "screenshot",
26
+ "pdf",
27
+ "automation",
28
+ "devtools",
29
+ "browser",
30
+ "model-context-protocol",
31
+ "claude",
32
+ "brave",
33
+ "testing",
34
+ "web-testing",
35
+ "accessibility",
36
+ "a11y",
37
+ "stealth",
38
+ "captcha-solver",
39
+ "anti-bot",
40
+ "cloudflare-bypass",
41
+ "datadome",
42
+ "adaptive-escalation",
43
+ "proxy-rotation",
44
+ "tls-fingerprint",
45
+ "cookie-management",
46
+ "video-understanding",
47
+ "screencast",
48
+ "shadow-dom",
49
+ "perimeterx",
50
+ "press-and-hold",
51
+ "progressive-resilience",
52
+ "multi-instance"
20
53
  ],
21
54
  "author": "Mehmet Nadir",
22
55
  "license": "MIT",
@@ -32,5 +65,10 @@
32
65
  "src/cdpilot.py",
33
66
  "README.md",
34
67
  "LICENSE"
35
- ]
68
+ ],
69
+ "devDependencies": {
70
+ "gray-matter": "^4.0.3",
71
+ "remark": "^15.0.1",
72
+ "remark-html": "^16.0.1"
73
+ }
36
74
  }