cdpilot 0.9.3 → 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,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
- console.log(` ${idleCloseLabel(port)}\n`);
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
- console.log(' ❌ No browser connected on this port.');
254
- 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
+ }
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 = process.env.CDPILOT_HOME || path.join(os.homedir(), '.cdpilot');
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(os.homedir(), '.cdpilot', 'registry.json');
320
- 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');
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>] Start browser with CDP enabled (--idle-close or
367
- CDPILOT_IDLE_CLOSE: close it after <min> idle minutes)
368
- 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)
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 (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.
463
592
  health JSON status: alive, port, tabs, browser, today's crashes
464
593
 
465
594
  PROJECTS
@@ -480,6 +609,17 @@ function showHelp() {
480
609
  params are redacted. CDPILOT_LOG=0 turns it off;
481
610
  CDPILOT_LOG_DAYS (default 14) sets how many days are kept.
482
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
+
483
623
  WATCH (continuous screencast for AI video understanding)
484
624
  watch start <url> Begin JPEG screencast at N fps to a disk ring buffer
485
625
  (default 10fps, 5min retention, 100MB cap). Background
@@ -715,6 +855,10 @@ if (cmd === 'status') {
715
855
 
716
856
  if (browser && !process.env.CHROME_BIN) {
717
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';
718
862
  }
719
863
 
720
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",
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",