@mseep/cdpilot 0.8.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.
package/bin/cdpilot.js ADDED
@@ -0,0 +1,625 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * cdpilot — Zero-dependency browser automation CLI
5
+ * Entry point: detects Python, finds browser, delegates to cdpilot.py
6
+ */
7
+
8
+ const { execSync, spawn } = require('child_process');
9
+ const path = require('path');
10
+ const fs = require('fs');
11
+ const os = require('os');
12
+
13
+ const SCRIPT = path.join(__dirname, '..', 'src', 'cdpilot.py');
14
+ const VERSION = require('../package.json').version;
15
+
16
+ // ── Browser Detection ──
17
+
18
+ function findBrowser() {
19
+ // User override
20
+ if (process.env.CHROME_BIN) {
21
+ if (fs.existsSync(process.env.CHROME_BIN)) return process.env.CHROME_BIN;
22
+ }
23
+
24
+ const platform = os.platform();
25
+ const candidates = [];
26
+
27
+ if (platform === 'darwin') {
28
+ candidates.push(
29
+ '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
30
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
31
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
32
+ );
33
+ } else if (platform === 'linux') {
34
+ candidates.push(
35
+ 'brave-browser',
36
+ 'brave',
37
+ 'google-chrome',
38
+ 'google-chrome-stable',
39
+ 'chromium-browser',
40
+ 'chromium',
41
+ );
42
+ } else if (platform === 'win32') {
43
+ const programFiles = process.env['PROGRAMFILES'] || 'C:\\Program Files';
44
+ const programFilesX86 = process.env['PROGRAMFILES(X86)'] || 'C:\\Program Files (x86)';
45
+ const localAppData = process.env.LOCALAPPDATA || '';
46
+ candidates.push(
47
+ path.join(programFiles, 'BraveSoftware', 'Brave-Browser', 'Application', 'brave.exe'),
48
+ path.join(programFilesX86, 'BraveSoftware', 'Brave-Browser', 'Application', 'brave.exe'),
49
+ path.join(localAppData, 'BraveSoftware', 'Brave-Browser', 'Application', 'brave.exe'),
50
+ path.join(programFiles, 'Google', 'Chrome', 'Application', 'chrome.exe'),
51
+ path.join(programFilesX86, 'Google', 'Chrome', 'Application', 'chrome.exe'),
52
+ path.join(localAppData, 'Google', 'Chrome', 'Application', 'chrome.exe'),
53
+ );
54
+ }
55
+
56
+ for (const bin of candidates) {
57
+ if (bin.startsWith('/') || bin.includes('\\')) {
58
+ if (fs.existsSync(bin)) return bin;
59
+ } else {
60
+ try {
61
+ execSync(`which ${bin} 2>/dev/null`, { stdio: 'pipe' });
62
+ return bin;
63
+ } catch {}
64
+ }
65
+ }
66
+ return null;
67
+ }
68
+
69
+ // ── Python Detection ──
70
+
71
+ function findPython() {
72
+ for (const cmd of ['python3', 'python']) {
73
+ try {
74
+ const ver = execSync(`${cmd} --version 2>&1`, { stdio: 'pipe' }).toString().trim();
75
+ const match = ver.match(/(\d+)\.(\d+)/);
76
+ if (match && parseInt(match[1]) >= 3 && parseInt(match[2]) >= 8) {
77
+ return cmd;
78
+ }
79
+ } catch {}
80
+ }
81
+ return null;
82
+ }
83
+
84
+ // ── Setup Command ──
85
+
86
+ function runSetup() {
87
+ const browser = findBrowser();
88
+ const config = resolveProjectConfig();
89
+
90
+ 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'}`);
96
+
97
+ if (!browser) {
98
+ console.log('\n ❌ No compatible browser found.');
99
+ console.log(' Install Brave (recommended): https://brave.com/download/');
100
+ console.log(' Or Google Chrome: https://www.google.com/chrome/\n');
101
+ process.exit(1);
102
+ }
103
+
104
+ if (!findPython()) {
105
+ console.log('\n ❌ Python 3.8+ not found.');
106
+ console.log(' Install: https://www.python.org/downloads/\n');
107
+ process.exit(1);
108
+ }
109
+
110
+ // Create profile directory
111
+ if (!fs.existsSync(config.profileDir)) {
112
+ fs.mkdirSync(config.profileDir, { recursive: true });
113
+ console.log(`\n ✓ Created profile: ${config.profileDir}`);
114
+ } else {
115
+ console.log(`\n ✓ Profile exists: ${config.profileDir}`);
116
+ }
117
+
118
+ console.log(' ✓ Setup complete! Run: cdpilot launch\n');
119
+ }
120
+
121
+ // ── Pre-flight Check (runs on first launch) ──
122
+
123
+ function checkWebsockets(python) {
124
+ try {
125
+ execSync(`${python} -c "import websockets"`, { stdio: 'pipe' });
126
+ return true;
127
+ } catch {
128
+ return false;
129
+ }
130
+ }
131
+
132
+ function preflight() {
133
+ const markerFile = path.join(os.homedir(), '.cdpilot', '.preflight-done');
134
+
135
+ // Skip if already passed (not first run) and all deps present
136
+ const python = findPython();
137
+ const browser = findBrowser();
138
+ if (fs.existsSync(markerFile) && python && browser && checkWebsockets(python)) {
139
+ return; // All good, skip silently
140
+ }
141
+
142
+ console.log(`\n cdpilot v${VERSION} — Pre-flight Check`);
143
+ console.log(' ' + '─'.repeat(35) + '\n');
144
+
145
+ // 1. Python
146
+ if (python) {
147
+ const ver = execSync(`${python} --version 2>&1`, { stdio: 'pipe' }).toString().trim();
148
+ console.log(` ✓ ${ver}`);
149
+ } else {
150
+ console.log(' ✗ Python 3.8+ not found');
151
+ console.log(' → Install: https://www.python.org/downloads/\n');
152
+ process.exit(1);
153
+ }
154
+
155
+ // 2. websockets
156
+ if (checkWebsockets(python)) {
157
+ console.log(' ✓ websockets');
158
+ } else {
159
+ console.log(' ✗ websockets — installing...');
160
+ try {
161
+ execSync(`${python} -m pip install websockets --quiet --disable-pip-version-check`, { stdio: 'pipe' });
162
+ if (checkWebsockets(python)) {
163
+ console.log(' ✓ websockets (installed)');
164
+ } else {
165
+ console.log(' ✗ websockets install failed');
166
+ console.log(' → Run manually: pip install websockets\n');
167
+ process.exit(1);
168
+ }
169
+ } catch {
170
+ console.log(' ✗ websockets auto-install failed');
171
+ console.log(' → Run manually: pip install websockets\n');
172
+ process.exit(1);
173
+ }
174
+ }
175
+
176
+ // 3. Browser
177
+ if (browser) {
178
+ const name = path.basename(browser).replace(/\.exe$/i, '');
179
+ console.log(` ✓ ${name} (${browser})`);
180
+ } else {
181
+ console.log(' ✗ No compatible browser found');
182
+ console.log(' → Install Brave (recommended): https://brave.com/download/');
183
+ console.log(' → Or Chrome: https://www.google.com/chrome/\n');
184
+ process.exit(1);
185
+ }
186
+
187
+ // Mark as done
188
+ const dir = path.dirname(markerFile);
189
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
190
+ fs.writeFileSync(markerFile, new Date().toISOString());
191
+
192
+ console.log('\n Ready!\n');
193
+ }
194
+
195
+ // ── Status Command ──
196
+
197
+ function runStatus() {
198
+ const config = resolveProjectConfig();
199
+ const port = config.port === '0' ? '9222' : config.port;
200
+ const projLabel = config.projectId ? ` [${config.projectId}]` : '';
201
+ console.log(`\n cdpilot status (port ${port})${projLabel}\n`);
202
+
203
+ try {
204
+ const http = require('http');
205
+ const req = http.get(`http://127.0.0.1:${port}/json/version`, { timeout: 2000 }, (res) => {
206
+ let data = '';
207
+ res.on('data', (chunk) => data += chunk);
208
+ res.on('end', () => {
209
+ try {
210
+ const info = JSON.parse(data);
211
+ console.log(` ✓ Connected`);
212
+ console.log(` Browser: ${info.Browser || 'Unknown'}`);
213
+ console.log(` Protocol: ${info['Protocol-Version'] || 'Unknown'}`);
214
+ console.log(` WebSocket: ${info.webSocketDebuggerUrl || 'N/A'}\n`);
215
+ } catch {
216
+ console.log(' ✓ CDP responding but version info unavailable\n');
217
+ }
218
+ });
219
+ });
220
+ req.on('error', () => {
221
+ console.log(' ❌ No browser connected on this port.');
222
+ console.log(' Run: cdpilot launch\n');
223
+ });
224
+ req.on('timeout', () => {
225
+ req.destroy();
226
+ console.log(' ❌ Connection timeout.');
227
+ console.log(' Run: cdpilot launch\n');
228
+ });
229
+ } catch {
230
+ console.log(' ❌ Could not check status.\n');
231
+ }
232
+ }
233
+
234
+ // ── Version ──
235
+
236
+ function showVersion() {
237
+ console.log(`cdpilot v${VERSION}`);
238
+ }
239
+
240
+ // ── Project-Based Multi-Instance ──
241
+
242
+ function getProjectId() {
243
+ const cwd = process.cwd();
244
+ const dirName = path.basename(cwd).replace(/[^a-zA-Z0-9-]/g, '').slice(0, 20);
245
+ const crypto = require('crypto');
246
+ const hash = crypto.createHash('md5').update(cwd).digest('hex').slice(0, 6);
247
+ return dirName ? `${dirName}-${hash}` : hash;
248
+ }
249
+
250
+ function resolveProjectConfig() {
251
+ const envPort = process.env.CDP_PORT;
252
+ const envProfile = process.env.CDPILOT_PROFILE;
253
+
254
+ // Full manual override
255
+ if (envPort && envProfile) {
256
+ return { port: envPort, profileDir: envProfile, projectId: null };
257
+ }
258
+
259
+ const projectId = getProjectId();
260
+ const registryFile = path.join(os.homedir(), '.cdpilot', 'registry.json');
261
+ const defaultProfile = path.join(os.homedir(), '.cdpilot', 'projects', projectId, 'profile');
262
+
263
+ let registry = {};
264
+ try {
265
+ const data = JSON.parse(fs.readFileSync(registryFile, 'utf-8'));
266
+ registry = data.projects || {};
267
+ } catch {}
268
+
269
+ const info = registry[projectId];
270
+ if (info) {
271
+ return {
272
+ port: envPort || String(info.port || 9222),
273
+ profileDir: envProfile || info.profile_dir || defaultProfile,
274
+ projectId,
275
+ };
276
+ }
277
+
278
+ // New project: let Python allocate port (pass 0 for auto)
279
+ return {
280
+ port: envPort || '0',
281
+ profileDir: envProfile || defaultProfile,
282
+ projectId,
283
+ };
284
+ }
285
+
286
+ // ── Help ──
287
+
288
+ function showHelp() {
289
+ console.log(`
290
+ cdpilot v${VERSION} — Zero-dependency browser automation
291
+
292
+ USAGE
293
+ cdpilot <command> [args]
294
+
295
+ SETUP
296
+ setup Auto-detect browser, create isolated profile
297
+ launch Start browser with CDP enabled
298
+ status Check browser connection
299
+ stop [--smart] Stop browser (--smart = close owned tabs, quit if empty)
300
+ close [--force|--keep] Smart close: close cdpilot's tabs; quit browser only
301
+ if no user tabs remain (--force quits anyway, --keep never quits)
302
+
303
+ NAVIGATION
304
+ go <url> Navigate to URL
305
+ content Get page text content
306
+ html Get page HTML
307
+ shot [file] Take screenshot
308
+ pdf [file] Save page as PDF
309
+
310
+ INTERACTION
311
+ click <sel> Click element
312
+ type <sel> <text> Type into input
313
+ fill <sel> <val> Set input value (React-compatible)
314
+ submit <form> Submit form
315
+ hover <sel> Hover element
316
+ keys <combo> Keyboard shortcut
317
+
318
+ DEBUGGING
319
+ console [url] Capture console logs
320
+ network [url] Monitor network requests
321
+ debug [url] Full diagnostic
322
+ eval <js> Execute JavaScript
323
+ eval-batch <json> Run N JS expressions in 1 roundtrip (perf)
324
+
325
+ PERFORMANCE
326
+ block [on|off|preset|patterns|clear]
327
+ Block requests via Network.setBlockedURLs (perf opt-in,
328
+ breaks fingerprint plausibility — not for stealth targets)
329
+ fast [on|off] Fast mode — auto-wait 5s→2s (env CDPILOT_WAIT_MS overrides)
330
+ show [on|off] Visual feedback (glow + cursor + ripples).
331
+ Default OFF since 0.4.4 — opt-in for "see automation" mode.
332
+
333
+ SMART NAVIGATION
334
+ dismiss [N|aggressive]
335
+ Click best "Stay signed out / No thanks / Skip" button.
336
+ English + Turkish patterns; never clicks destructive
337
+ lookalikes (Delete account, Sign out, Subscribe).
338
+ Pass N (1-10) or "aggressive" for chained modals.
339
+
340
+ TABS
341
+ tabs List open tabs
342
+ new-tab [url] Open new tab
343
+ close-tab [id] Close tab
344
+
345
+ PARALLEL CONTEXTS (isolated cookies/storage inside one browser)
346
+ context create [url] Make a fresh browser context + tab; prints JSON
347
+ context list List all browser contexts and their tabs
348
+ context close <ctx-id> Destroy a browser context (closes all its tabs)
349
+ (Address a context's tab in subsequent commands via CDPILOT_TARGET=<tgt-id>)
350
+
351
+ STEALTH & CAPTCHA
352
+ mode [regular|stealth|undetected]
353
+ Three-tier stealth (crawl4ai-style). regular = no patch
354
+ (cleanest, default); stealth = light patch (webdriver/
355
+ chrome.runtime/permissions); undetected = full patch
356
+ (+ plugins + WebGL + Worker). Adaptive auto-escalates.
357
+ stealth [on|off] Legacy binary toggle (on -> undetected tier)
358
+ captcha-check Detect CAPTCHA on active page (JSON output)
359
+ captcha-wait [s] Pause until user solves CAPTCHA (default 300s)
360
+ captcha-solve [--provider P]
361
+ Solve Amazon classic image CAPTCHA (opt-in). amazon-local
362
+ (optional amazoncaptcha lib) or BYOK capsolver/2captcha.
363
+ Auto-routes PerimeterX 'Press & Hold' to press-hold.
364
+ press-hold [selector]
365
+ Solve a PerimeterX/HUMAN 'Press & Hold' challenge with a
366
+ humanized press->hold(jitter)->release gesture (no token,
367
+ no provider). Auto-finds #px-captcha if no selector given.
368
+ friction Detect highest anti-bot rung (none/rate_limited/
369
+ soft_captcha/login_wall/otp_sms/hard_block) + policy.
370
+ rate_limit auto-backoff in 'go'; login/OTP/block = human
371
+ handoff (no autonomous bypass). Env: CDPILOT_FRICTION_BACKOFF,
372
+ CDPILOT_FRICTION_MAX_RETRY.
373
+ profile warm [--minutes N]
374
+ Age cookies/history on safe sites to boost reCAPTCHA v3 score.
375
+ adaptive [on|off] Auto-escalate to stealth on hosts that show CAPTCHA.
376
+ Remembers per-host. Use 'adaptive forget <host>' to reset.
377
+ cookies save <file> [<dom>]
378
+ Export cookies (all or scoped). Replay clearance cookies
379
+ across cdpilot runs to skip Cloudflare walls.
380
+ cookies load <file>
381
+ Import previously-saved cookies into the current jar.
382
+
383
+ RELIABILITY
384
+ browser [name] Show or set preferred browser (chrome|brave|chromium|edge|vivaldi|auto)
385
+ health JSON status: alive, port, tabs, browser, today's crashes
386
+
387
+ PROJECTS
388
+ projects List all project browser instances
389
+ project-stop <id> Stop a specific project's browser
390
+ stop-all Stop all browser instances
391
+
392
+ AI AGENT
393
+ mcp Start MCP server (stdin/stdout JSON-RPC)
394
+
395
+ WATCH (continuous screencast for AI video understanding)
396
+ watch start <url> Begin JPEG screencast at N fps to a disk ring buffer
397
+ (default 10fps, 5min retention, 100MB cap). Background
398
+ daemon — command returns immediately.
399
+ watch query --at MM:SS --window 5s
400
+ Return JSON list of frame paths around a video time.
401
+ watch query --last 5s | --since-last
402
+ Recent frames or everything new since the last query.
403
+ watch status Daemon state, frame count, disk usage.
404
+ watch stop Stop daemon + clean up frames (--keep-frames to retain).
405
+ watch ask "<q>" Tiny NL parser: extracts time window from a question.
406
+
407
+ More: https://github.com/mehmetnadir/cdpilot#commands
408
+ `);
409
+ }
410
+
411
+ // ── Internal Test Runner ──
412
+
413
+ function runInternalTestRunner(testFile, traceDir, traceMode, grepPattern) {
414
+ if (traceDir) {
415
+ fs.mkdirSync(path.join(traceDir, 'screenshots'), { recursive: true });
416
+ fs.mkdirSync(path.join(traceDir, 'a11y'), { recursive: true });
417
+ }
418
+
419
+ const metaPath = traceDir ? path.join(traceDir, 'meta.json') : null;
420
+ const stepsPath = traceDir && traceMode !== 'off' ? path.join(traceDir, 'steps.jsonl') : null;
421
+
422
+ const meta = { name: path.basename(testFile), started_at: new Date().toISOString(), status: 'running', tests: [] };
423
+ if (metaPath) fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2));
424
+
425
+ const testQueue = [];
426
+ global.test = (name, fn) => testQueue.push({ name, fn });
427
+
428
+ try {
429
+ require(path.resolve(testFile));
430
+ } catch (err) {
431
+ const out = { passed: 0, failed: 1, skipped: 0, tests: [{ name: testFile, status: 'failed', duration_ms: 0, error: err.message }] };
432
+ process.stdout.write(JSON.stringify(out) + '\n');
433
+ process.exit(1);
434
+ }
435
+
436
+ const results = { passed: 0, failed: 0, skipped: 0, tests: [] };
437
+ const cdpPort = process.env.CDP_PORT || '9222';
438
+ const SCRIPT = path.join(__dirname, '..', 'src', 'cdpilot.py');
439
+ const python = 'python3';
440
+ let stepIdx = 0;
441
+
442
+ const makeT = () => {
443
+ const runCmd = (cmd, ...cargs) => {
444
+ const padded = String(stepIdx).padStart(3, '0');
445
+ const step = { action: cmd + ' ' + cargs.join(' '), ts_ms: Date.now(), duration_ms: 0, error: null };
446
+ const t0 = Date.now();
447
+ try {
448
+ const quoted = cargs.map(a => JSON.stringify(String(a))).join(' ');
449
+ execSync(`${python} ${SCRIPT} ${cmd} ${quoted}`, {
450
+ stdio: 'pipe',
451
+ env: { ...process.env, CDP_PORT: cdpPort },
452
+ timeout: 30000,
453
+ });
454
+ step.duration_ms = Date.now() - t0;
455
+ if (stepsPath) fs.appendFileSync(stepsPath, JSON.stringify(step) + '\n');
456
+ // Screenshot after each step (best-effort — no browser = skipped)
457
+ if (traceDir && traceMode !== 'off') {
458
+ try {
459
+ const shotPath = path.join(traceDir, 'screenshots', `step-${padded}.png`);
460
+ execSync(`${python} ${SCRIPT} shot ${shotPath}`, { stdio: 'pipe', env: { ...process.env, CDP_PORT: cdpPort }, timeout: 10000 });
461
+ } catch (_) { /* no browser is OK in unit-style tests */ }
462
+ }
463
+ stepIdx++;
464
+ } catch (err) {
465
+ step.duration_ms = Date.now() - t0;
466
+ step.error = err.stderr ? err.stderr.toString().trim() : err.message;
467
+ if (stepsPath) fs.appendFileSync(stepsPath, JSON.stringify(step) + '\n');
468
+ stepIdx++;
469
+ throw new Error(`${cmd} failed: ${step.error}`);
470
+ }
471
+ };
472
+
473
+ const t = {
474
+ goto: (url) => runCmd('go', url),
475
+ click: (sel) => runCmd('click', sel),
476
+ fill: (sel, val) => runCmd('fill', sel, val),
477
+ type: (sel, val) => runCmd('type', sel, val),
478
+ hover: (sel) => runCmd('hover', sel),
479
+ screenshot: (p) => runCmd('shot', p),
480
+ eval: (js) => {
481
+ try {
482
+ const out = execSync(`${python} ${SCRIPT} eval ${JSON.stringify(js)}`, { stdio: 'pipe', env: { ...process.env, CDP_PORT: cdpPort }, timeout: 10000 });
483
+ return out.toString().trim();
484
+ } catch (e) { throw new Error('eval failed: ' + e.message); }
485
+ },
486
+ a11y: () => runCmd('a11y-snapshot'),
487
+ };
488
+
489
+ t.expect = (textOrSel) => runCmd('assert', textOrSel);
490
+ t.expect.url = (expected) => runCmd('assert-url', expected);
491
+ t.expect.visible = (sel) => runCmd('assert-visible', sel);
492
+ t.expect.hidden = (sel) => runCmd('assert-hidden', sel);
493
+
494
+ return t;
495
+ };
496
+
497
+ // Run tests sequentially (parallel is managed at the Python level across files)
498
+ const runAll = async () => {
499
+ for (const tst of testQueue) {
500
+ if (grepPattern && !tst.name.match(new RegExp(grepPattern, 'i'))) {
501
+ results.skipped++;
502
+ continue;
503
+ }
504
+ const t0 = Date.now();
505
+ const rec = { name: tst.name, status: 'passed', duration_ms: 0, error: null };
506
+ try {
507
+ await tst.fn(makeT());
508
+ rec.status = 'passed';
509
+ results.passed++;
510
+ } catch (err) {
511
+ rec.status = 'failed';
512
+ rec.error = err.message;
513
+ results.failed++;
514
+ }
515
+ rec.duration_ms = Date.now() - t0;
516
+ results.tests.push(rec);
517
+ if (metaPath) {
518
+ meta.tests.push(rec);
519
+ meta.status = 'running';
520
+ fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2));
521
+ }
522
+ }
523
+
524
+ // Finalize meta
525
+ if (metaPath) {
526
+ meta.status = results.failed > 0 ? 'failed' : 'passed';
527
+ meta.passed = results.passed;
528
+ meta.failed = results.failed;
529
+ meta.skipped = results.skipped;
530
+ meta.ended_at = new Date().toISOString();
531
+ fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2));
532
+ }
533
+
534
+ process.stdout.write(JSON.stringify(results) + '\n');
535
+ process.exit(results.failed > 0 ? 1 : 0);
536
+ };
537
+
538
+ runAll().catch(err => {
539
+ process.stderr.write('Test runner error: ' + err.message + '\n');
540
+ process.exit(1);
541
+ });
542
+ }
543
+
544
+ // ── Main ──
545
+
546
+ // Internal test runner mode — intercept before normal CLI dispatch
547
+ if (process.argv.includes('--internal-test-runner')) {
548
+ const idx = process.argv.indexOf('--internal-test-runner');
549
+ const testFile = process.argv[idx + 1];
550
+ const traceDirArg = process.argv.find(a => a.startsWith('--trace-dir='));
551
+ const traceArg = process.argv.find(a => a.startsWith('--trace='));
552
+ const grepArg = process.argv.find(a => a.startsWith('--grep='));
553
+ runInternalTestRunner(
554
+ testFile,
555
+ traceDirArg ? traceDirArg.split('=').slice(1).join('=') : null,
556
+ traceArg ? traceArg.split('=')[1] : 'default',
557
+ grepArg ? grepArg.split('=').slice(1).join('=') : null,
558
+ );
559
+ return; // runAll() is async, this exits via process.exit
560
+ }
561
+
562
+ const args = process.argv.slice(2);
563
+ const cmd = args[0];
564
+
565
+ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
566
+ showHelp();
567
+ process.exit(0);
568
+ }
569
+
570
+ if (cmd === '--version' || cmd === '-v') {
571
+ showVersion();
572
+ process.exit(0);
573
+ }
574
+
575
+ if (cmd === 'setup') {
576
+ runSetup();
577
+ process.exit(0);
578
+ }
579
+
580
+ if (cmd === 'status') {
581
+ runStatus();
582
+ // Don't exit immediately — let http callback complete
583
+ } else {
584
+ // Pre-flight check on first run or 'launch' command
585
+ if (cmd === 'launch') {
586
+ preflight();
587
+ }
588
+
589
+ // Delegate to Python
590
+ const python = findPython();
591
+ if (!python) {
592
+ console.error('Error: Python 3.8+ required. Install: https://www.python.org/downloads/');
593
+ process.exit(1);
594
+ }
595
+
596
+ const browser = findBrowser();
597
+ const config = resolveProjectConfig();
598
+
599
+ const env = {
600
+ ...process.env,
601
+ CDPILOT_PROFILE: config.profileDir,
602
+ };
603
+
604
+ // Only pass CDP_PORT if explicitly set or resolved from registry (not 0)
605
+ if (config.port !== '0') {
606
+ env.CDP_PORT = config.port;
607
+ }
608
+
609
+ if (config.projectId) {
610
+ env.CDPILOT_PROJECT_ID = config.projectId;
611
+ }
612
+
613
+ if (browser && !process.env.CHROME_BIN) {
614
+ env.CHROME_BIN = browser;
615
+ }
616
+
617
+ const child = spawn(python, [SCRIPT, ...args], {
618
+ stdio: 'inherit',
619
+ env,
620
+ });
621
+
622
+ child.on('close', (code) => {
623
+ process.exit(code || 0);
624
+ });
625
+ }
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "@mseep/cdpilot",
3
+ "version": "0.8.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
+ "bin": {
6
+ "cdpilot": "./bin/cdpilot.js",
7
+ "bctl": "./bin/cdpilot.js"
8
+ },
9
+ "scripts": {
10
+ "test": "node test/test.js",
11
+ "start": "node bin/cdpilot.js"
12
+ },
13
+ "keywords": [
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",
53
+ "mseep",
54
+ "mcp-server"
55
+ ],
56
+ "author": "Mehmet Nadir",
57
+ "license": "MIT",
58
+ "repository": {
59
+ "type": "git",
60
+ "url": "https://github.com/mehmetnadir/cdpilot"
61
+ },
62
+ "engines": {
63
+ "node": ">=18.0.0"
64
+ },
65
+ "files": [
66
+ "bin/",
67
+ "src/cdpilot.py",
68
+ "README.md",
69
+ "LICENSE"
70
+ ],
71
+ "devDependencies": {
72
+ "gray-matter": "^4.0.3",
73
+ "remark": "^15.0.1",
74
+ "remark-html": "^16.0.1"
75
+ },
76
+ "publisher": "mseep"
77
+ }