chrome-bridge-mcp 1.24.0 → 1.25.0
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 +12 -9
- package/extension/console-capture.js +13 -0
- package/extension/lib/capture-pacing.js +7 -3
- package/extension/lib/css-cascade.js +26 -1
- package/extension/lib/page-fingerprint.js +24 -1
- package/extension/manifest.json +1 -1
- package/extension/network-hook.js +100 -0
- package/extension/service-worker.js +143 -136
- package/install.sh +4 -4
- package/package.json +2 -2
- package/server/cli.js +49 -1
- package/server/formatters.js +9 -6
- package/server/index.js +6 -0
- package/server/protocol.js +1 -1
- package/server/sourcemaps.js +39 -3
- package/server/tools.js +277 -82
- package/skills/chrome-bridge/SKILL.md +62 -270
- package/skills/chrome-bridge/recipes/accessibility-keyboard.md +7 -0
- package/skills/chrome-bridge/recipes/checkout-test-card.md +11 -0
- package/skills/chrome-bridge/recipes/cookies-consent-banner.md +9 -0
- package/skills/chrome-bridge/recipes/design-tokens-fonts-colours.md +13 -0
- package/skills/chrome-bridge/recipes/email-deliverability.md +4 -0
- package/skills/chrome-bridge/recipes/error-states-backend-will-not.md +10 -0
- package/skills/chrome-bridge/recipes/evidence-file-incident.md +10 -0
- package/skills/chrome-bridge/recipes/export-from-back-office-analyse.md +4 -0
- package/skills/chrome-bridge/recipes/finding-setting-unknown-admin.md +7 -0
- package/skills/chrome-bridge/recipes/form-end-end-until-email-arrives.md +12 -0
- package/skills/chrome-bridge/recipes/gutenberg-block-editors.md +5 -0
- package/skills/chrome-bridge/recipes/hand-browser-user.md +9 -0
- package/skills/chrome-bridge/recipes/heavy-web-apps.md +14 -0
- package/skills/chrome-bridge/recipes/hosting-panel-logs.md +6 -0
- package/skills/chrome-bridge/recipes/page-audit-before-release.md +7 -0
- package/skills/chrome-bridge/recipes/pagespeed-without-api-key.md +6 -0
- package/skills/chrome-bridge/recipes/pixel-ga4-gtm-events.md +8 -0
- package/skills/chrome-bridge/recipes/post-deploy-check.md +6 -0
- package/skills/chrome-bridge/recipes/redirects-after-migration.md +7 -0
- package/skills/chrome-bridge/recipes/second-pair-eyes-before.md +10 -0
- package/skills/chrome-bridge/recipes/security-headers-https.md +3 -0
- package/skills/chrome-bridge/recipes/seo-links-structured-data.md +5 -0
- package/skills/chrome-bridge/recipes/smoke-test-recorded-flow.md +10 -0
- package/skills/chrome-bridge/recipes/three-viewports.md +8 -0
- package/skills/chrome-bridge/recipes/visual-regression-between-two.md +9 -0
- package/skills/chrome-bridge/recipes/watch-how-do-it.md +11 -0
- package/skills/chrome-bridge/recipes/watch-page-tell-when.md +10 -0
- package/skills/chrome-bridge/recipes/which-plugin-slows-page.md +8 -0
package/README.md
CHANGED
|
@@ -9,15 +9,16 @@ with your extensions and your cookies — works in it, and hands it back. What
|
|
|
9
9
|
only a person can do (a login, a 2FA code, a CAPTCHA, a choice) comes back to
|
|
10
10
|
you through `handoff`: a banner in the page, you act, the agent continues.**
|
|
11
11
|
|
|
12
|
-
Measured against the official browser extension (paired runs,
|
|
13
|
-
per task, same model): filling a form,
|
|
14
|
-
finding one row in a 1,500-row table, 2.3×
|
|
15
|
-
|
|
12
|
+
Measured against the official browser extension (paired runs, 29/09/2026, n=5
|
|
13
|
+
per task, same model): filling a form, 4.7× fewer turns and 2.8× lower cost;
|
|
14
|
+
finding one row in a 1,500-row table, 2.3× fewer turns and 3.4× lower cost;
|
|
15
|
+
debugging a broken page, 4.7× fewer turns and 3.2× lower cost. 15 correct
|
|
16
|
+
answers out of 15, against 12 ([bench/RESULTS.md](bench/RESULTS.md)). ~3× the toolset and no paid plan. 60
|
|
16
17
|
web-development tools (navigation, DOM inspection, visual regression, audits,
|
|
17
18
|
network mocking) over a local WebSocket bridge, plus a headless instance for
|
|
18
19
|
CI. Self-hosted, local-only. Works on ChromeOS.
|
|
19
20
|
|
|
20
|
-

