@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/LICENSE +21 -0
- package/README.md +820 -0
- package/bin/cdpilot.js +625 -0
- package/package.json +77 -0
- package/src/cdpilot.py +13031 -0
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
|
+
}
|