cdpilot 0.4.1 → 0.4.3
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 +4 -0
- package/package.json +1 -1
- package/src/cdpilot.py +207 -33
package/bin/cdpilot.js
CHANGED
|
@@ -329,6 +329,10 @@ function showHelp() {
|
|
|
329
329
|
captcha-check Detect CAPTCHA on active page (JSON output)
|
|
330
330
|
captcha-wait [s] Pause until user solves CAPTCHA (default 300s)
|
|
331
331
|
|
|
332
|
+
RELIABILITY
|
|
333
|
+
browser [name] Show or set preferred browser (chrome|brave|chromium|edge|vivaldi|auto)
|
|
334
|
+
health JSON status: alive, port, tabs, browser, today's crashes
|
|
335
|
+
|
|
332
336
|
PROJECTS
|
|
333
337
|
projects List all project browser instances
|
|
334
338
|
project-stop <id> Stop a specific project's browser
|
package/package.json
CHANGED
package/src/cdpilot.py
CHANGED
|
@@ -14,7 +14,7 @@ Environment:
|
|
|
14
14
|
CDPILOT_PROFILE Isolated browser profile directory
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
|
-
__version__ = "0.4.
|
|
17
|
+
__version__ = "0.4.3"
|
|
18
18
|
|
|
19
19
|
import asyncio
|
|
20
20
|
import json
|
|
@@ -1227,44 +1227,137 @@ async def navigate_collect(ws_url, url, network=False, console=False, glow=True)
|
|
|
1227
1227
|
|
|
1228
1228
|
# ─── Helper Functions ───
|
|
1229
1229
|
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
]
|
|
1259
|
-
else:
|
|
1230
|
+
# ─── Browser preference & detection ───────────────────────────────────────────
|
|
1231
|
+
# Map of canonical browser names to platform-specific binary candidates.
|
|
1232
|
+
# Order matters within each list: first existing path wins.
|
|
1233
|
+
BROWSER_BINARIES = {
|
|
1234
|
+
'brave': {'Darwin': ["/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"],
|
|
1235
|
+
'Linux': ["/usr/bin/brave-browser", "/usr/bin/brave"],
|
|
1236
|
+
'Windows': [os.path.expandvars(r"C:\Program Files\BraveSoftware\Brave-Browser\Application\brave.exe")]},
|
|
1237
|
+
'chrome': {'Darwin': ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"],
|
|
1238
|
+
'Linux': ["/usr/bin/google-chrome", "/usr/bin/google-chrome-stable"],
|
|
1239
|
+
'Windows': [os.path.expandvars(r"C:\Program Files\Google\Chrome\Application\chrome.exe"),
|
|
1240
|
+
os.path.expandvars(r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe")]},
|
|
1241
|
+
'vivaldi': {'Darwin': ["/Applications/Vivaldi.app/Contents/MacOS/Vivaldi"],
|
|
1242
|
+
'Linux': ["/usr/bin/vivaldi", "/usr/bin/vivaldi-stable"],
|
|
1243
|
+
'Windows': [os.path.expandvars(r"C:\Program Files\Vivaldi\Application\vivaldi.exe")]},
|
|
1244
|
+
'edge': {'Darwin': ["/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"],
|
|
1245
|
+
'Linux': ["/usr/bin/microsoft-edge"],
|
|
1246
|
+
'Windows': [os.path.expandvars(r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe")]},
|
|
1247
|
+
'chromium': {'Darwin': ["/Applications/Chromium.app/Contents/MacOS/Chromium"],
|
|
1248
|
+
'Linux': ["/usr/bin/chromium", "/usr/bin/chromium-browser", "/snap/bin/chromium"],
|
|
1249
|
+
'Windows': [os.path.expandvars(r"C:\Program Files\Chromium\Application\chromium.exe")]},
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
BROWSER_CONFIG_FILE = os.path.join(CDPILOT_HOME, 'browser.json')
|
|
1253
|
+
|
|
1254
|
+
|
|
1255
|
+
def _macos_major():
|
|
1256
|
+
"""Return macOS major version (e.g. 26) or None on other platforms."""
|
|
1257
|
+
if platform.system() != 'Darwin':
|
|
1260
1258
|
return None
|
|
1259
|
+
try:
|
|
1260
|
+
return int(platform.mac_ver()[0].split('.')[0])
|
|
1261
|
+
except (ValueError, IndexError):
|
|
1262
|
+
return None
|
|
1263
|
+
|
|
1261
1264
|
|
|
1262
|
-
|
|
1265
|
+
def _resolve_browser_name(name):
|
|
1266
|
+
"""Resolve a browser name to its binary path. Returns None if not installed."""
|
|
1267
|
+
name = (name or '').lower().strip()
|
|
1268
|
+
if name not in BROWSER_BINARIES:
|
|
1269
|
+
return None
|
|
1270
|
+
candidates = BROWSER_BINARIES[name].get(platform.system(), [])
|
|
1271
|
+
for path in candidates:
|
|
1263
1272
|
if os.path.exists(path):
|
|
1264
1273
|
return path
|
|
1265
1274
|
return None
|
|
1266
1275
|
|
|
1267
1276
|
|
|
1277
|
+
def get_browser_preference():
|
|
1278
|
+
"""Read user's persisted browser preference. Returns name or 'auto'."""
|
|
1279
|
+
if os.path.exists(BROWSER_CONFIG_FILE):
|
|
1280
|
+
try:
|
|
1281
|
+
with open(BROWSER_CONFIG_FILE) as f:
|
|
1282
|
+
return (json.load(f).get('browser') or 'auto').lower()
|
|
1283
|
+
except (OSError, ValueError):
|
|
1284
|
+
pass
|
|
1285
|
+
return 'auto'
|
|
1286
|
+
|
|
1287
|
+
|
|
1288
|
+
def _auto_browser_priority():
|
|
1289
|
+
"""Workload-aware browser priority.
|
|
1290
|
+
|
|
1291
|
+
Two axes:
|
|
1292
|
+
1. Extension workload — `cdpilot ext-install` populates dev-extensions.json.
|
|
1293
|
+
If non-empty, the user is doing extension work, so prioritise browsers
|
|
1294
|
+
that honor --load-extension for unpacked extensions:
|
|
1295
|
+
Vivaldi ✅, Brave ✅, Edge ✅, Chrome ❌ (silently drops, 147+),
|
|
1296
|
+
Chromium ✅ (but rarely installed).
|
|
1297
|
+
If the registry is empty, prefer Chrome — most stable, fastest startup,
|
|
1298
|
+
no idiosyncratic background workers.
|
|
1299
|
+
2. Platform stability — macOS 26 (Tahoe) demotes Brave because the
|
|
1300
|
+
current Brave 1.89 build crashes deterministically at ~7min uptime
|
|
1301
|
+
(SIGTRAP in ThreadPoolForegroundWorker, observed across 9+ dumps).
|
|
1302
|
+
|
|
1303
|
+
Returns (priority_list, reason_string) — reason explains *why* this order
|
|
1304
|
+
was picked, surfaced by `cdpilot browser status`.
|
|
1305
|
+
"""
|
|
1306
|
+
has_extensions = bool(get_dev_extensions())
|
|
1307
|
+
on_tahoe = _macos_major() and _macos_major() >= 26
|
|
1308
|
+
|
|
1309
|
+
if has_extensions:
|
|
1310
|
+
if on_tahoe:
|
|
1311
|
+
order = ['vivaldi', 'brave', 'edge', 'chromium', 'chrome']
|
|
1312
|
+
reason = f"extension mode (registry has dev extensions) + macOS {_macos_major()} (Brave demoted)"
|
|
1313
|
+
else:
|
|
1314
|
+
order = ['brave', 'vivaldi', 'edge', 'chromium', 'chrome']
|
|
1315
|
+
reason = "extension mode (registry has dev extensions)"
|
|
1316
|
+
else:
|
|
1317
|
+
if on_tahoe:
|
|
1318
|
+
order = ['chrome', 'vivaldi', 'edge', 'chromium', 'brave']
|
|
1319
|
+
reason = f"stability mode (no dev extensions) + macOS {_macos_major()} (Brave demoted)"
|
|
1320
|
+
else:
|
|
1321
|
+
order = ['chrome', 'brave', 'vivaldi', 'edge', 'chromium']
|
|
1322
|
+
reason = "stability mode (no dev extensions)"
|
|
1323
|
+
return order, reason
|
|
1324
|
+
|
|
1325
|
+
|
|
1326
|
+
def _find_browser():
|
|
1327
|
+
"""Locate the browser binary. Priority:
|
|
1328
|
+
1. CHROME_BIN env var (full path, backward-compatible)
|
|
1329
|
+
2. ~/.cdpilot/browser.json preference (set via `cdpilot browser <name>`)
|
|
1330
|
+
3. Auto-detection per _auto_browser_priority()
|
|
1331
|
+
"""
|
|
1332
|
+
# 1) PATH lookup for common command names (Linux/Brew installs)
|
|
1333
|
+
for b in ["brave-browser", "vivaldi", "google-chrome", "chromium-browser", "chromium"]:
|
|
1334
|
+
found = shutil.which(b)
|
|
1335
|
+
if found:
|
|
1336
|
+
# Only use PATH match as fallback — preference still wins below.
|
|
1337
|
+
path_match = found
|
|
1338
|
+
break
|
|
1339
|
+
else:
|
|
1340
|
+
path_match = None
|
|
1341
|
+
|
|
1342
|
+
# 2) User preference from config (overrides auto)
|
|
1343
|
+
pref = get_browser_preference()
|
|
1344
|
+
if pref and pref != 'auto':
|
|
1345
|
+
resolved = _resolve_browser_name(pref)
|
|
1346
|
+
if resolved:
|
|
1347
|
+
return resolved
|
|
1348
|
+
# Configured browser not installed — fall through to auto with a warning.
|
|
1349
|
+
sys.stderr.write(f"⚠️ Configured browser '{pref}' not found, falling back to auto-detect.\n")
|
|
1350
|
+
|
|
1351
|
+
# 3) Auto-detection in priority order
|
|
1352
|
+
order, _reason = _auto_browser_priority()
|
|
1353
|
+
for name in order:
|
|
1354
|
+
resolved = _resolve_browser_name(name)
|
|
1355
|
+
if resolved:
|
|
1356
|
+
return resolved
|
|
1357
|
+
|
|
1358
|
+
return path_match # Last resort
|
|
1359
|
+
|
|
1360
|
+
|
|
1268
1361
|
def _is_port_in_use(port):
|
|
1269
1362
|
"""Check if a port is actively in use."""
|
|
1270
1363
|
return not _is_port_free(port)
|
|
@@ -1512,10 +1605,28 @@ def cmd_launch():
|
|
|
1512
1605
|
# 2) `--disable-features=...` added — catches Brave-specific background
|
|
1513
1606
|
# tasks that individual `--disable-brave-*` flags don't fully kill
|
|
1514
1607
|
# (Rewards ad scanner, Sync heartbeat, News fetcher).
|
|
1608
|
+
# Per-browser profile isolation. Brave-written profiles include keys that
|
|
1609
|
+
# confuse Vivaldi/Chrome (search_engines.json missing, prefs schema diffs),
|
|
1610
|
+
# so each browser gets its own subdir. Brave keeps the original path for
|
|
1611
|
+
# backward compatibility — existing users don't lose state on upgrade.
|
|
1612
|
+
browser_basename = os.path.basename(CHROME_BIN).lower()
|
|
1613
|
+
if 'brave' in browser_basename:
|
|
1614
|
+
profile_dir = PROFILE_DIR
|
|
1615
|
+
elif 'vivaldi' in browser_basename:
|
|
1616
|
+
profile_dir = PROFILE_DIR + '-vivaldi'
|
|
1617
|
+
elif 'edge' in browser_basename or 'msedge' in browser_basename:
|
|
1618
|
+
profile_dir = PROFILE_DIR + '-edge'
|
|
1619
|
+
elif 'chromium' in browser_basename:
|
|
1620
|
+
profile_dir = PROFILE_DIR + '-chromium'
|
|
1621
|
+
else:
|
|
1622
|
+
# Default: chrome and unknown chromium-based browsers
|
|
1623
|
+
profile_dir = PROFILE_DIR + '-chrome'
|
|
1624
|
+
os.makedirs(profile_dir, exist_ok=True)
|
|
1625
|
+
|
|
1515
1626
|
chrome_args = [
|
|
1516
1627
|
CHROME_BIN,
|
|
1517
1628
|
f'--remote-debugging-port={CDP_PORT}',
|
|
1518
|
-
f'--user-data-dir={
|
|
1629
|
+
f'--user-data-dir={profile_dir}',
|
|
1519
1630
|
'--remote-allow-origins=*',
|
|
1520
1631
|
'--disable-fre', '--no-default-browser-check', '--no-first-run',
|
|
1521
1632
|
# ─── Brave-specific features (harmless on other Chromium builds) ───
|
|
@@ -2344,6 +2455,18 @@ def cmd_ext_install(source):
|
|
|
2344
2455
|
|
|
2345
2456
|
print(f'✅ Dev extension registered: {name} (v{version})')
|
|
2346
2457
|
print(f' Path: {abs_source}')
|
|
2458
|
+
|
|
2459
|
+
# Warn if the active browser silently drops --load-extension.
|
|
2460
|
+
# Chrome 147+ does this without any console message, easily wasting
|
|
2461
|
+
# hours of "why isn't my extension loading?" debugging.
|
|
2462
|
+
active = os.environ.get('CHROME_BIN') or _find_browser() or ''
|
|
2463
|
+
active_lower = os.path.basename(active).lower()
|
|
2464
|
+
if 'google chrome' in active_lower or active_lower == 'chrome' or 'google-chrome' in active_lower:
|
|
2465
|
+
sys.stderr.write(
|
|
2466
|
+
"⚠️ Active browser is Chrome — Chrome 147+ silently ignores --load-extension\n"
|
|
2467
|
+
" for unpacked extensions. Switch with: cdpilot browser vivaldi\n"
|
|
2468
|
+
)
|
|
2469
|
+
|
|
2347
2470
|
print(' Restarting browser...')
|
|
2348
2471
|
cmd_stop()
|
|
2349
2472
|
import time as _time
|
|
@@ -2627,6 +2750,56 @@ def cmd_headless(state=None):
|
|
|
2627
2750
|
print('Restart browser (stop → launch).')
|
|
2628
2751
|
|
|
2629
2752
|
|
|
2753
|
+
def cmd_browser(name=None):
|
|
2754
|
+
"""Show or set the preferred browser (chrome|brave|chromium|edge|vivaldi|auto).
|
|
2755
|
+
|
|
2756
|
+
The choice is persisted in ~/.cdpilot/browser.json and applies to all
|
|
2757
|
+
projects. `auto` (default) picks per-platform priority — on macOS 26
|
|
2758
|
+
Vivaldi is preferred over Brave due to a known Brave 1.89/macOS 26
|
|
2759
|
+
crash. `CHROME_BIN` env var still overrides if set.
|
|
2760
|
+
|
|
2761
|
+
Usage:
|
|
2762
|
+
cdpilot browser # show current + which is detected
|
|
2763
|
+
cdpilot browser vivaldi # force Vivaldi
|
|
2764
|
+
cdpilot browser auto # restore platform default
|
|
2765
|
+
"""
|
|
2766
|
+
if name is None or name.lower() == 'status':
|
|
2767
|
+
pref = get_browser_preference()
|
|
2768
|
+
resolved = _resolve_browser_name(pref) if pref != 'auto' else None
|
|
2769
|
+
auto_resolved = _find_browser()
|
|
2770
|
+
installed = [n for n in BROWSER_BINARIES if _resolve_browser_name(n)]
|
|
2771
|
+
order, reason = _auto_browser_priority()
|
|
2772
|
+
ext_count = len(get_dev_extensions())
|
|
2773
|
+
print(f"Preference: {pref}")
|
|
2774
|
+
if pref != 'auto':
|
|
2775
|
+
print(f" resolved: {resolved or '(not found — falling back to auto)'}")
|
|
2776
|
+
print(f"Auto-pick: {auto_resolved or '(none)'}")
|
|
2777
|
+
print(f" reason: {reason}")
|
|
2778
|
+
print(f" order: {' > '.join(order)}")
|
|
2779
|
+
print(f"Installed: {', '.join(installed) or '(none detected)'}")
|
|
2780
|
+
print(f"Dev exts: {ext_count} registered (run `cdpilot extensions` to list)")
|
|
2781
|
+
return
|
|
2782
|
+
|
|
2783
|
+
name = name.lower().strip()
|
|
2784
|
+
if name not in BROWSER_BINARIES and name != 'auto':
|
|
2785
|
+
valid = ', '.join(['auto'] + list(BROWSER_BINARIES.keys()))
|
|
2786
|
+
print(f"Invalid browser: {name}. Valid: {valid}", file=sys.stderr)
|
|
2787
|
+
sys.exit(1)
|
|
2788
|
+
|
|
2789
|
+
if name != 'auto' and not _resolve_browser_name(name):
|
|
2790
|
+
print(f"⚠️ Browser '{name}' not installed on this system.", file=sys.stderr)
|
|
2791
|
+
candidates = BROWSER_BINARIES[name].get(platform.system(), [])
|
|
2792
|
+
if candidates:
|
|
2793
|
+
print(f" Looked at: {candidates[0]}", file=sys.stderr)
|
|
2794
|
+
sys.exit(1)
|
|
2795
|
+
|
|
2796
|
+
os.makedirs(os.path.dirname(BROWSER_CONFIG_FILE), exist_ok=True)
|
|
2797
|
+
with open(BROWSER_CONFIG_FILE, 'w') as f:
|
|
2798
|
+
json.dump({'browser': name}, f)
|
|
2799
|
+
print(f"Browser preference: {name}")
|
|
2800
|
+
print('Restart the browser (`cdpilot stop` then any command) for the change to take effect.')
|
|
2801
|
+
|
|
2802
|
+
|
|
2630
2803
|
def cmd_health():
|
|
2631
2804
|
"""Print JSON health summary of the cdpilot browser session.
|
|
2632
2805
|
|
|
@@ -4842,6 +5015,7 @@ if __name__ == "__main__":
|
|
|
4842
5015
|
'proxy': lambda: cmd_proxy(args[0] if args else None),
|
|
4843
5016
|
'headless': lambda: cmd_headless(args[0] if args else None),
|
|
4844
5017
|
'stealth': lambda: cmd_stealth(args[0] if args else None),
|
|
5018
|
+
'browser': lambda: cmd_browser(args[0] if args else None),
|
|
4845
5019
|
'health': cmd_health,
|
|
4846
5020
|
'session': cmd_session,
|
|
4847
5021
|
'sessions': cmd_sessions,
|