|
|
21
22
|
|
|
22
23
|
## Quickstart
|
|
23
24
|
|
|
@@ -94,7 +95,7 @@ or let you pick one or more elements on the page.
|
|
|
94
95
|
| | Chrome Bridge | Claude in Chrome | Chrome DevTools MCP | Playwright MCP |
|
|
95
96
|
|---|---|---|---|---|
|
|
96
97
|
| **ChromeOS / Crostini** | **Yes** (real host) | No | Container only | Container only |
|
|
97
|
-
| **Tools** | **60** (
|
|
98
|
+
| **Tools** | **60** (43 core) | 22 | 29 default (56 with flags) | 24 core (71 total) |
|
|
98
99
|
| **Requires paid plan** | **No** | Yes (Pro+) | No | No |
|
|
99
100
|
| **Network mocking** | **Yes** (stub/headers) | No | No | Yes |
|
|
100
101
|
| **Visual regression** | **Yes** (`screenshot_diff`) | No | No | No |
|
|
@@ -163,8 +164,10 @@ user-script toggle isn't available.
|
|
|
163
164
|
|
|
164
165
|
## Tools
|
|
165
166
|
|
|
166
|
-
60 in total
|
|
167
|
-
|
|
167
|
+
60 in total. `core` (43 tools, every tool used in 101 real sessions) loads by
|
|
168
|
+
default; the other 17 sit in six optional caps that the agent switches on
|
|
169
|
+
mid-session with `get_status({enable: ["visual"]})`, with no restart
|
|
170
|
+
(`--caps` still sets them at startup).
|
|
168
171
|
|
|
169
172
|

|
|
170
173
|
|
|
@@ -202,7 +205,7 @@ Environment variables, each with a matching CLI flag:
|
|
|
202
205
|
| `CHROME_BRIDGE_PORT` | `8765` | |
|
|
203
206
|
| `CHROME_BRIDGE_HOST` / `--host` | `127.0.0.1` | `0.0.0.0` **only** where the browser lives outside the container (ChromeOS/Crostini port-forward) — and only with a token |
|
|
204
207
|
| `CHROME_BRIDGE_TOKEN` | unset | Required on both `ext_init` and `relay_init`. Strongly recommended whenever the bind isn't loopback |
|
|
205
|
-
| `CHROME_BRIDGE_CAPS` / `--caps` | `core` | `core`, `audits`, `visual`, `network`, `storage`, `dom`, `files`, `all`.
|
|
208
|
+
| `CHROME_BRIDGE_CAPS` / `--caps` | `core` | `core`, `audits`, `visual`, `network`, `storage`, `dom`, `files`, `all`. Optional caps can also be switched on at runtime with `get_status({enable})` |
|
|
206
209
|
| `CHROME_BRIDGE_NO_JS` / `--no-js` | unset | No arbitrary JavaScript in the page: `execute_js` and `modify_dom` leave the schema, `wait_for(condition=function)` and `javascript:`/`data:` URLs are refused. `get_status` reports `js_evaluation` |
|
|
207
210
|
| `CHROME_BRIDGE_READ_ROOT` / `--read-root` | unset | Where `upload_file` may read from. Unset: any file except keys and credentials (`~/.ssh`, `~/.aws`, `~/.gnupg`, `.env*`, `id_*`, `*.pem`, `*.key`, …), symlinks resolved first. Set: only files under this directory, whatever their name — the way to upload a certificate key on purpose |
|
|
208
211
|
| `CHROME_BRIDGE_BROWSER` | unset | Launch mode only: the browser binary, e.g. `C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe`. Unset: the first of Chromium, Edge, Brave, Google Chrome found in the standard paths (on Linux Chromium, then Google Chrome). Google Chrome 137+ cannot load the extension |
|
|
@@ -12,7 +12,11 @@
|
|
|
12
12
|
window.__chromeBridge_consoleHooked = true;
|
|
13
13
|
window.__chromeBridge_consoleLogs = [];
|
|
14
14
|
const MAX = 1000;
|
|
15
|
+
// seq: cursore per read_console({since}), che restituisce solo le voci nuove
|
|
16
|
+
// senza cancellare niente.
|
|
17
|
+
let seq = 0;
|
|
15
18
|
const push = (entry) => {
|
|
19
|
+
entry.seq = ++seq;
|
|
16
20
|
const buf = window.__chromeBridge_consoleLogs;
|
|
17
21
|
if (buf.length >= MAX) buf.shift(); // ring buffer: tieni i più recenti, non i primi 1000
|
|
18
22
|
buf.push(entry);
|
|
@@ -42,6 +46,15 @@
|
|
|
42
46
|
}
|
|
43
47
|
window.addEventListener('error', (e) => {
|
|
44
48
|
try {
|
|
49
|
+
// Un'immagine o uno script che non si carica arriva qui in fase di
|
|
50
|
+
// cattura, con target l'elemento e nessun messaggio: prima diventava
|
|
51
|
+
// «Uncaught at ?:0:0». Ora dice quale risorsa.
|
|
52
|
+
const t = e.target;
|
|
53
|
+
if (t && t !== window && t.tagName) {
|
|
54
|
+
const url = t.currentSrc || t.src || t.href || '';
|
|
55
|
+
push({ level: 'error', args: [`Failed to load ${String(t.tagName).toLowerCase()}${url ? ` ${url}` : ''}`], timestamp: Date.now() });
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
45
58
|
// Con la colonna il frame diventa url:riga:colonna, risolvibile dalla source map.
|
|
46
59
|
const stack = errorText(e.error);
|
|
47
60
|
push({
|
|
@@ -5,11 +5,15 @@
|
|
|
5
5
|
* 520 ms dalla precedente. Orologio e sleep iniettabili per i test.
|
|
6
6
|
*/
|
|
7
7
|
export function createPacer({ minIntervalMs = 520, now = Date.now, sleep = (ms) => new Promise((r) => setTimeout(r, ms)) } = {}) {
|
|
8
|
-
|
|
8
|
+
// Lo slot si prenota prima di dormire: due catture concorrenti leggevano lo
|
|
9
|
+
// stesso "ultimo" e si svegliavano insieme, e la seconda sforava la quota.
|
|
10
|
+
let next = -Infinity;
|
|
9
11
|
const pace = async () => {
|
|
10
|
-
const
|
|
12
|
+
const t = now();
|
|
13
|
+
const slot = Math.max(t, next);
|
|
14
|
+
next = slot + minIntervalMs;
|
|
15
|
+
const wait = slot - t;
|
|
11
16
|
if (wait > 0) await sleep(wait);
|
|
12
|
-
last = now();
|
|
13
17
|
return wait;
|
|
14
18
|
};
|
|
15
19
|
pace.minIntervalMs = minIntervalMs;
|
|
@@ -290,7 +290,21 @@
|
|
|
290
290
|
const out = [];
|
|
291
291
|
for (let i = 0; i < style.length; i += 1) {
|
|
292
292
|
const prop = style[i];
|
|
293
|
-
|
|
293
|
+
let value = style.getPropertyValue(prop);
|
|
294
|
+
// `background: var(--x)`: per la specifica CSSOM le proprietà singole
|
|
295
|
+
// valgono "" (sostituzione in sospeso). Il testo vero sta nella
|
|
296
|
+
// abbreviata: background-color → background, border-top-color →
|
|
297
|
+
// border-top → border.
|
|
298
|
+
let shorthand;
|
|
299
|
+
if (value === '' && !prop.startsWith('--')) {
|
|
300
|
+
const parts = prop.split('-');
|
|
301
|
+
for (let k = parts.length - 1; k >= 1 && value === ''; k -= 1) {
|
|
302
|
+
const sh = parts.slice(0, k).join('-');
|
|
303
|
+
const v = style.getPropertyValue(sh);
|
|
304
|
+
if (v) { value = v; shorthand = sh; }
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
out.push({ prop, value, ...(shorthand && { shorthand }), important: style.getPropertyPriority(prop) === 'important' });
|
|
294
308
|
}
|
|
295
309
|
return out;
|
|
296
310
|
};
|
|
@@ -330,6 +344,7 @@
|
|
|
330
344
|
const fmtEntry = (list, computed, inheritedFrom) => {
|
|
331
345
|
const [winner, ...rest] = list;
|
|
332
346
|
const entry = { value: winner.value };
|
|
347
|
+
if (winner.shorthand) entry.shorthand = winner.shorthand;
|
|
333
348
|
if (winner.important) entry.important = true;
|
|
334
349
|
if (computed != null) entry.computed = computed;
|
|
335
350
|
if (inheritedFrom) entry.inherited_from = inheritedFrom;
|
|
@@ -386,6 +401,16 @@
|
|
|
386
401
|
}
|
|
387
402
|
}
|
|
388
403
|
|
|
404
|
+
// var(--x) senza --x definita sull'elemento (né ereditata): la proprietà
|
|
405
|
+
// cade sul valore iniziale. Detto qui, il modello non spende una seconda
|
|
406
|
+
// chiamata a cercare dove --x sia definita (benchmark debug del 28/09).
|
|
407
|
+
for (const entry of Object.values(properties)) {
|
|
408
|
+
const names = [...String(entry.value ?? '').matchAll(/var\(\s*(--[\w-]+)\s*(,)?/g)]
|
|
409
|
+
.filter((m) => !m[2]).map((m) => m[1]);
|
|
410
|
+
const missing = [...new Set(names)].filter((n) => String(computedOf(n) ?? '').trim() === '');
|
|
411
|
+
if (missing.length) entry.undefined_vars = missing;
|
|
412
|
+
}
|
|
413
|
+
|
|
389
414
|
const sorted = {};
|
|
390
415
|
for (const k of Object.keys(properties).sort()) sorted[k] = properties[k];
|
|
391
416
|
const out = { selector: opts.selector ?? null, element: describe(el), matched_rules: matched };
|
|
@@ -7,8 +7,31 @@
|
|
|
7
7
|
* sorgente e la esegue nella pagina, dove niente di questo modulo esiste.
|
|
8
8
|
* Prende `doc` per essere testabile su un DOM finto (test/unit).
|
|
9
9
|
*/
|
|
10
|
-
export function pageFingerprint(doc) {
|
|
10
|
+
export function pageFingerprint(doc, settle) {
|
|
11
11
|
const d = doc || document;
|
|
12
|
+
// Con settle l'impronta aspetta che il DOM si fermi: nessuna mutazione per
|
|
13
|
+
// quiet_ms, al più max_ms. Sostituisce la pausa fissa dopo un click (150 ms
|
|
14
|
+
// anche su una pagina già ferma). Su una pagina nascosta i timer sono
|
|
15
|
+
// strozzati a 1 s: niente attesa, e lo dice.
|
|
16
|
+
if (settle && typeof MutationObserver === 'function') {
|
|
17
|
+
const quiet = settle.quiet_ms ?? 50;
|
|
18
|
+
const max = settle.max_ms ?? 250;
|
|
19
|
+
if (d.hidden) return { ...pageFingerprint(d), settled_ms: 0, hidden: true };
|
|
20
|
+
return new Promise((resolve) => {
|
|
21
|
+
const t0 = performance.now();
|
|
22
|
+
let last = t0;
|
|
23
|
+
const mo = new MutationObserver(() => { last = performance.now(); });
|
|
24
|
+
try { mo.observe(d.documentElement, { subtree: true, childList: true, attributes: true, characterData: true }); } catch { /* niente da osservare */ }
|
|
25
|
+
const tick = () => {
|
|
26
|
+
const now = performance.now();
|
|
27
|
+
if (now - last >= quiet || now - t0 >= max) {
|
|
28
|
+
mo.disconnect();
|
|
29
|
+
resolve({ ...pageFingerprint(d), settled_ms: Math.round(now - t0) });
|
|
30
|
+
} else setTimeout(tick, 10);
|
|
31
|
+
};
|
|
32
|
+
setTimeout(tick, quiet);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
12
35
|
const count = (sel) => { try { return d.querySelectorAll(sel).length; } catch { return 0; } };
|
|
13
36
|
let checked = 0;
|
|
14
37
|
try { for (const el of d.querySelectorAll('input')) if (el.checked) checked += 1; } catch { /* nessun input */ }
|
package/extension/manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "Chrome Bridge for Claude Code",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.25.0",
|
|
5
5
|
"description": "Bridge your browser to Claude Code: 60 token-efficient web-dev automation tools over a local WebSocket. ChromeOS included.",
|
|
6
6
|
"homepage_url": "https://github.com/frsorrentino/chrome-bridge",
|
|
7
7
|
"minimum_chrome_version": "135",
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gancio fetch/XHR in MAIN world, a document_start insieme alla cattura della
|
|
3
|
+
* console (vedi _applyInstrumentation nel service worker): le richieste fatte
|
|
4
|
+
* durante il caricamento della pagina entrano nel buffer letto da
|
|
5
|
+
* monitor_network. Prima il gancio arrivava solo alla prima chiamata e la
|
|
6
|
+
* richiesta fallita all'avvio, il caso tipico da debuggare, non si vedeva.
|
|
7
|
+
* Lo stesso file viene iniettato da ensureNetworkHook sulle pagine aperte
|
|
8
|
+
* prima dell'estensione; la guardia lo rende idempotente.
|
|
9
|
+
*/
|
|
10
|
+
(() => {
|
|
11
|
+
if (window.__chromeBridge_networkHooked) return;
|
|
12
|
+
window.__chromeBridge_networkHooked = true;
|
|
13
|
+
window.__chromeBridge_networkRequests = [];
|
|
14
|
+
window.__chromeBridge_netSeq = 0;
|
|
15
|
+
window.__chromeBridge_inflight = 0;
|
|
16
|
+
window.__chromeBridge_lastNetActivity = Date.now();
|
|
17
|
+
const MAX = 1000;
|
|
18
|
+
|
|
19
|
+
// --- Patch fetch ---
|
|
20
|
+
const origFetch = window.fetch.bind(window);
|
|
21
|
+
window.fetch = async (...args) => {
|
|
22
|
+
const req = args[0];
|
|
23
|
+
const url = typeof req === 'string' ? req : req?.url || String(req);
|
|
24
|
+
const method = (args[1]?.method || (req?.method) || 'GET').toUpperCase();
|
|
25
|
+
const entry = { type: 'fetch', method, url, startTime: Date.now(), status: null, duration: null, error: null };
|
|
26
|
+
window.__chromeBridge_inflight += 1;
|
|
27
|
+
window.__chromeBridge_lastNetActivity = Date.now();
|
|
28
|
+
try {
|
|
29
|
+
const resp = await origFetch(...args);
|
|
30
|
+
entry.status = resp.status;
|
|
31
|
+
entry.duration = Date.now() - entry.startTime;
|
|
32
|
+
// Ring buffer: scarta le più VECCHIE, non le nuove. Scartare le nuove
|
|
33
|
+
// faceva consegnare al modello le richieste dei primi secondi di vita
|
|
34
|
+
// della pagina etichettate come "most recent".
|
|
35
|
+
entry.seq = ++window.__chromeBridge_netSeq; window.__chromeBridge_networkRequests.push(entry);
|
|
36
|
+
if (window.__chromeBridge_networkRequests.length > MAX) {
|
|
37
|
+
window.__chromeBridge_networkRequests.shift();
|
|
38
|
+
}
|
|
39
|
+
window.__chromeBridge_inflight -= 1;
|
|
40
|
+
window.__chromeBridge_lastNetActivity = Date.now();
|
|
41
|
+
return resp;
|
|
42
|
+
} catch (err) {
|
|
43
|
+
entry.error = err.message;
|
|
44
|
+
entry.duration = Date.now() - entry.startTime;
|
|
45
|
+
// Ring buffer: scarta le più VECCHIE, non le nuove. Scartare le nuove
|
|
46
|
+
// faceva consegnare al modello le richieste dei primi secondi di vita
|
|
47
|
+
// della pagina etichettate come "most recent".
|
|
48
|
+
entry.seq = ++window.__chromeBridge_netSeq; window.__chromeBridge_networkRequests.push(entry);
|
|
49
|
+
if (window.__chromeBridge_networkRequests.length > MAX) {
|
|
50
|
+
window.__chromeBridge_networkRequests.shift();
|
|
51
|
+
}
|
|
52
|
+
window.__chromeBridge_inflight -= 1;
|
|
53
|
+
window.__chromeBridge_lastNetActivity = Date.now();
|
|
54
|
+
throw err;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// --- Patch XMLHttpRequest ---
|
|
59
|
+
const OrigXHR = window.XMLHttpRequest;
|
|
60
|
+
const origOpen = OrigXHR.prototype.open;
|
|
61
|
+
const origSend = OrigXHR.prototype.send;
|
|
62
|
+
OrigXHR.prototype.open = function (method, url, ...rest) {
|
|
63
|
+
this.__cb_method = method;
|
|
64
|
+
this.__cb_url = url;
|
|
65
|
+
return origOpen.call(this, method, url, ...rest);
|
|
66
|
+
};
|
|
67
|
+
OrigXHR.prototype.send = function (...args) {
|
|
68
|
+
const entry = { type: 'xhr', method: (this.__cb_method || 'GET').toUpperCase(), url: this.__cb_url || '', startTime: Date.now(), status: null, duration: null, error: null };
|
|
69
|
+
this.addEventListener('load', () => {
|
|
70
|
+
entry.status = this.status;
|
|
71
|
+
entry.duration = Date.now() - entry.startTime;
|
|
72
|
+
// Ring buffer: scarta le più VECCHIE, non le nuove. Scartare le nuove
|
|
73
|
+
// faceva consegnare al modello le richieste dei primi secondi di vita
|
|
74
|
+
// della pagina etichettate come "most recent".
|
|
75
|
+
entry.seq = ++window.__chromeBridge_netSeq; window.__chromeBridge_networkRequests.push(entry);
|
|
76
|
+
if (window.__chromeBridge_networkRequests.length > MAX) {
|
|
77
|
+
window.__chromeBridge_networkRequests.shift();
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
this.addEventListener('error', () => {
|
|
81
|
+
entry.error = 'Network error';
|
|
82
|
+
entry.duration = Date.now() - entry.startTime;
|
|
83
|
+
// Ring buffer: scarta le più VECCHIE, non le nuove. Scartare le nuove
|
|
84
|
+
// faceva consegnare al modello le richieste dei primi secondi di vita
|
|
85
|
+
// della pagina etichettate come "most recent".
|
|
86
|
+
entry.seq = ++window.__chromeBridge_netSeq; window.__chromeBridge_networkRequests.push(entry);
|
|
87
|
+
if (window.__chromeBridge_networkRequests.length > MAX) {
|
|
88
|
+
window.__chromeBridge_networkRequests.shift();
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
// loadend copre load, error e abort: traccia sempre la fine dell'in-flight
|
|
92
|
+
this.addEventListener('loadend', () => {
|
|
93
|
+
window.__chromeBridge_inflight -= 1;
|
|
94
|
+
window.__chromeBridge_lastNetActivity = Date.now();
|
|
95
|
+
});
|
|
96
|
+
window.__chromeBridge_inflight += 1;
|
|
97
|
+
window.__chromeBridge_lastNetActivity = Date.now();
|
|
98
|
+
return origSend.apply(this, args);
|
|
99
|
+
};
|
|
100
|
+
})();
|