cdpilot 0.9.3 → 0.9.5
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 +451 -22
- package/bin/cdpilot.js +160 -15
- package/package.json +3 -2
- package/src/cdpilot.py +6885 -511
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,18 +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
278
|
console.log(` WebSocket: ${info.webSocketDebuggerUrl || 'N/A'}`);
|
|
246
|
-
|
|
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
|
+
}
|
|
247
286
|
} catch {
|
|
248
287
|
console.log(' ✓ CDP responding but version info unavailable\n');
|
|
249
288
|
}
|
|
250
289
|
});
|
|
251
290
|
});
|
|
252
291
|
req.on('error', () => {
|
|
253
|
-
|
|
254
|
-
|
|
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
|
+
}
|
|
255
301
|
});
|
|
256
302
|
req.on('timeout', () => {
|
|
257
303
|
req.destroy();
|
|
@@ -267,7 +313,7 @@ function runStatus() {
|
|
|
267
313
|
// _idle_status there): CDPILOT_HOME/idle/<port>.json names the watcher and its
|
|
268
314
|
// minutes, CDPILOT_HOME/projects/<id>/last-activity the last command.
|
|
269
315
|
function idleCloseLabel(port) {
|
|
270
|
-
const home =
|
|
316
|
+
const home = cdpilotHome();
|
|
271
317
|
try {
|
|
272
318
|
const st = JSON.parse(fs.readFileSync(path.join(home, 'idle', `${port}.json`), 'utf-8'));
|
|
273
319
|
const minutes = Number(st.minutes) || 0;
|
|
@@ -290,6 +336,53 @@ function idleCloseLabel(port) {
|
|
|
290
336
|
}
|
|
291
337
|
}
|
|
292
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
|
+
|
|
293
386
|
// ── Version ──
|
|
294
387
|
|
|
295
388
|
function showVersion() {
|
|
@@ -316,8 +409,8 @@ function resolveProjectConfig() {
|
|
|
316
409
|
}
|
|
317
410
|
|
|
318
411
|
const projectId = getProjectId();
|
|
319
|
-
const registryFile = path.join(
|
|
320
|
-
const defaultProfile = path.join(
|
|
412
|
+
const registryFile = path.join(cdpilotHome(), 'registry.json');
|
|
413
|
+
const defaultProfile = path.join(cdpilotHome(), 'projects', projectId, 'profile');
|
|
321
414
|
|
|
322
415
|
let registry = {};
|
|
323
416
|
try {
|
|
@@ -363,10 +456,21 @@ function showHelp() {
|
|
|
363
456
|
|
|
364
457
|
SETUP
|
|
365
458
|
setup Auto-detect browser, create isolated profile
|
|
366
|
-
launch [--idle-close <min>]
|
|
367
|
-
|
|
368
|
-
|
|
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)
|
|
369
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)
|
|
370
474
|
close [--force|--keep] Smart close: close cdpilot's tabs; quit browser only
|
|
371
475
|
if no user tabs remain (--force quits anyway, --keep never quits)
|
|
372
476
|
|
|
@@ -378,7 +482,8 @@ function showHelp() {
|
|
|
378
482
|
pdf [file] Save page as PDF
|
|
379
483
|
|
|
380
484
|
INTERACTION
|
|
381
|
-
click <sel> Click element
|
|
485
|
+
click <sel> Click element; exit 3 =
|
|
486
|
+
pressed, release missed the target, not clicked
|
|
382
487
|
type <sel> <text> Type into input
|
|
383
488
|
fill <sel> <val> Set input value (React-compatible)
|
|
384
489
|
submit <form> Submit form
|
|
@@ -458,8 +563,32 @@ function showHelp() {
|
|
|
458
563
|
cookies load <file>
|
|
459
564
|
Import previously-saved cookies into the current jar.
|
|
460
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
|
+
|
|
461
584
|
RELIABILITY
|
|
462
|
-
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.
|
|
463
592
|
health JSON status: alive, port, tabs, browser, today's crashes
|
|
464
593
|
|
|
465
594
|
PROJECTS
|
|
@@ -469,6 +598,7 @@ function showHelp() {
|
|
|
469
598
|
|
|
470
599
|
AI AGENT
|
|
471
600
|
mcp Start MCP server (stdin/stdout JSON-RPC)
|
|
601
|
+
--tools core|all|a,b (env CDPILOT_MCP_TOOLS): smaller tool list
|
|
472
602
|
|
|
473
603
|
SESSION LOG (always on, local, redacted)
|
|
474
604
|
log Today's commands for this project: time, exit, command,
|
|
@@ -480,6 +610,17 @@ function showHelp() {
|
|
|
480
610
|
params are redacted. CDPILOT_LOG=0 turns it off;
|
|
481
611
|
CDPILOT_LOG_DAYS (default 14) sets how many days are kept.
|
|
482
612
|
|
|
613
|
+
WEBMCP (tools a page registers on document.modelContext; needs launch --webmcp)
|
|
614
|
+
tools list [--json] The page's tools from getTools(): imperative, <form
|
|
615
|
+
toolname> and same-origin iframe tools, with title,
|
|
616
|
+
annotations, inputSchema. None: says why (WebMCP off,
|
|
617
|
+
insecure page, old browser, none registered); exit 0.
|
|
618
|
+
tools call <name> [json-args | --arg k=v ...] [--frame <url-part>]
|
|
619
|
+
Check args against inputSchema, run executeTool() with
|
|
620
|
+
an abort signal (--timeout, default 20s), print the
|
|
621
|
+
result. Bad args / tool error: exit 1; timeout: 124.
|
|
622
|
+
Runs the page's own code: its result is untrusted.
|
|
623
|
+
|
|
483
624
|
WATCH (continuous screencast for AI video understanding)
|
|
484
625
|
watch start <url> Begin JPEG screencast at N fps to a disk ring buffer
|
|
485
626
|
(default 10fps, 5min retention, 100MB cap). Background
|
|
@@ -715,6 +856,10 @@ if (cmd === 'status') {
|
|
|
715
856
|
|
|
716
857
|
if (browser && !process.env.CHROME_BIN) {
|
|
717
858
|
env.CHROME_BIN = browser;
|
|
859
|
+
// Tells src/cdpilot.py this CHROME_BIN is our guess, not the user's: it
|
|
860
|
+
// still applies `cdpilot browser <name>` and the dev-extension rule
|
|
861
|
+
// (Chrome -> Chrome for Testing) on top of it.
|
|
862
|
+
env.CDPILOT_CHROME_BIN_AUTO = '1';
|
|
718
863
|
}
|
|
719
864
|
|
|
720
865
|
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.5",
|
|
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",
|