cdpilot 0.4.2 → 0.4.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/README.md +65 -12
- package/package.json +1 -1
- package/src/cdpilot.py +54 -12
package/README.md
CHANGED
|
@@ -37,22 +37,34 @@ AI agents and developers need browser control that **just works**:
|
|
|
37
37
|
- **AI control warning** — Red toast notification appears when you hover during active automation
|
|
38
38
|
- **Privacy-first** — Everything runs locally. No data leaves your machine
|
|
39
39
|
|
|
40
|
-
###
|
|
40
|
+
### Browser Selection (Workload-Aware Auto-Pick)
|
|
41
41
|
|
|
42
|
-
cdpilot
|
|
42
|
+
cdpilot picks the right browser for what you're doing. `auto` (default) is a
|
|
43
|
+
two-axis policy — extension workload × platform stability:
|
|
43
44
|
|
|
44
|
-
|
|
|
45
|
-
|
|
46
|
-
|
|
|
47
|
-
|
|
|
48
|
-
| **Fingerprint protection** | Native | None | Better privacy for automated sessions |
|
|
49
|
-
| **Chromium-based** | Full CDP support | Full CDP support | Same DevTools Protocol, same power |
|
|
50
|
-
| **Open source** | Yes (MPL 2.0) | Chromium yes, Chrome no | Transparent, auditable |
|
|
51
|
-
| **Resource usage** | Lower memory | Higher memory | Better for running alongside your work |
|
|
45
|
+
| Your workload | Auto-pick order |
|
|
46
|
+
|---|---|
|
|
47
|
+
| Has extensions registered (`ext-install`) | vivaldi → brave → edge → chromium → chrome |
|
|
48
|
+
| No extensions (pure automation) | chrome → vivaldi → edge → chromium → brave |
|
|
52
49
|
|
|
53
|
-
|
|
50
|
+
Override anytime:
|
|
54
51
|
|
|
55
|
-
|
|
52
|
+
```bash
|
|
53
|
+
cdpilot browser # show current pick + reason
|
|
54
|
+
cdpilot browser vivaldi # pin to Vivaldi
|
|
55
|
+
cdpilot browser auto # restore smart default
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Why the split?**
|
|
59
|
+
- **Chrome 147+ silently drops `--load-extension`** for unpacked extensions
|
|
60
|
+
(no error, no warning). Verified — `chrome://extensions` shows 0 items.
|
|
61
|
+
- **Vivaldi, Brave, Edge, Chromium** honor `--load-extension` (tested).
|
|
62
|
+
- On **macOS 26 (Tahoe)** Brave 1.89 crashes deterministically at ~7min
|
|
63
|
+
uptime (SIGTRAP in ThreadPoolForegroundWorker). cdpilot detects the OS
|
|
64
|
+
and demotes Brave automatically until a fixed Brave release ships.
|
|
65
|
+
|
|
66
|
+
Each browser gets its own isolated profile (`~/.cdpilot/.../profile-vivaldi`
|
|
67
|
+
etc.) so switching never causes prefs corruption.
|
|
56
68
|
|
|
57
69
|
## Installation
|
|
58
70
|
|
|
@@ -187,6 +199,47 @@ cdpilot dialog auto-accept # Auto-accept dialogs
|
|
|
187
199
|
cdpilot permission grant geo # Grant geolocation
|
|
188
200
|
```
|
|
189
201
|
|
|
202
|
+
### Stealth & CAPTCHA
|
|
203
|
+
|
|
204
|
+
Zero-dependency anti-fingerprint layer — patches `navigator.webdriver`,
|
|
205
|
+
`chrome.runtime`, plugins (proper `PluginArray` inheritance), WebGL
|
|
206
|
+
vendor/renderer, permissions, hardware concurrency, and the `Worker`
|
|
207
|
+
constructor. Injected via `Page.addScriptToEvaluateOnNewDocument` before
|
|
208
|
+
any page script runs. Disabled by default; opt-in.
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
cdpilot stealth on # enable fingerprint patches (opt-in)
|
|
212
|
+
cdpilot stealth off # disable (default)
|
|
213
|
+
cdpilot stealth status # show which patches are applied
|
|
214
|
+
|
|
215
|
+
cdpilot captcha-check # JSON detection of Turnstile/hCaptcha/reCAPTCHA/
|
|
216
|
+
# DataDome/PerimeterX/Arkose/GeeTest. Exit 0/3
|
|
217
|
+
cdpilot captcha-wait [sec] # block until user solves (interactive)
|
|
218
|
+
# or poll with JSON stream (non-interactive)
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Verified against public bot-detection panels:
|
|
222
|
+
- **bot.sannysoft.com:** 24/24 PASS (WebDriver, Chrome obj, Plugins as PluginArray, WebGL, PHANTOM_*, HEADCHR_*, SELENIUM_DRIVER)
|
|
223
|
+
- **bot.incolumitas.com** intoli: 6/6 OK — new-tests: 6/7 OK (one FAIL = pure CDP presence, cannot be JS-patched)
|
|
224
|
+
- **nowsecure.nl** (Cloudflare full challenge): passed
|
|
225
|
+
- **arh.antoinevastel.com/areyouheadless:** "You are not Chrome headless"
|
|
226
|
+
|
|
227
|
+
### Reliability
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
cdpilot browser [name|auto] # workload-aware browser selection
|
|
231
|
+
cdpilot health # JSON: alive, port, tabs, browser, today's crashes
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
`cdpilot health` is designed for shell watchdogs:
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
until cdpilot health >/dev/null; do cdpilot launch; sleep 2; done
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Surfaces today's Brave crash count from `~/Library/Logs/DiagnosticReports/`
|
|
241
|
+
on macOS — spot degradation before your automation silently stalls.
|
|
242
|
+
|
|
190
243
|
## Use with AI Agents
|
|
191
244
|
|
|
192
245
|
cdpilot is designed to be called by AI agents as a tool:
|
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.4"
|
|
18
18
|
|
|
19
19
|
import asyncio
|
|
20
20
|
import json
|
|
@@ -1286,15 +1286,41 @@ def get_browser_preference():
|
|
|
1286
1286
|
|
|
1287
1287
|
|
|
1288
1288
|
def _auto_browser_priority():
|
|
1289
|
-
"""
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
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`.
|
|
1294
1305
|
"""
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
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
|
|
1298
1324
|
|
|
1299
1325
|
|
|
1300
1326
|
def _find_browser():
|
|
@@ -1323,7 +1349,8 @@ def _find_browser():
|
|
|
1323
1349
|
sys.stderr.write(f"⚠️ Configured browser '{pref}' not found, falling back to auto-detect.\n")
|
|
1324
1350
|
|
|
1325
1351
|
# 3) Auto-detection in priority order
|
|
1326
|
-
|
|
1352
|
+
order, _reason = _auto_browser_priority()
|
|
1353
|
+
for name in order:
|
|
1327
1354
|
resolved = _resolve_browser_name(name)
|
|
1328
1355
|
if resolved:
|
|
1329
1356
|
return resolved
|
|
@@ -2428,6 +2455,18 @@ def cmd_ext_install(source):
|
|
|
2428
2455
|
|
|
2429
2456
|
print(f'✅ Dev extension registered: {name} (v{version})')
|
|
2430
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
|
+
|
|
2431
2470
|
print(' Restarting browser...')
|
|
2432
2471
|
cmd_stop()
|
|
2433
2472
|
import time as _time
|
|
@@ -2729,13 +2768,16 @@ def cmd_browser(name=None):
|
|
|
2729
2768
|
resolved = _resolve_browser_name(pref) if pref != 'auto' else None
|
|
2730
2769
|
auto_resolved = _find_browser()
|
|
2731
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())
|
|
2732
2773
|
print(f"Preference: {pref}")
|
|
2733
2774
|
if pref != 'auto':
|
|
2734
2775
|
print(f" resolved: {resolved or '(not found — falling back to auto)'}")
|
|
2735
2776
|
print(f"Auto-pick: {auto_resolved or '(none)'}")
|
|
2777
|
+
print(f" reason: {reason}")
|
|
2778
|
+
print(f" order: {' > '.join(order)}")
|
|
2736
2779
|
print(f"Installed: {', '.join(installed) or '(none detected)'}")
|
|
2737
|
-
|
|
2738
|
-
print(f"Note: macOS {_macos_major()} demotes Brave in auto-pick (Brave 1.89 crashes ~7min).")
|
|
2780
|
+
print(f"Dev exts: {ext_count} registered (run `cdpilot extensions` to list)")
|
|
2739
2781
|
return
|
|
2740
2782
|
|
|
2741
2783
|
name = name.lower().strip()
|