chrome-bridge-mcp 1.8.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Francesco Sorrentino (frsorrentino)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,185 @@
1
+ # Chrome Bridge
2
+
3
+ ![License: MIT](https://img.shields.io/badge/license-MIT-green) ![Node 18+](https://img.shields.io/badge/node-%E2%89%A518-brightgreen) ![Chrome 135+](https://img.shields.io/badge/chrome-%E2%89%A5135-blue) ![Tests](https://img.shields.io/badge/tests-109%20unit%20%2B%20e2e-brightgreen) [![Chrome Web Store](https://img.shields.io/badge/web%20store-published-blue)](https://chromewebstore.google.com/detail/chrome-bridge-for-claude/bioknpaeahidbelaljjohjofiloeodmb)
4
+
5
+ **Chrome Bridge is an MCP server that connects Claude Code to your real, logged-in Chrome browser — measured 2.75× fewer turns and 2.28× lower cost than the official "Claude in Chrome" extension on a form-filling task, with ~3× the toolset and no paid plan.**
6
+
7
+ By using a local WebSocket bridge and a specialized Chrome extension, Chrome Bridge provides 59 web-development tools (navigation, DOM inspection, visual regression, audits, network mocking) and a dedicated headless instance for CI. It is self-hosted, local-only, and requires no paid plan.
8
+
9
+ ## Why Chrome Bridge?
10
+
11
+ Chrome Bridge is designed to be both more efficient and more powerful than Anthropic's official browser extension.
12
+
13
+ ### 1. Efficiency Benchmark
14
+ Same model (Claude Sonnet 5), same task, paired runs on the same date and versions, **all runs included** (n=2 per arm — a small sample: direction, not precision):
15
+
16
+ | Task | Chrome Bridge | Claude in Chrome | Ratio |
17
+ | :--- | :--- | :--- | :--- |
18
+ | **Form fill** | 6.0 turns (6-6) / $0.211 | 16.5 turns (16-17) / $0.481 | **2.75× turns, 2.28× cost** |
19
+ | **1500-row table lookup** | see note | not re-run on this version | *not published* |
20
+
21
+ On the table-lookup task, an `extract_table` fix took our side from 13.5 to 4.0
22
+ turns, but the Claude-in-Chrome arm has not been re-run on that version — so no
23
+ ratio is published for it. The inclusion rule, every raw run (including the
24
+ unfavourable ones) and the harness limits are in
25
+ [bench/RESULTS.md](bench/RESULTS.md).
26
+
27
+ **Why it wins — it makes fewer round trips, not smaller payloads:**
28
+ - **Compact References:** The agent acts on short element handles (e.g., `n1`, `n2`) returned by `navigate` or `get_interactives` instead of the slow screenshot → read-coordinates → click loop.
29
+ - **Batching:** `fill_form` fills N fields and submits in one call — measured, 3 calls instead of 9 for the same form, at the same byte count.
30
+ - **Server-Side Processing:** Filtering large tables happens server-side (`extract_table` with `where`): 236 bytes to find one row among 1500, against 50,070 bytes for `read_page`. The extension-to-model payload is the token bottleneck; Chrome Bridge moves the heavy lifting to localhost.
31
+ - **Honest caveat:** per *turn*, Chrome Bridge costs slightly more than Claude in Chrome (41,985 vs 36,613 cache-read tokens; $0.0373 vs $0.0337). The win is in the number of turns.
32
+
33
+ ### 2. Feature Comparison
34
+ Chrome Bridge ships 59 specialized tools (30 core by default) compared to ~20 in Claude in Chrome. It is also the only automation that drives the real host Chrome on ChromeOS/Crostini.
35
+
36
+ | | Chrome Bridge | Claude in Chrome | Chrome DevTools MCP | Playwright MCP |
37
+ |---|---|---|---|---|
38
+ | **ChromeOS / Crostini** | **Yes** (real host) | No | Container only | Container only |
39
+ | **Tools** | **59** (30 core) | ~20 | ~50 | 23 core (71 total) |
40
+ | **Requires Paid Plan** | **No** | Yes (Pro+) | No | No |
41
+ | **Network Mocking** | **Yes** (stub/headers) | No | No | Yes |
42
+ | **Visual Regression** | **Yes** (`screenshot_diff`) | No | No | No |
43
+ | **Audits (A11y/SEO/Sec)** | **Yes** (Full suite) | No | Partial | No |
44
+ | **Headless / CI** | **Yes** | No | Yes | Yes |
45
+ | **GIF / Video** | No | **Yes** | Partial | No |
46
+ | **Breakpoints / Heap** | No | No | **Yes** | No |
47
+
48
+ ## Quickstart
49
+
50
+ ```bash
51
+ git clone git@github.com:frsorrentino/chrome-bridge.git && cd chrome-bridge && ./install.sh
52
+ ```
53
+
54
+ 1. Load the extension: Open `chrome://extensions`, enable **Developer mode**, click **Load unpacked**, and select the `extension/` folder.
55
+ 2. Restart Claude Code.
56
+
57
+ > On **ChromeOS/Crostini**, install the published extension from the [Chrome Web Store](https://chromewebstore.google.com/detail/chrome-bridge-for-claude/bioknpaeahidbelaljjohjofiloeodmb) instead of loading it unpacked — a filesystem-loaded extension is dropped on every reboot (the container isn't mounted when Chrome starts).
58
+
59
+ ### Example Session
60
+ > *"Open localhost:3000, run an accessibility audit, and find the 'Sign Up' button"*
61
+
62
+ Claude Code calls `navigate` (returning clickable element refs), `accessibility_audit`, and `find_text`. Because `navigate` returns refs like `n1`, the agent can immediately call `click(ref="n1")` without a discovery turn.
63
+
64
+ ## Token-Efficient Design
65
+
66
+ - **Compact Schemas**: 30 core tools use ≈3.9k tokens of schemas (vs Playwright MCP's ~4.6k). Specialized groups (audits, visual, network, storage, dom, files) are opt-in via `--caps`.
67
+ - **Act-from-Result**: Tools attach a capped preview of interactive elements with short refs. Actions only report a `page_changed` delta when the URL or title actually changes.
68
+ - **Optimized Media**: Screenshots are downscaled to ≤1568px. Full-page captures are sliced into readable segments.
69
+ - **Zero-Token Escape Hatches**: The [CLI](#cli) skips MCP schemas entirely, and recorded flows [replay](#launch-mode-headless--ci) without any model in the loop.
70
+
71
+ ## Highlights
72
+
73
+ - **Launch Mode**: Start a dedicated Chromium instance with an ephemeral profile for isolated sessions or CI.
74
+ - **Network Mocking**: Block/redirect requests, rewrite headers, or stub response bodies with synthetic data.
75
+ - **Visual Regression**: Use `screenshot_diff` to compare current pages against named baselines.
76
+ - **Structured Extraction**: One-call `extract` for repeated data and `extract_table` with server-side `where` filtering.
77
+
78
+ ## Architecture
79
+
80
+ ```
81
+ Claude Code <--stdio--> MCP Server <--WebSocket :8765--> Chrome Extension
82
+ (server/) (extension/, MV3)
83
+ ```
84
+
85
+ The **MCP Server** (Node.js) handles the protocol and tool logic. The **Chrome Extension** (MV3) executes commands via Chrome APIs. User scripts (`execute_js`) run via `chrome.userScripts.execute()`, requiring the "Allow user scripts" toggle in extension settings.
86
+
87
+ ## Tools (59 total)
88
+
89
+ ### Core & Navigation (7)
90
+ `get_status`, `get_tabs`, `create_tab`, `navigate`, `tab_action`, `get_frames`, `screenshot`.
91
+
92
+ ### Interaction (11)
93
+ `click`, `type_text`, `fill_form`, `hover`, `press_key`, `scroll`, `drag_and_drop`, `upload_file`, `dismiss_overlays`, `handle_dialogs`, `clipboard`.
94
+
95
+ ### DOM & Inspection (11)
96
+ `read_page`, `extract`, `get_page_info`, `query_dom`, `modify_dom`, `find_text`, `get_interactives`, `inject_css`, `highlight_elements`, `watch_dom`, `measure_spacing`.
97
+
98
+ ### Debugging & Network (8)
99
+ `execute_js`, `read_console`, `monitor_network`, `monitor_websocket`, `network_rules` (block/redirect/stub/headers), `get_performance`, `web_vitals`, `list_event_listeners`.
100
+
101
+ ### Visual & Responsive (7)
102
+ `element_screenshot`, `full_page_screenshot`, `screenshot_diff`, `viewport_resize`, `set_zoom`, `emulate_media`, `set_geolocation`.
103
+
104
+ ### Audits (6)
105
+ `accessibility_audit`, `seo_audit`, `security_headers`, `check_links` (server-side verification), `unused_css`, `extract_table` (with `where` filtering).
106
+
107
+ ### State, Storage & Files (9)
108
+ `get_storage`, `set_storage`, `session_fixture`, `http_auth`, `save_page` (MHTML), `manage_downloads`, `session_record`, `wait_for`, `assert`.
109
+
110
+ ## Install
111
+
112
+ **Requirements:** Node.js 18+, Chrome 135+.
113
+
114
+ ### From Source
115
+ ```bash
116
+ git clone git@github.com:frsorrentino/chrome-bridge.git
117
+ cd chrome-bridge
118
+ ./install.sh
119
+ ```
120
+ `install.sh` registers the MCP server in Claude Code (`--scope user`).
121
+
122
+ ### Manual Registration
123
+ ```bash
124
+ claude mcp add --scope user chrome-bridge node /path/to/server/index.js
125
+ ```
126
+
127
+ **Note:** For `execute_js`, you must enable **"Allow user scripts"** in `chrome://extensions` → Chrome Bridge → Details. On Chrome 135-137, enable Developer Mode instead.
128
+
129
+ ## Configuration
130
+
131
+ Configure the server via environment variables (or the matching CLI flags):
132
+ - `CHROME_BRIDGE_PORT`: Default `8765`.
133
+ - `CHROME_BRIDGE_HOST` / `--host`: Bind address, default `127.0.0.1`. Set to `0.0.0.0` **only** where the browser lives outside the container (ChromeOS/Crostini port-forward) — and pair it with a token.
134
+ - `CHROME_BRIDGE_TOKEN`: Shared secret required on both `ext_init` and `relay_init`. Strongly recommended whenever the bind is not loopback.
135
+ - `CHROME_BRIDGE_CAPS` / `--caps`: Tool groups to load (`core`, `audits`, `visual`, `network`, `storage`, `dom`, `files`, or `all`). `install.sh` registers with `all`; the bare server defaults to `core` (30 tools). `get_status` reports `caps_active` and `caps_available`.
136
+
137
+ Run `node tools/measure-schema.mjs` to see the exact schema cost of the active set.
138
+
139
+ ## CLI
140
+
141
+ The CLI allows batch operations and pipes output through `grep` or `jq` before it reaches the model:
142
+ ```bash
143
+ chrome-bridge navigate --url https://example.com
144
+ chrome-bridge read_console --level error | head -20
145
+ chrome-bridge assert --selector "#success" --text "Done"
146
+ chrome-bridge replay --file ./recordings/login.jsonl
147
+ ```
148
+
149
+ ## Launch Mode (Headless / CI)
150
+
151
+ Launch mode starts a dedicated Chromium instance with an ephemeral profile:
152
+ ```bash
153
+ node server/index.js --launch --headless
154
+ ```
155
+ Perfect for CI smoke tests using `session_record` + `replay`. Note: `execute_js` uses a `new Function` fallback in launch mode if the user-script toggle is unavailable.
156
+
157
+ ## Security
158
+
159
+ - **Loopback by default**: the WebSocket server binds `127.0.0.1`. Exposing it to the network is opt-in via `CHROME_BRIDGE_HOST`/`--host`.
160
+ - **Origin validation**: extension connections are only accepted from a `chrome-extension://` origin, and — with a loopback bind — only from loopback.
161
+ - **Token on both handshakes**: `CHROME_BRIDGE_TOKEN`, when set, is required for `ext_init` *and* `relay_init`. Without it, any local process could act as a relay and reach `execute_js` in your authenticated browser session.
162
+ - **Relay security**: secondary MCP instances must connect via loopback and are acknowledged with `relay_init_ok`, so a foreign process holding the port fails fast instead of timing out per command.
163
+ - **What is *not* protected**: page content reaches the model unfiltered, so a hostile page's text is untrusted input; `get_storage`, `session_fixture`, HAR exports and screenshots are **not** redacted and may carry cookies, tokens or personal data. Do not point the automation at pages holding secrets you would not paste into a chat.
164
+
165
+ ## Troubleshooting
166
+
167
+ | Symptom | Cause / fix |
168
+ |---|---|
169
+ | `Chrome extension not connected` | Extension disabled, or its port differs from the server's. The error names the actual host/port; check them in the popup (⚙). |
170
+ | Port 8765 already in use | Expected: a second MCP session becomes a **relay** and shares the one bridge. Set `CHROME_BRIDGE_PORT` for a separate one. |
171
+ | `Port N is held by a process that is not chrome-bridge` | Something else owns the port. Free it or change `CHROME_BRIDGE_PORT`. |
172
+ | `execute_js` fails | Enable **Allow user scripts** in `chrome://extensions` → Chrome Bridge → Details (Chrome 138+; on 135-137 enable Developer Mode). |
173
+ | `read_console` returns a `note=Instrumentation not loaded` | The page was opened before the extension, "Capture console & metrics" is off, or the page is not injectable (`chrome://`). Reload the page. |
174
+ | Screenshot times out | On ChromeOS a fully occluded window stops producing frames; captures fail after 10s. Bring the window forward. |
175
+ | Commands work then stop after a while | The MV3 service worker restarted and in-memory state (network log, diff baselines, HTTP auth) was reset. Re-run the monitoring call. |
176
+ | Extension is dropped on every ChromeOS reboot | Install from the Chrome Web Store instead of Load unpacked: the Crostini filesystem isn't mounted when Chrome starts. |
177
+ | Tool missing from the list | It's in an opt-in capability group. Check `get_status` → `caps_available`, then set `CHROME_BRIDGE_CAPS=all`. |
178
+
179
+ ## Tests
180
+ - **Unit**: `npm test` (Chrome-free, ~22s).
181
+ - **E2E**: `npm run test:e2e` (requires Chrome and a connected extension).
182
+ - **Schema cost**: `npm run measure`.
183
+
184
+ ## License
185
+ MIT
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Iniettato a document_start in MAIN world: cattura console.* fin dal
3
+ * primo istante di vita della pagina, più errori non gestiti.
4
+ *
5
+ * Registrato dinamicamente dal service worker solo quando l'instrumentation
6
+ * è attiva (toggle nel popup): pagine non in debug hanno zero footprint.
7
+ * La cattura non deve mai lanciare nella pagina: ogni hook è in try/catch e
8
+ * delega sempre all'originale.
9
+ */
10
+ (() => {
11
+ if (window.__chromeBridge_consoleHooked) return;
12
+ window.__chromeBridge_consoleHooked = true;
13
+ window.__chromeBridge_consoleLogs = [];
14
+ const MAX = 1000;
15
+ const push = (entry) => {
16
+ const buf = window.__chromeBridge_consoleLogs;
17
+ if (buf.length >= MAX) buf.shift(); // ring buffer: tieni i più recenti, non i primi 1000
18
+ buf.push(entry);
19
+ };
20
+ for (const method of ['log', 'warn', 'error', 'info', 'debug']) {
21
+ const orig = console[method].bind(console);
22
+ console[method] = (...args) => {
23
+ try {
24
+ push({
25
+ level: method,
26
+ args: args.map((a) => {
27
+ try { return typeof a === 'object' ? JSON.stringify(a) : String(a); }
28
+ catch { return String(a); }
29
+ }),
30
+ timestamp: Date.now(),
31
+ });
32
+ } catch {}
33
+ return orig(...args);
34
+ };
35
+ }
36
+ window.addEventListener('error', (e) => {
37
+ try { push({ level: 'error', args: [`Uncaught ${e.message} at ${e.filename || '?'}:${e.lineno || 0}`], timestamp: Date.now() }); } catch {}
38
+ }, true);
39
+ window.addEventListener('unhandledrejection', (e) => {
40
+ try {
41
+ let reason;
42
+ try { reason = String(e.reason); } catch { reason = '<unstringifiable>'; }
43
+ push({ level: 'error', args: [`Unhandled rejection: ${reason}`], timestamp: Date.now() });
44
+ } catch {}
45
+ });
46
+ })();
Binary file
Binary file
Binary file
@@ -0,0 +1,44 @@
1
+ {
2
+ "manifest_version": 3,
3
+ "name": "Chrome Bridge for Claude Code",
4
+ "version": "1.8.0",
5
+ "description": "Bridge your browser to Claude Code: 59 token-efficient web-dev automation tools over a local WebSocket. ChromeOS included.",
6
+ "homepage_url": "https://github.com/frsorrentino/chrome-bridge",
7
+ "minimum_chrome_version": "135",
8
+ "permissions": [
9
+ "tabs",
10
+ "scripting",
11
+ "userScripts",
12
+ "alarms",
13
+ "storage",
14
+ "cookies",
15
+ "webNavigation",
16
+ "webRequest",
17
+ "declarativeNetRequest",
18
+ "clipboardRead",
19
+ "clipboardWrite",
20
+ "downloads",
21
+ "pageCapture",
22
+ "webRequestAuthProvider"
23
+ ],
24
+ "host_permissions": [
25
+ "<all_urls>"
26
+ ],
27
+ "background": {
28
+ "service_worker": "service-worker.js",
29
+ "type": "module"
30
+ },
31
+ "action": {
32
+ "default_popup": "popup.html",
33
+ "default_icon": {
34
+ "16": "icons/icon-16.png",
35
+ "48": "icons/icon-48.png",
36
+ "128": "icons/icon-128.png"
37
+ }
38
+ },
39
+ "icons": {
40
+ "16": "icons/icon-16.png",
41
+ "48": "icons/icon-48.png",
42
+ "128": "icons/icon-128.png"
43
+ }
44
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Iniettato a document_start in MAIN world: raccoglie Web Vitals
3
+ * (CLS, LCP, long task, INP approssimato) e censisce gli event listener
4
+ * registrati via addEventListener.
5
+ */
6
+ (() => {
7
+ if (window.__chromeBridge_instrumented) return;
8
+ window.__chromeBridge_instrumented = true;
9
+
10
+ // --- Web Vitals ---
11
+ const vitals = {
12
+ cls: 0,
13
+ lcp: null,
14
+ longTasks: { count: 0, totalMs: 0, maxMs: 0 },
15
+ maxEventDelayMs: null, // INP approssimato (max event duration)
16
+ };
17
+ window.__chromeBridge_vitals = vitals;
18
+
19
+ try {
20
+ new PerformanceObserver((list) => {
21
+ for (const entry of list.getEntries()) {
22
+ if (!entry.hadRecentInput) vitals.cls += entry.value;
23
+ }
24
+ }).observe({ type: 'layout-shift', buffered: true });
25
+ } catch {}
26
+
27
+ try {
28
+ new PerformanceObserver((list) => {
29
+ const entries = list.getEntries();
30
+ if (entries.length) vitals.lcp = Math.round(entries[entries.length - 1].startTime);
31
+ }).observe({ type: 'largest-contentful-paint', buffered: true });
32
+ } catch {}
33
+
34
+ try {
35
+ new PerformanceObserver((list) => {
36
+ for (const entry of list.getEntries()) {
37
+ vitals.longTasks.count++;
38
+ vitals.longTasks.totalMs += Math.round(entry.duration);
39
+ vitals.longTasks.maxMs = Math.max(vitals.longTasks.maxMs, Math.round(entry.duration));
40
+ }
41
+ }).observe({ type: 'longtask', buffered: true });
42
+ } catch {}
43
+
44
+ try {
45
+ new PerformanceObserver((list) => {
46
+ for (const entry of list.getEntries()) {
47
+ if (vitals.maxEventDelayMs === null || entry.duration > vitals.maxEventDelayMs) {
48
+ vitals.maxEventDelayMs = Math.round(entry.duration);
49
+ }
50
+ }
51
+ }).observe({ type: 'event', buffered: true, durationThreshold: 40 });
52
+ } catch {}
53
+
54
+ // --- Censimento event listener ---
55
+ const listeners = [];
56
+ window.__chromeBridge_listeners = listeners;
57
+ const MAX_LISTENERS = 2000;
58
+
59
+ const describeTarget = (t) => {
60
+ if (t === window) return 'window';
61
+ if (t === document) return 'document';
62
+ if (t && t.nodeType === 1) {
63
+ const tag = t.tagName.toLowerCase();
64
+ const id = t.id ? `#${t.id}` : '';
65
+ const cls = t.classList && t.classList.length ? `.${[...t.classList].slice(0, 2).join('.')}` : '';
66
+ return `${tag}${id}${cls}`;
67
+ }
68
+ return String(t && t.constructor ? t.constructor.name : t);
69
+ };
70
+
71
+ const origAdd = EventTarget.prototype.addEventListener;
72
+ EventTarget.prototype.addEventListener = function (type, fn, opts) {
73
+ // La cattura non deve mai impedire la registrazione reale del listener.
74
+ try {
75
+ if (listeners.length >= MAX_LISTENERS) listeners.shift(); // ring buffer
76
+ listeners.push({
77
+ type,
78
+ target: describeTarget(this),
79
+ capture: typeof opts === 'boolean' ? opts : !!(opts && opts.capture),
80
+ once: !!(opts && opts.once),
81
+ passive: !!(opts && opts.passive),
82
+ timestamp: Date.now(),
83
+ });
84
+ } catch {}
85
+ return origAdd.call(this, type, fn, opts);
86
+ };
87
+
88
+ // --- SPA route tracking ---
89
+ const routes = [];
90
+ window.__chromeBridge_routes = routes;
91
+ const MAX_ROUTES = 200;
92
+ const recordRoute = (type) => {
93
+ const url = location.href;
94
+ const last = routes[routes.length - 1];
95
+ if (last && last.url === url) return; // dedup consecutivi
96
+ if (routes.length >= MAX_ROUTES) routes.shift();
97
+ routes.push({ url, type, timestamp: Date.now() });
98
+ window.__chromeBridge_lastRoute = url;
99
+ };
100
+ recordRoute('initial');
101
+ const origPush = history.pushState;
102
+ history.pushState = function (...args) {
103
+ const r = origPush.apply(this, args);
104
+ recordRoute('pushState');
105
+ return r;
106
+ };
107
+ const origReplace = history.replaceState;
108
+ history.replaceState = function (...args) {
109
+ const r = origReplace.apply(this, args);
110
+ recordRoute('replaceState');
111
+ return r;
112
+ };
113
+ window.addEventListener('popstate', () => recordRoute('popstate'));
114
+ window.addEventListener('hashchange', () => recordRoute('hashchange'));
115
+ })();
@@ -0,0 +1,128 @@
1
+ * { margin: 0; padding: 0; box-sizing: border-box; }
2
+
3
+ [hidden] { display: none !important; }
4
+
5
+ :root {
6
+ --bg: #ffffff;
7
+ --fg: #1f1f1f;
8
+ --muted: #5f6368;
9
+ --surface: #f8f9fa;
10
+ --border: #e0e0e0;
11
+ --accent: #1a73e8;
12
+ --accent-bg: #f1f3f4;
13
+ --ok: #188038;
14
+ --ok-bg: #e6f4ea;
15
+ --err: #d93025;
16
+ --warn-bg: #fef7e0;
17
+ --warn-fg: #994c00;
18
+ --warn-btn: #1a73e8;
19
+ }
20
+
21
+ @media (prefers-color-scheme: dark) {
22
+ :root {
23
+ --bg: #0d1117;
24
+ --fg: #e6edf3;
25
+ --muted: #8b949e;
26
+ --surface: #161b22;
27
+ --border: #21262d;
28
+ --accent: #58a6ff;
29
+ --accent-bg: #21262d;
30
+ --ok: #3fb950;
31
+ --ok-bg: #12261e;
32
+ --err: #f85149;
33
+ --warn-bg: #341a00;
34
+ --warn-fg: #e3b341;
35
+ --warn-btn: #9e6a03;
36
+ }
37
+ }
38
+
39
+ body {
40
+ width: 320px;
41
+ font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
42
+ font-size: 13px;
43
+ background: var(--bg);
44
+ color: var(--fg);
45
+ }
46
+
47
+ .hdr {
48
+ display: flex; align-items: center; gap: 8px;
49
+ padding: 12px 14px;
50
+ border-bottom: 1px solid var(--border);
51
+ }
52
+ .hdr .logo { width: 22px; height: 22px; border-radius: 6px; }
53
+ .hdr h1 { font-size: 14px; font-weight: 700; }
54
+ .hdr .ver { font-size: 10px; color: var(--muted); margin-left: auto; }
55
+
56
+ .status-row { display: flex; align-items: center; gap: 6px; padding: 10px 14px; }
57
+ .dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
58
+ .dot.connected { background: var(--ok); box-shadow: 0 0 6px var(--ok); }
59
+ .dot.connecting { background: #f39c12; }
60
+ .dot.disconnected { background: var(--err); }
61
+ #status-text { font-weight: 600; }
62
+ .status-row .ws-url {
63
+ margin-left: auto;
64
+ font-family: ui-monospace, monospace;
65
+ font-size: 11px; color: var(--muted);
66
+ }
67
+
68
+ .warn {
69
+ margin: 0 14px 10px; padding: 10px 12px;
70
+ border-radius: 8px; font-size: 12px; line-height: 1.45;
71
+ background: var(--warn-bg); color: var(--warn-fg);
72
+ }
73
+ .warn b { display: block; margin-bottom: 2px; }
74
+ .fixbtn {
75
+ margin-top: 8px; padding: 5px 12px;
76
+ border: none; border-radius: 6px; cursor: pointer;
77
+ font-size: 12px; font-weight: 600;
78
+ background: var(--warn-btn); color: #fff;
79
+ }
80
+
81
+ .card {
82
+ margin: 0 14px 10px; padding: 8px 10px;
83
+ border-radius: 8px; background: var(--surface);
84
+ }
85
+ .card-title {
86
+ font-size: 10px; text-transform: uppercase; letter-spacing: .05em;
87
+ color: var(--muted); margin-bottom: 4px;
88
+ }
89
+ .kv { display: flex; justify-content: space-between; font-size: 12px; padding: 2px 0; }
90
+ .kv span { color: var(--muted); }
91
+ .kv b.bad { color: var(--err); }
92
+ .hint { font-size: 11px; color: var(--muted); padding-top: 4px; }
93
+ #page-unavailable { font-size: 12px; color: var(--muted); }
94
+
95
+ .stats { display: flex; gap: 8px; padding: 0 14px 10px; }
96
+ .stat { flex: 1; border-radius: 8px; padding: 8px 10px; background: var(--surface); }
97
+ .stat .n { font-size: 16px; font-weight: 700; font-family: ui-monospace, monospace; }
98
+ .stat .n.small { font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
99
+ .stat .n.bad { color: var(--err); }
100
+ .stat .l { font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: .04em; }
101
+
102
+ #err-items { list-style: none; font-size: 11px; }
103
+ #err-items li { padding: 2px 0; color: var(--muted); }
104
+ #err-items li b { color: var(--err); font-family: ui-monospace, monospace; }
105
+
106
+ .acts { display: flex; gap: 8px; padding: 0 14px 14px; }
107
+ .abtn {
108
+ flex: 1; padding: 7px 0; text-align: center;
109
+ border: none; border-radius: 8px; cursor: pointer;
110
+ font-size: 12px; font-weight: 600;
111
+ background: var(--accent-bg); color: var(--accent);
112
+ }
113
+ .abtn.icon { flex: 0 0 36px; }
114
+ .abtn.primary { background: var(--accent); color: #fff; }
115
+ .abtn:hover { filter: brightness(0.95); }
116
+
117
+ .config {
118
+ display: flex; flex-direction: column; gap: 6px;
119
+ margin: 0 14px 14px; padding: 10px;
120
+ border-radius: 8px; background: var(--surface); font-size: 12px;
121
+ }
122
+ .config input {
123
+ width: 100%; padding: 4px 6px;
124
+ border: 1px solid var(--border); border-radius: 4px;
125
+ background: var(--bg); color: var(--fg);
126
+ }
127
+ .config label.check { display: flex; align-items: center; gap: 6px; }
128
+ .config label.check input { width: auto; }
@@ -0,0 +1,64 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <link rel="stylesheet" href="popup.css">
6
+ </head>
7
+ <body>
8
+ <header class="hdr">
9
+ <img src="icons/icon-48.png" alt="" class="logo">
10
+ <h1>Chrome Bridge</h1>
11
+ <span class="ver" id="versions">v—</span>
12
+ </header>
13
+
14
+ <div class="status-row">
15
+ <span id="indicator" class="dot disconnected"></span>
16
+ <b id="status-text">Disconnesso</b>
17
+ <span class="ws-url" id="ws-url">ws://localhost:8765</span>
18
+ </div>
19
+
20
+ <div id="us-warning" class="warn" hidden>
21
+ <b>⚠ Script utente disattivati</b>
22
+ <span>execute_js e wait_for(function) non sono disponibili senza il permesso dedicato.</span>
23
+ <button id="us-fix" class="fixbtn">Apri impostazioni →</button>
24
+ </div>
25
+
26
+ <section class="card" id="page-card">
27
+ <div class="card-title">Pagina corrente</div>
28
+ <div id="page-body">
29
+ <div class="kv"><span>Errori console</span><b id="pg-errors">—</b></div>
30
+ <div class="kv"><span>LCP / CLS</span><b id="pg-vitals">—</b></div>
31
+ <div class="kv"><span>Stack</span><b id="pg-stack">—</b></div>
32
+ <div class="hint" id="pg-hint" hidden>Attiva "Capture console &amp; metrics" (⚙) per errori e metriche.</div>
33
+ </div>
34
+ <div id="page-unavailable" hidden>Non disponibile su questa pagina.</div>
35
+ </section>
36
+
37
+ <div class="stats">
38
+ <div class="stat"><div class="n" id="st-calls">0</div><div class="l">tool call</div></div>
39
+ <div class="stat"><div class="n small" id="st-last">—</div><div class="l" id="st-last-ago">ultimo</div></div>
40
+ <div class="stat"><div class="n" id="st-errors">0</div><div class="l">errori</div></div>
41
+ </div>
42
+
43
+ <div id="err-list" class="card" hidden>
44
+ <div class="card-title">Errori recenti</div>
45
+ <ul id="err-items"></ul>
46
+ </div>
47
+
48
+ <div class="acts">
49
+ <button id="reconnect" class="abtn">↻ Riconnetti</button>
50
+ <button id="diagnostics" class="abtn">⧉ Diagnostica</button>
51
+ <button id="toggle-config" class="abtn icon" title="Impostazioni">⚙</button>
52
+ </div>
53
+
54
+ <div class="config" id="config" hidden>
55
+ <label>Porta <input type="number" id="port" min="1" max="65535"></label>
56
+ <label>Token <input type="password" id="token" placeholder="(nessuno)"></label>
57
+ <label class="check"><input type="checkbox" id="instrument"> Capture console &amp; metrics</label>
58
+ <button id="save" class="abtn primary">Salva e riconnetti</button>
59
+ </div>
60
+
61
+ <script src="telemetry.js"></script>
62
+ <script src="popup.js"></script>
63
+ </body>
64
+ </html>