cdpilot 0.4.0 → 0.4.1

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/cdpilot.py +125 -38
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cdpilot",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Zero-dependency browser automation from your terminal. One command, full control.",
5
5
  "bin": {
6
6
  "cdpilot": "./bin/cdpilot.js",
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.0"
17
+ __version__ = "0.4.1"
18
18
 
19
19
  import asyncio
20
20
  import json
@@ -1503,6 +1503,15 @@ def cmd_launch():
1503
1503
  proj_label = f' [{PROJECT_ID}]' if PROJECT_ID else ''
1504
1504
  print(f'Launching browser (isolated session, port {CDP_PORT}){proj_label}...')
1505
1505
 
1506
+ # ─── Stability fixes (2026-04-21) ───
1507
+ # Brave on Apple Silicon crashed with SIGTRAP/EXC_BREAKPOINT deterministically
1508
+ # at ~7 min uptime on ThreadPoolForegroundWorker. Two changes:
1509
+ # 1) `--disable-gpu-compositing` removed — conflicts with Metal accel on
1510
+ # M-series. Browser falls back to GPU-accel rendering which is the
1511
+ # Chromium default and matches what websites expect.
1512
+ # 2) `--disable-features=...` added — catches Brave-specific background
1513
+ # tasks that individual `--disable-brave-*` flags don't fully kill
1514
+ # (Rewards ad scanner, Sync heartbeat, News fetcher).
1506
1515
  chrome_args = [
1507
1516
  CHROME_BIN,
1508
1517
  f'--remote-debugging-port={CDP_PORT}',
@@ -1521,6 +1530,14 @@ def cmd_launch():
1521
1530
  '--disable-tor',
1522
1531
  '--disable-ipfs',
1523
1532
  '--disable-brave-extension',
1533
+ # Feature-level disables — catch background workers the
1534
+ # single-purpose flags above miss. Comma-joined string = one arg.
1535
+ '--disable-features=' + ','.join([
1536
+ 'BraveRewards', 'BraveAds', 'BraveSync', 'BraveNewsToday',
1537
+ 'BraveVPN', 'BraveWalletBubble', 'SpeedReader', 'Tor',
1538
+ 'IPFSCompanion', 'Translate', 'OptimizationGuideModelDownloading',
1539
+ 'InterestFeedContentSuggestions', 'CalculateNativeWinOcclusion',
1540
+ ]),
1524
1541
  # ─── Chromium performance flags ───
1525
1542
  '--disable-background-networking',
1526
1543
  '--disable-background-timer-throttling',
@@ -1542,7 +1559,9 @@ def cmd_launch():
1542
1559
  '--safebrowsing-disable-auto-update',
1543
1560
  '--password-store=basic',
1544
1561
  # ─── GPU / rendering ───
1545
- '--disable-gpu-compositing',
1562
+ # --disable-gpu-compositing REMOVED: caused EXC_BREAKPOINT on Apple
1563
+ # Silicon Brave 147+ after ~7min. Let the browser use its default
1564
+ # (Metal-accelerated) compositor.
1546
1565
  '--disable-smooth-scrolling',
1547
1566
  '--new-window', 'about:blank',
1548
1567
  ]
@@ -2225,52 +2244,71 @@ def cmd_session_close(session_id=None):
2225
2244
 
2226
2245
 
2227
2246
  def cmd_extensions():
2228
- """List installed extensions."""
2229
- ext_dir = os.path.join(PROFILE_DIR, "Default", "Extensions")
2230
- if not os.path.isdir(ext_dir):
2231
- print("No extensions installed.")
2232
- return
2247
+ """List installed extensions (packed via Chrome Web Store + dev mode unpacked).
2233
2248
 
2234
- prefs_path = os.path.join(PROFILE_DIR, "Default", "Preferences")
2249
+ Two sources are checked independently — neither absence should hide the other.
2250
+ Previously an early-return on missing Default/Extensions/ silently swallowed
2251
+ dev-mode entries written by `ext-install`.
2252
+ """
2253
+ # 1) Packed (CRX) extensions live under Default/Extensions and are described
2254
+ # in Default/Preferences. Only present after the user installs from the
2255
+ # Chrome Web Store; ext-install does NOT write here.
2256
+ ext_dir = os.path.join(PROFILE_DIR, "Default", "Extensions")
2257
+ packed_ids = []
2235
2258
  ext_names = {}
2236
- if os.path.exists(prefs_path):
2237
- try:
2238
- with open(prefs_path) as f:
2239
- prefs = json.load(f)
2240
- settings = prefs.get("extensions", {}).get("settings", {})
2241
- for ext_id, info in settings.items():
2242
- manifest = info.get("manifest", {})
2243
- ext_names[ext_id] = {
2244
- "name": manifest.get("name", ext_id),
2245
- "version": manifest.get("version", "?"),
2246
- "enabled": info.get("state", 1) == 1,
2247
- "path": info.get("path", ""),
2248
- }
2249
- except Exception:
2250
- pass
2259
+ if os.path.isdir(ext_dir):
2260
+ prefs_path = os.path.join(PROFILE_DIR, "Default", "Preferences")
2261
+ if os.path.exists(prefs_path):
2262
+ try:
2263
+ with open(prefs_path) as f:
2264
+ prefs = json.load(f)
2265
+ settings = prefs.get("extensions", {}).get("settings", {})
2266
+ for ext_id, info in settings.items():
2267
+ manifest = info.get("manifest", {})
2268
+ ext_names[ext_id] = {
2269
+ "name": manifest.get("name", ext_id),
2270
+ "version": manifest.get("version", "?"),
2271
+ "enabled": info.get("state", 1) == 1,
2272
+ }
2273
+ except Exception:
2274
+ pass
2275
+ packed_ids = [d for d in os.listdir(ext_dir) if not d.startswith(".")]
2276
+
2277
+ # 2) Dev-mode (unpacked) extensions registered by `ext-install` —
2278
+ # loaded into the browser via --load-extension on each launch.
2279
+ dev_exts = get_dev_extensions()
2251
2280
 
2252
- ext_ids = [d for d in os.listdir(ext_dir) if not d.startswith(".")]
2253
- if not ext_ids:
2281
+ if not packed_ids and not dev_exts:
2254
2282
  print("No extensions installed.")
2255
2283
  return
2256
2284
 
2257
- for ext_id in sorted(ext_ids):
2258
- info = ext_names.get(ext_id, {})
2259
- name = info.get("name", ext_id)
2260
- version = info.get("version", "?")
2261
- enabled = info.get("enabled", True)
2262
- status = "✅" if enabled else "⏸️"
2263
- print(f" {status} {name} (v{version})")
2264
- print(f" ID: {ext_id}")
2265
-
2266
- print(f"\n{len(ext_ids)} extensions")
2285
+ if packed_ids:
2286
+ for ext_id in sorted(packed_ids):
2287
+ info = ext_names.get(ext_id, {})
2288
+ name = info.get("name", ext_id)
2289
+ version = info.get("version", "?")
2290
+ enabled = info.get("enabled", True)
2291
+ status = "✅" if enabled else "⏸️"
2292
+ print(f" {status} {name} (v{version})")
2293
+ print(f" ID: {ext_id}")
2294
+ print(f"\n{len(packed_ids)} packed extension{'s' if len(packed_ids) != 1 else ''}")
2267
2295
 
2268
- dev_exts = get_dev_extensions()
2269
2296
  if dev_exts:
2270
- print(f'\nDev Mode Extensions ({len(dev_exts)}):')
2297
+ if packed_ids:
2298
+ print()
2299
+ print(f"Dev Mode Extensions ({len(dev_exts)}):")
2271
2300
  for i, path in enumerate(dev_exts):
2272
2301
  exists = '✅' if os.path.isdir(path) else '❌ (directory not found)'
2273
- print(f' {exists} [{i}] {path}')
2302
+ # Try to read manifest for friendlier output
2303
+ label = os.path.basename(path.rstrip('/'))
2304
+ try:
2305
+ with open(os.path.join(path, 'manifest.json')) as f:
2306
+ mf = json.load(f)
2307
+ label = f"{mf.get('name', label)} (v{mf.get('version', '?')})"
2308
+ except Exception:
2309
+ pass
2310
+ print(f" {exists} [{i}] {label}")
2311
+ print(f" {path}")
2274
2312
 
2275
2313
 
2276
2314
  def cmd_ext_install(source):
@@ -2589,6 +2627,54 @@ def cmd_headless(state=None):
2589
2627
  print('Restart browser (stop → launch).')
2590
2628
 
2591
2629
 
2630
+ def cmd_health():
2631
+ """Print JSON health summary of the cdpilot browser session.
2632
+
2633
+ Output keys:
2634
+ alive — bool, CDP /json/version reachable
2635
+ port — int, current CDP port
2636
+ project_id — str|null, project identifier (multi-instance)
2637
+ tabs — int, count of page targets (when alive)
2638
+ browser — str|null, version string from /json/version
2639
+ crashes_today — int, Brave crash dump count from macOS today
2640
+ stealth — bool, current stealth config
2641
+ uptime_warning — str|null, hint when browser is alive but very old
2642
+
2643
+ Exit codes: 0 = alive, 2 = down. Designed for shell watchdog loops:
2644
+ `until cdpilot health >/dev/null; do cdpilot launch; sleep 2; done`
2645
+ """
2646
+ import datetime as _dt
2647
+ import glob as _glob
2648
+
2649
+ info = {
2650
+ 'alive': False,
2651
+ 'port': CDP_PORT,
2652
+ 'project_id': PROJECT_ID,
2653
+ 'tabs': 0,
2654
+ 'browser': None,
2655
+ 'crashes_today': 0,
2656
+ 'stealth': get_stealth_config(),
2657
+ 'uptime_warning': None,
2658
+ }
2659
+ ver = cdp_get('/json/version')
2660
+ if ver:
2661
+ info['alive'] = True
2662
+ info['browser'] = ver.get('Browser') or ver.get('browser') or ''
2663
+ targets = cdp_get('/json') or []
2664
+ info['tabs'] = sum(1 for t in targets if t.get('type') == 'page')
2665
+
2666
+ # Today's crash count from macOS DiagnosticReports (Brave only).
2667
+ if platform.system() == 'Darwin':
2668
+ today = _dt.date.today().strftime('%Y-%m-%d')
2669
+ pattern = os.path.expanduser(f'~/Library/Logs/DiagnosticReports/Brave Browser-{today}-*.ips')
2670
+ info['crashes_today'] = len(_glob.glob(pattern))
2671
+ if info['crashes_today'] >= 3:
2672
+ info['uptime_warning'] = f"{info['crashes_today']} Brave crashes today — consider `cdpilot stop` then relaunch"
2673
+
2674
+ print(json.dumps(info, ensure_ascii=False))
2675
+ sys.exit(0 if info['alive'] else 2)
2676
+
2677
+
2592
2678
  def cmd_stealth(state=None):
2593
2679
  """Toggle stealth fingerprint patches.
2594
2680
 
@@ -4756,6 +4842,7 @@ if __name__ == "__main__":
4756
4842
  'proxy': lambda: cmd_proxy(args[0] if args else None),
4757
4843
  'headless': lambda: cmd_headless(args[0] if args else None),
4758
4844
  'stealth': lambda: cmd_stealth(args[0] if args else None),
4845
+ 'health': cmd_health,
4759
4846
  'session': cmd_session,
4760
4847
  'sessions': cmd_sessions,
4761
4848
  'session-close': lambda: cmd_session_close(args[0] if args else None),