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 +21 -0
- package/README.md +185 -0
- package/extension/console-capture.js +46 -0
- package/extension/icons/icon-128.png +0 -0
- package/extension/icons/icon-16.png +0 -0
- package/extension/icons/icon-48.png +0 -0
- package/extension/manifest.json +44 -0
- package/extension/page-instrumentation.js +115 -0
- package/extension/popup.css +128 -0
- package/extension/popup.html +64 -0
- package/extension/popup.js +142 -0
- package/extension/service-worker.js +3711 -0
- package/extension/stack-detect.js +54 -0
- package/extension/telemetry.js +29 -0
- package/install.sh +70 -0
- package/package.json +57 -0
- package/server/assertions.js +81 -0
- package/server/cli.js +380 -0
- package/server/formatters.js +35 -0
- package/server/har.js +22 -0
- package/server/index.js +107 -0
- package/server/launcher.js +97 -0
- package/server/link-checker.js +57 -0
- package/server/protocol.js +135 -0
- package/server/security-headers.js +68 -0
- package/server/stub-server.js +81 -0
- package/server/tools.js +1728 -0
- package/server/ws-manager.js +655 -0
- package/tools/measure-schema.mjs +108 -0
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
|
+
    [](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 & 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 & 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>
|