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/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(os.homedir(), '.cdpilot', '.preflight-done');
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
- console.log(`\n cdpilot status (port ${port})${projLabel}\n`);
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'}\n`);
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
- console.log(' ❌ No browser connected on this port.');
253
- console.log(' Run: cdpilot launch\n');
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(os.homedir(), '.cdpilot', 'registry.json');
292
- const defaultProfile = path.join(os.homedir(), '.cdpilot', 'projects', projectId, 'profile');
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 Start browser with CDP enabled
336
- status Check browser connection
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 (chrome|brave|chromium|edge|vivaldi|auto)
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.2",
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",