cdpilot 0.9.2 → 0.9.4
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 +523 -7
- package/bin/cdpilot.js +207 -13
- package/package.json +3 -2
- package/src/cdpilot.py +7098 -597
package/bin/cdpilot.js
CHANGED
|
@@ -5,7 +5,14 @@
|
|
|
5
5
|
* Entry point: detects Python, finds browser, delegates to cdpilot.py
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
const { execSync, spawn } = require('child_process');
|
|
8
|
+
const { execSync, spawn, spawnSync } = require('child_process');
|
|
9
|
+
|
|
10
|
+
// Output the launcher prints itself (help, status) must survive process.exit():
|
|
11
|
+
// on macOS a piped stdout is asynchronous, and exiting right after a write
|
|
12
|
+
// longer than the pipe buffer (8 KB) cut --help off mid-text.
|
|
13
|
+
for (const stream of [process.stdout, process.stderr]) {
|
|
14
|
+
if (stream._handle && typeof stream._handle.setBlocking === 'function') stream._handle.setBlocking(true);
|
|
15
|
+
}
|
|
9
16
|
const path = require('path');
|
|
10
17
|
const fs = require('fs');
|
|
11
18
|
const os = require('os');
|
|
@@ -13,6 +20,16 @@ const os = require('os');
|
|
|
13
20
|
const SCRIPT = path.join(__dirname, '..', 'src', 'cdpilot.py');
|
|
14
21
|
const VERSION = require('../package.json').version;
|
|
15
22
|
|
|
23
|
+
// CDPILOT_HOME resolution — must match src/cdpilot.py exactly:
|
|
24
|
+
// CDPILOT_HOME = os.environ.get("CDPILOT_HOME") or os.path.expanduser("~/.cdpilot")
|
|
25
|
+
// A set-but-empty CDPILOT_HOME falls back to the default (empty string is
|
|
26
|
+
// falsy in both Python's `or` and JS's `||`). A non-empty CDPILOT_HOME is
|
|
27
|
+
// used verbatim — no `~` expansion — same as Python's os.environ.get() here
|
|
28
|
+
// (expanduser is only applied to the hardcoded "~/.cdpilot" fallback).
|
|
29
|
+
function cdpilotHome() {
|
|
30
|
+
return process.env.CDPILOT_HOME || path.join(os.homedir(), '.cdpilot');
|
|
31
|
+
}
|
|
32
|
+
|
|
16
33
|
// ── Browser Detection ──
|
|
17
34
|
|
|
18
35
|
function findBrowser() {
|
|
@@ -161,7 +178,7 @@ function checkWebsockets(python) {
|
|
|
161
178
|
}
|
|
162
179
|
|
|
163
180
|
function preflight() {
|
|
164
|
-
const markerFile = path.join(
|
|
181
|
+
const markerFile = path.join(cdpilotHome(), '.preflight-done');
|
|
165
182
|
|
|
166
183
|
// Skip if already passed (not first run) and all deps present
|
|
167
184
|
const python = findPython();
|
|
@@ -229,7 +246,23 @@ function runStatus() {
|
|
|
229
246
|
const config = resolveProjectConfig();
|
|
230
247
|
const port = config.port === '0' ? '9222' : config.port;
|
|
231
248
|
const projLabel = config.projectId ? ` [${config.projectId}]` : '';
|
|
232
|
-
|
|
249
|
+
|
|
250
|
+
// Check if this project has an external (connect'ed) browser
|
|
251
|
+
let isExternal = false;
|
|
252
|
+
let browserName = '';
|
|
253
|
+
try {
|
|
254
|
+
const home = cdpilotHome();
|
|
255
|
+
const regFile = path.join(home, 'registry.json');
|
|
256
|
+
const data = JSON.parse(fs.readFileSync(regFile, 'utf-8'));
|
|
257
|
+
const entry = (data.projects || {})[config.projectId];
|
|
258
|
+
if (entry && entry.external) {
|
|
259
|
+
isExternal = true;
|
|
260
|
+
browserName = entry.browser_name || '';
|
|
261
|
+
}
|
|
262
|
+
} catch {}
|
|
263
|
+
|
|
264
|
+
const extLabel = isExternal ? ' (external)' : '';
|
|
265
|
+
console.log(`\n cdpilot status (port ${port})${projLabel}${extLabel}\n`);
|
|
233
266
|
|
|
234
267
|
try {
|
|
235
268
|
const http = require('http');
|
|
@@ -240,17 +273,31 @@ function runStatus() {
|
|
|
240
273
|
try {
|
|
241
274
|
const info = JSON.parse(data);
|
|
242
275
|
console.log(` ✓ Connected`);
|
|
243
|
-
console.log(` Browser: ${info.Browser || 'Unknown'}`);
|
|
276
|
+
console.log(` Browser: ${info.Browser || 'Unknown'}${isExternal ? ' (external — your browser)' : ''}`);
|
|
244
277
|
console.log(` Protocol: ${info['Protocol-Version'] || 'Unknown'}`);
|
|
245
|
-
console.log(` WebSocket: ${info.webSocketDebuggerUrl || 'N/A'}
|
|
278
|
+
console.log(` WebSocket: ${info.webSocketDebuggerUrl || 'N/A'}`);
|
|
279
|
+
if (isExternal) {
|
|
280
|
+
console.log(` cdpilot will never close this browser.\n`);
|
|
281
|
+
} else {
|
|
282
|
+
const extra = [webmcpLabel(config.profileDir), botAuthLabel(port)].filter(Boolean);
|
|
283
|
+
console.log(` ${idleCloseLabel(port)}${extra.length ? '' : '\n'}`);
|
|
284
|
+
extra.forEach((l, i) => console.log(` ${l}${i === extra.length - 1 ? '\n' : ''}`));
|
|
285
|
+
}
|
|
246
286
|
} catch {
|
|
247
287
|
console.log(' ✓ CDP responding but version info unavailable\n');
|
|
248
288
|
}
|
|
249
289
|
});
|
|
250
290
|
});
|
|
251
291
|
req.on('error', () => {
|
|
252
|
-
|
|
253
|
-
|
|
292
|
+
if (isExternal) {
|
|
293
|
+
console.log(` ❌ External browser (${browserName || 'unknown'}) is gone.`);
|
|
294
|
+
console.log(' Run: cdpilot connect again or cdpilot disconnect\n');
|
|
295
|
+
} else {
|
|
296
|
+
console.log(' ❌ No browser connected on this port.');
|
|
297
|
+
const webmcp = webmcpLabel(config.profileDir);
|
|
298
|
+
if (webmcp) console.log(` ${webmcp}`);
|
|
299
|
+
console.log(' Run: cdpilot launch\n');
|
|
300
|
+
}
|
|
254
301
|
});
|
|
255
302
|
req.on('timeout', () => {
|
|
256
303
|
req.destroy();
|
|
@@ -262,6 +309,80 @@ function runStatus() {
|
|
|
262
309
|
}
|
|
263
310
|
}
|
|
264
311
|
|
|
312
|
+
// Idle auto-close countdown, from the files src/cdpilot.py keeps (see
|
|
313
|
+
// _idle_status there): CDPILOT_HOME/idle/<port>.json names the watcher and its
|
|
314
|
+
// minutes, CDPILOT_HOME/projects/<id>/last-activity the last command.
|
|
315
|
+
function idleCloseLabel(port) {
|
|
316
|
+
const home = cdpilotHome();
|
|
317
|
+
try {
|
|
318
|
+
const st = JSON.parse(fs.readFileSync(path.join(home, 'idle', `${port}.json`), 'utf-8'));
|
|
319
|
+
const minutes = Number(st.minutes) || 0;
|
|
320
|
+
if (minutes <= 0 || !st.pid) return 'idle close off';
|
|
321
|
+
try {
|
|
322
|
+
process.kill(st.pid, 0); // existence probe; safe on Windows in Node
|
|
323
|
+
} catch (e) {
|
|
324
|
+
if (e.code !== 'EPERM') return 'idle close off';
|
|
325
|
+
}
|
|
326
|
+
let last = Number(st.started) || Date.now() / 1000;
|
|
327
|
+
try {
|
|
328
|
+
const file = path.join(home, 'projects', String(st.project_id), 'last-activity');
|
|
329
|
+
const t = parseFloat(fs.readFileSync(file, 'utf-8'));
|
|
330
|
+
if (t > last) last = t;
|
|
331
|
+
} catch {}
|
|
332
|
+
const left = Math.max(0, Math.floor(minutes * 60 - (Date.now() / 1000 - last)));
|
|
333
|
+
return `idle close in ${Math.ceil(left / 60)}m`;
|
|
334
|
+
} catch {
|
|
335
|
+
return 'idle close off';
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// This project's WebMCP mode, as src/cdpilot.py reads it (get_webmcp_config):
|
|
340
|
+
// CDPILOT_WEBMCP=1|0 wins, else <profile>/webmcp.json written by
|
|
341
|
+
// `launch --webmcp` / `launch --no-webmcp`.
|
|
342
|
+
function webmcpMode(profileDir) {
|
|
343
|
+
const raw = (process.env.CDPILOT_WEBMCP || '').trim().toLowerCase();
|
|
344
|
+
if (['1', 'true', 'yes', 'on'].includes(raw)) return { on: true, from: 'CDPILOT_WEBMCP' };
|
|
345
|
+
if (['0', 'false', 'no', 'off'].includes(raw)) return { on: false, from: 'CDPILOT_WEBMCP' };
|
|
346
|
+
try {
|
|
347
|
+
const st = JSON.parse(fs.readFileSync(path.join(profileDir, 'webmcp.json'), 'utf-8'));
|
|
348
|
+
return { on: st.webmcp === true, from: 'launch --webmcp' };
|
|
349
|
+
} catch {
|
|
350
|
+
return { on: false, from: null };
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Printed by `status` only while the mode is on (the default output is unchanged).
|
|
355
|
+
function webmcpLabel(profileDir) {
|
|
356
|
+
const m = webmcpMode(profileDir);
|
|
357
|
+
if (!m.on) return null;
|
|
358
|
+
return `WebMCP: on (${m.from}; browsers start with --enable-features=WebMCP)`;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Web Bot Auth signer line, printed by `status` only when bot-auth is set up
|
|
362
|
+
// (a key in CDPILOT_HOME/bot-auth/, or signer state for this port), so the
|
|
363
|
+
// default output is unchanged. With no signer state file or stale marker in
|
|
364
|
+
// CDPILOT_HOME/bot-auth/signers/ it is "off" without starting Python.
|
|
365
|
+
// Otherwise Python decides (hidden --_bot-auth-label, see
|
|
366
|
+
// _bot_auth_helper_state there): a pid alone proves nothing, the signer is
|
|
367
|
+
// "on" only if that pid's command line is this port's signer and the browser
|
|
368
|
+
// is still the one it attached to; a dead or stale signer also prints the
|
|
369
|
+
// one-line "requests go out unsigned" warning on stderr.
|
|
370
|
+
function botAuthLabel(port) {
|
|
371
|
+
const home = cdpilotHome();
|
|
372
|
+
const dir = path.join(home, 'bot-auth', 'signers');
|
|
373
|
+
const signer = fs.existsSync(path.join(dir, `${port}.json`)) || fs.existsSync(path.join(dir, `${port}.stale`));
|
|
374
|
+
if (!signer) return fs.existsSync(path.join(home, 'bot-auth', 'config.json')) ? 'bot-auth: off' : null;
|
|
375
|
+
const python = findPython();
|
|
376
|
+
if (!python) return 'bot-auth: off';
|
|
377
|
+
const r = spawnSync(python, [SCRIPT, '--_bot-auth-label'], {
|
|
378
|
+
encoding: 'utf-8', timeout: 20000,
|
|
379
|
+
env: { ...process.env, CDP_PORT: String(port), CDPILOT_LOG: '0' },
|
|
380
|
+
});
|
|
381
|
+
if (r.stderr) process.stderr.write(r.stderr);
|
|
382
|
+
const line = (r.stdout || '').trim().split('\n').pop();
|
|
383
|
+
return /^bot-auth: /.test(line || '') ? line : 'bot-auth: off';
|
|
384
|
+
}
|
|
385
|
+
|
|
265
386
|
// ── Version ──
|
|
266
387
|
|
|
267
388
|
function showVersion() {
|
|
@@ -288,8 +409,8 @@ function resolveProjectConfig() {
|
|
|
288
409
|
}
|
|
289
410
|
|
|
290
411
|
const projectId = getProjectId();
|
|
291
|
-
const registryFile = path.join(
|
|
292
|
-
const defaultProfile = path.join(
|
|
412
|
+
const registryFile = path.join(cdpilotHome(), 'registry.json');
|
|
413
|
+
const defaultProfile = path.join(cdpilotHome(), 'projects', projectId, 'profile');
|
|
293
414
|
|
|
294
415
|
let registry = {};
|
|
295
416
|
try {
|
|
@@ -329,12 +450,27 @@ function showHelp() {
|
|
|
329
450
|
(the flag wins, 0 disables).
|
|
330
451
|
Page commands start the browser if it is not running
|
|
331
452
|
(set CDPILOT_NO_AUTOLAUNCH=1 to get an error instead).
|
|
453
|
+
An auto-launched (or MCP-launched) browser closes after 15 min without a
|
|
454
|
+
cdpilot command or page change (CDPILOT_IDLE_CLOSE=<minutes>,
|
|
455
|
+
0 = never); an explicit launch stays open unless asked (below).
|
|
332
456
|
|
|
333
457
|
SETUP
|
|
334
458
|
setup Auto-detect browser, create isolated profile
|
|
335
|
-
launch
|
|
336
|
-
|
|
459
|
+
launch [--idle-close <min>] [--webmcp|--no-webmcp] [--bot-auth]
|
|
460
|
+
Start browser with CDP enabled (--idle-close or
|
|
461
|
+
CDPILOT_IDLE_CLOSE: close it after <min> idle minutes;
|
|
462
|
+
--webmcp: this project's browsers start with WebMCP on,
|
|
463
|
+
saved until --no-webmcp; see WEBMCP below; --bot-auth:
|
|
464
|
+
sign every request, see WEB BOT AUTH)
|
|
465
|
+
status Check browser connection (idle close; WebMCP and the
|
|
466
|
+
bot-auth signer when set up)
|
|
337
467
|
stop [--smart] Stop browser (--smart = close owned tabs, quit if empty)
|
|
468
|
+
connect [<port> | <ws-url> | --auto]
|
|
469
|
+
Use a browser you started with --remote-debugging-port
|
|
470
|
+
and --user-data-dir; cdpilot never closes or injects
|
|
471
|
+
into it. --auto reads DevToolsActivePort files.
|
|
472
|
+
(chrome://inspect remote-debugging mode: not supported yet)
|
|
473
|
+
disconnect Forget the connected browser (it keeps running)
|
|
338
474
|
close [--force|--keep] Smart close: close cdpilot's tabs; quit browser only
|
|
339
475
|
if no user tabs remain (--force quits anyway, --keep never quits)
|
|
340
476
|
|
|
@@ -346,12 +482,21 @@ function showHelp() {
|
|
|
346
482
|
pdf [file] Save page as PDF
|
|
347
483
|
|
|
348
484
|
INTERACTION
|
|
349
|
-
click <sel> Click element
|
|
485
|
+
click <sel> Click element; exit 3 =
|
|
486
|
+
pressed, release missed the target, not clicked
|
|
350
487
|
type <sel> <text> Type into input
|
|
351
488
|
fill <sel> <val> Set input value (React-compatible)
|
|
352
489
|
submit <form> Submit form
|
|
353
490
|
hover <sel> Hover element
|
|
354
491
|
keys <combo> Keyboard shortcut
|
|
492
|
+
Inside an iframe: click "iframe#card >>> input[name=cardnumber]"
|
|
493
|
+
(nest: "iframe.a >>> iframe.b >>> button") or
|
|
494
|
+
--frame <selector|index|url-substring>; same-origin and
|
|
495
|
+
cross-origin frames. With no match in the page,
|
|
496
|
+
smart-click searches all frames, smart-fill and
|
|
497
|
+
smart-select only frames of the page's origin.
|
|
498
|
+
frame list|eval [--frame <f>]
|
|
499
|
+
List iframes / run JS inside a frame
|
|
355
500
|
|
|
356
501
|
DEBUGGING
|
|
357
502
|
console [url] Capture console logs
|
|
@@ -418,8 +563,32 @@ function showHelp() {
|
|
|
418
563
|
cookies load <file>
|
|
419
564
|
Import previously-saved cookies into the current jar.
|
|
420
565
|
|
|
566
|
+
WEB BOT AUTH (signed agent — the opposite of stealth; needs: pip install cryptography)
|
|
567
|
+
bot-auth init --agent-url https://your-domain.com [--force]
|
|
568
|
+
Generate an Ed25519 key (0600, never printed) in CDPILOT_HOME/bot-auth/
|
|
569
|
+
bot-auth directory Print the JWKS to serve at
|
|
570
|
+
https://your-domain.com/.well-known/http-message-signatures-directory
|
|
571
|
+
bot-auth directory --headers [--authority <host>] [--ttl <s>] [--content-digest] [--json]
|
|
572
|
+
The same plus the Signature / Signature-Input headers that
|
|
573
|
+
response must carry (Cloudflare checks them; one per key)
|
|
574
|
+
bot-auth format [legacy|dict]
|
|
575
|
+
Signature-Agent format. legacy (default): "https://…",
|
|
576
|
+
what Cloudflare verifies; dict: sig1="https://…" (draft-05)
|
|
577
|
+
bot-auth status Agent URL, keyid, and whether the signer is running
|
|
578
|
+
launch --bot-auth Launch with a signer that adds Signature, Signature-Input and
|
|
579
|
+
Signature-Agent to EVERY request the browser makes (new tabs,
|
|
580
|
+
popups, iframes, workers, page fetches after cdpilot exits).
|
|
581
|
+
Or CDPILOT_BOT_AUTH=1. With stealth mode: one warning, stealth
|
|
582
|
+
injection skipped. "stop" ends the signer.
|
|
583
|
+
|
|
421
584
|
RELIABILITY
|
|
422
|
-
browser [name] Show or set preferred browser
|
|
585
|
+
browser [name] Show or set preferred browser
|
|
586
|
+
(chrome|brave|chromium|edge|vivaldi|chrome-for-testing|auto)
|
|
587
|
+
browser install chrome-for-testing [--channel stable|beta|dev|canary] [--version X]
|
|
588
|
+
Download Chrome for Testing (~150-190 MB, only on this
|
|
589
|
+
command) for extension development: Chrome 137+ ignores
|
|
590
|
+
--load-extension, CfT does not. Used instead of Chrome
|
|
591
|
+
while dev extensions are registered. Not for stealth.
|
|
423
592
|
health JSON status: alive, port, tabs, browser, today's crashes
|
|
424
593
|
|
|
425
594
|
PROJECTS
|
|
@@ -430,6 +599,27 @@ function showHelp() {
|
|
|
430
599
|
AI AGENT
|
|
431
600
|
mcp Start MCP server (stdin/stdout JSON-RPC)
|
|
432
601
|
|
|
602
|
+
SESSION LOG (always on, local, redacted)
|
|
603
|
+
log Today's commands for this project: time, exit, command,
|
|
604
|
+
url, result
|
|
605
|
+
log --md Markdown report (pages visited, actions, errors, files
|
|
606
|
+
produced) to paste into an issue or PR
|
|
607
|
+
log --json Raw JSON lines · log --days N · log --path (log directory)
|
|
608
|
+
Typed values, secret-looking args and token/key/secret URL
|
|
609
|
+
params are redacted. CDPILOT_LOG=0 turns it off;
|
|
610
|
+
CDPILOT_LOG_DAYS (default 14) sets how many days are kept.
|
|
611
|
+
|
|
612
|
+
WEBMCP (tools a page registers on document.modelContext; needs launch --webmcp)
|
|
613
|
+
tools list [--json] The page's tools from getTools(): imperative, <form
|
|
614
|
+
toolname> and same-origin iframe tools, with title,
|
|
615
|
+
annotations, inputSchema. None: says why (WebMCP off,
|
|
616
|
+
insecure page, old browser, none registered); exit 0.
|
|
617
|
+
tools call <name> [json-args | --arg k=v ...] [--frame <url-part>]
|
|
618
|
+
Check args against inputSchema, run executeTool() with
|
|
619
|
+
an abort signal (--timeout, default 20s), print the
|
|
620
|
+
result. Bad args / tool error: exit 1; timeout: 124.
|
|
621
|
+
Runs the page's own code: its result is untrusted.
|
|
622
|
+
|
|
433
623
|
WATCH (continuous screencast for AI video understanding)
|
|
434
624
|
watch start <url> Begin JPEG screencast at N fps to a disk ring buffer
|
|
435
625
|
(default 10fps, 5min retention, 100MB cap). Background
|
|
@@ -665,6 +855,10 @@ if (cmd === 'status') {
|
|
|
665
855
|
|
|
666
856
|
if (browser && !process.env.CHROME_BIN) {
|
|
667
857
|
env.CHROME_BIN = browser;
|
|
858
|
+
// Tells src/cdpilot.py this CHROME_BIN is our guess, not the user's: it
|
|
859
|
+
// still applies `cdpilot browser <name>` and the dev-extension rule
|
|
860
|
+
// (Chrome -> Chrome for Testing) on top of it.
|
|
861
|
+
env.CDPILOT_CHROME_BIN_AUTO = '1';
|
|
668
862
|
}
|
|
669
863
|
|
|
670
864
|
const child = spawn(python, [SCRIPT, ...args], {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cdpilot",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.4",
|
|
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": {
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"test": "node test/test.js",
|
|
12
|
-
"start": "node bin/cdpilot.js"
|
|
12
|
+
"start": "node bin/cdpilot.js",
|
|
13
|
+
"build:mcpb": "bash scripts/build-mcpb.sh"
|
|
13
14
|
},
|
|
14
15
|
"keywords": [
|
|
15
16
|
"browser-automation",
|