browser-bridge-mcp 0.1.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 +144 -0
- package/bin/cli.mjs +153 -0
- package/middleware.mjs +22 -0
- package/next.mjs +40 -0
- package/package.json +52 -0
- package/src/client.js +725 -0
- package/src/server.mjs +691 -0
- package/vite.mjs +18 -0
package/src/client.js
ADDED
|
@@ -0,0 +1,725 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser Bridge Client
|
|
3
|
+
*
|
|
4
|
+
* Paste this entire script into your browser console (or load as bookmarklet)
|
|
5
|
+
* to connect the current tab to Claude Code's Browser Bridge MCP server.
|
|
6
|
+
*
|
|
7
|
+
* What it captures (automatically pushed to Claude):
|
|
8
|
+
* - All console output (log, warn, error, info, debug)
|
|
9
|
+
* - All fetch and XHR network requests with status, timing, headers, body
|
|
10
|
+
* - JavaScript errors and unhandled promise rejections
|
|
11
|
+
* - Page navigation changes
|
|
12
|
+
*
|
|
13
|
+
* What Claude can pull on-demand:
|
|
14
|
+
* - localStorage / sessionStorage
|
|
15
|
+
* - Cookies
|
|
16
|
+
* - Page info (URL, title, viewport, etc.)
|
|
17
|
+
* - Execute arbitrary JS
|
|
18
|
+
* - Redux state
|
|
19
|
+
* - React Query cache
|
|
20
|
+
* - Performance metrics
|
|
21
|
+
* - DOM snapshots
|
|
22
|
+
*
|
|
23
|
+
* Configuration:
|
|
24
|
+
* window.__BRIDGE_PORT = 8089; // WebSocket port (default 8089)
|
|
25
|
+
* window.__BRIDGE_LABEL = "my-tab"; // Label for this tab (default: auto from URL)
|
|
26
|
+
* window.__BRIDGE_CAPTURE_BODIES = true; // Capture request/response bodies (default true)
|
|
27
|
+
* window.__BRIDGE_MAX_BODY_SIZE = 10000; // Max body size in chars (default 10000)
|
|
28
|
+
*/
|
|
29
|
+
(function () {
|
|
30
|
+
"use strict";
|
|
31
|
+
|
|
32
|
+
// Prevent double-injection
|
|
33
|
+
if (window.__browserBridge) {
|
|
34
|
+
console.log("[Bridge] Already connected. Use window.__browserBridge.disconnect() to reconnect.");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const PORT = window.__BRIDGE_PORT || 8089;
|
|
39
|
+
const WS_URL = `ws://127.0.0.1:${PORT}`;
|
|
40
|
+
const CAPTURE_BODIES = window.__BRIDGE_CAPTURE_BODIES !== false;
|
|
41
|
+
const MAX_BODY_SIZE = window.__BRIDGE_MAX_BODY_SIZE || 10000;
|
|
42
|
+
|
|
43
|
+
let ws = null;
|
|
44
|
+
let reconnectTimer = null;
|
|
45
|
+
let connected = false;
|
|
46
|
+
let focused = false;
|
|
47
|
+
let tabId = null;
|
|
48
|
+
let badge = null;
|
|
49
|
+
|
|
50
|
+
// Auto-generate label from URL if not set
|
|
51
|
+
function getLabel() {
|
|
52
|
+
if (window.__BRIDGE_LABEL) return window.__BRIDGE_LABEL;
|
|
53
|
+
try {
|
|
54
|
+
const u = new URL(location.href);
|
|
55
|
+
const path = u.pathname === "/" ? "" : u.pathname.slice(0, 30);
|
|
56
|
+
return `${u.hostname}${path}`;
|
|
57
|
+
} catch {
|
|
58
|
+
return location.href.slice(0, 40);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Connection indicator badge
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
function createBadge() {
|
|
67
|
+
badge = document.createElement("div");
|
|
68
|
+
badge.id = "__bridge-badge";
|
|
69
|
+
badge.style.cssText = `
|
|
70
|
+
position: fixed; bottom: 8px; right: 8px; z-index: 999999;
|
|
71
|
+
padding: 4px 10px; border-radius: 12px; font-size: 11px;
|
|
72
|
+
font-family: system-ui, sans-serif; font-weight: 600;
|
|
73
|
+
color: white; cursor: pointer; transition: all 0.3s;
|
|
74
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.2); user-select: none;
|
|
75
|
+
`;
|
|
76
|
+
badge.addEventListener("click", () => {
|
|
77
|
+
badge.style.display = badge.style.display === "none" ? "block" : "none";
|
|
78
|
+
});
|
|
79
|
+
document.body.appendChild(badge);
|
|
80
|
+
updateBadge();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function updateBadge() {
|
|
84
|
+
if (!badge) return;
|
|
85
|
+
if (!connected) {
|
|
86
|
+
badge.style.background = "linear-gradient(135deg, #ef4444, #dc2626)";
|
|
87
|
+
badge.textContent = "⏳ Bridge Reconnecting...";
|
|
88
|
+
} else if (focused) {
|
|
89
|
+
badge.style.background = "linear-gradient(135deg, #3b82f6, #2563eb)";
|
|
90
|
+
badge.textContent = "🎯 Claude Focused";
|
|
91
|
+
badge.title = `Tab ID: ${tabId}\nThis tab is Claude's active target`;
|
|
92
|
+
} else {
|
|
93
|
+
badge.style.background = "linear-gradient(135deg, #6b7280, #4b5563)";
|
|
94
|
+
badge.textContent = "🔗 Bridge Connected";
|
|
95
|
+
badge.title = `Tab ID: ${tabId}\nConnected but not focused. Claude is watching another tab.`;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// WebSocket connection
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
function connect() {
|
|
104
|
+
if (ws && (ws.readyState === WebSocket.CONNECTING || ws.readyState === WebSocket.OPEN)) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
ws = new WebSocket(WS_URL);
|
|
110
|
+
} catch {
|
|
111
|
+
scheduleReconnect();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
ws.onopen = () => {
|
|
116
|
+
connected = true;
|
|
117
|
+
updateBadge();
|
|
118
|
+
console.log("[Bridge] Connected to Claude Code");
|
|
119
|
+
|
|
120
|
+
// Send initial page info
|
|
121
|
+
sendPageInfo();
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
ws.onclose = () => {
|
|
125
|
+
connected = false;
|
|
126
|
+
updateBadge();
|
|
127
|
+
scheduleReconnect();
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
ws.onerror = () => {
|
|
131
|
+
// onclose will fire after this
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
ws.onmessage = (event) => {
|
|
135
|
+
try {
|
|
136
|
+
const msg = JSON.parse(event.data);
|
|
137
|
+
if (msg.type === "request") {
|
|
138
|
+
handleServerRequest(msg);
|
|
139
|
+
} else if (msg.type === "tab_id") {
|
|
140
|
+
tabId = msg.tabId;
|
|
141
|
+
updateBadge();
|
|
142
|
+
} else if (msg.type === "focus_state") {
|
|
143
|
+
focused = msg.focused;
|
|
144
|
+
updateBadge();
|
|
145
|
+
if (focused) {
|
|
146
|
+
originalConsole.log(`[Bridge] This tab is now Claude's focused target (${tabId})`);
|
|
147
|
+
} else {
|
|
148
|
+
originalConsole.log("[Bridge] This tab is no longer focused");
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
} catch {
|
|
152
|
+
// ignore
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function scheduleReconnect() {
|
|
158
|
+
if (reconnectTimer) return;
|
|
159
|
+
reconnectTimer = setTimeout(() => {
|
|
160
|
+
reconnectTimer = null;
|
|
161
|
+
connect();
|
|
162
|
+
}, 2000);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function send(data) {
|
|
166
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
167
|
+
ws.send(JSON.stringify(data));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function sendPageInfo() {
|
|
172
|
+
send({
|
|
173
|
+
type: "page_info",
|
|
174
|
+
url: location.href,
|
|
175
|
+
title: document.title,
|
|
176
|
+
label: getLabel(),
|
|
177
|
+
userAgent: navigator.userAgent,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// Console hooking
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
const originalConsole = {
|
|
186
|
+
log: console.log.bind(console),
|
|
187
|
+
warn: console.warn.bind(console),
|
|
188
|
+
error: console.error.bind(console),
|
|
189
|
+
info: console.info.bind(console),
|
|
190
|
+
debug: console.debug.bind(console),
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
function hookConsole() {
|
|
194
|
+
["log", "warn", "error", "info", "debug"].forEach((level) => {
|
|
195
|
+
console[level] = function (...args) {
|
|
196
|
+
// Call original
|
|
197
|
+
originalConsole[level](...args);
|
|
198
|
+
|
|
199
|
+
// Skip our own bridge messages
|
|
200
|
+
if (args[0] && typeof args[0] === "string" && args[0].startsWith("[Bridge]")) return;
|
|
201
|
+
|
|
202
|
+
send({
|
|
203
|
+
type: "console",
|
|
204
|
+
level,
|
|
205
|
+
args: args.map(serializeArg),
|
|
206
|
+
timestamp: Date.now(),
|
|
207
|
+
stack: level === "error" ? new Error().stack : undefined,
|
|
208
|
+
});
|
|
209
|
+
};
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function serializeArg(arg) {
|
|
214
|
+
if (arg === undefined) return "undefined";
|
|
215
|
+
if (arg === null) return null;
|
|
216
|
+
if (typeof arg === "string") return arg;
|
|
217
|
+
if (typeof arg === "number" || typeof arg === "boolean") return arg;
|
|
218
|
+
if (arg instanceof Error) return { __type: "Error", message: arg.message, stack: arg.stack };
|
|
219
|
+
if (arg instanceof HTMLElement) return { __type: "HTMLElement", tag: arg.tagName, id: arg.id, className: arg.className };
|
|
220
|
+
try {
|
|
221
|
+
const str = JSON.stringify(arg);
|
|
222
|
+
return str.length > MAX_BODY_SIZE ? JSON.parse(str.slice(0, MAX_BODY_SIZE) + '..."') : JSON.parse(str);
|
|
223
|
+
} catch {
|
|
224
|
+
return String(arg);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
// Fetch hooking
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
|
|
232
|
+
const originalFetch = window.fetch.bind(window);
|
|
233
|
+
|
|
234
|
+
function hookFetch() {
|
|
235
|
+
window.fetch = async function (input, init = {}) {
|
|
236
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
237
|
+
const method = init.method || (input instanceof Request ? input.method : "GET");
|
|
238
|
+
const startTime = Date.now();
|
|
239
|
+
let requestBody = null;
|
|
240
|
+
|
|
241
|
+
if (CAPTURE_BODIES && init.body) {
|
|
242
|
+
requestBody = await readBody(init.body);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
const response = await originalFetch(input, init);
|
|
247
|
+
const duration = Date.now() - startTime;
|
|
248
|
+
|
|
249
|
+
// Clone to read body without consuming
|
|
250
|
+
let responseBody = null;
|
|
251
|
+
if (CAPTURE_BODIES) {
|
|
252
|
+
try {
|
|
253
|
+
const clone = response.clone();
|
|
254
|
+
const text = await clone.text();
|
|
255
|
+
responseBody = text.length > MAX_BODY_SIZE ? text.slice(0, MAX_BODY_SIZE) + "...[truncated]" : text;
|
|
256
|
+
} catch {
|
|
257
|
+
responseBody = "[could not read response body]";
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
send({
|
|
262
|
+
type: "network",
|
|
263
|
+
method: method.toUpperCase(),
|
|
264
|
+
url,
|
|
265
|
+
status: response.status,
|
|
266
|
+
statusText: response.statusText,
|
|
267
|
+
duration,
|
|
268
|
+
requestHeaders: init.headers ? serializeHeaders(init.headers) : null,
|
|
269
|
+
responseHeaders: serializeHeaders(response.headers),
|
|
270
|
+
requestBody,
|
|
271
|
+
responseBody,
|
|
272
|
+
timestamp: startTime,
|
|
273
|
+
initiator: "fetch",
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
return response;
|
|
277
|
+
} catch (err) {
|
|
278
|
+
send({
|
|
279
|
+
type: "network",
|
|
280
|
+
method: method.toUpperCase(),
|
|
281
|
+
url,
|
|
282
|
+
status: 0,
|
|
283
|
+
error: err.message,
|
|
284
|
+
duration: Date.now() - startTime,
|
|
285
|
+
requestBody,
|
|
286
|
+
timestamp: startTime,
|
|
287
|
+
initiator: "fetch",
|
|
288
|
+
});
|
|
289
|
+
throw err;
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
// XHR hooking
|
|
296
|
+
// ---------------------------------------------------------------------------
|
|
297
|
+
|
|
298
|
+
const XHROpen = XMLHttpRequest.prototype.open;
|
|
299
|
+
const XHRSend = XMLHttpRequest.prototype.send;
|
|
300
|
+
const XHRSetHeader = XMLHttpRequest.prototype.setRequestHeader;
|
|
301
|
+
|
|
302
|
+
function hookXHR() {
|
|
303
|
+
XMLHttpRequest.prototype.open = function (method, url, ...rest) {
|
|
304
|
+
this.__bridge = { method: method.toUpperCase(), url: String(url), headers: {}, startTime: null };
|
|
305
|
+
return XHROpen.call(this, method, url, ...rest);
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
XMLHttpRequest.prototype.setRequestHeader = function (name, value) {
|
|
309
|
+
if (this.__bridge) {
|
|
310
|
+
this.__bridge.headers[name] = value;
|
|
311
|
+
}
|
|
312
|
+
return XHRSetHeader.call(this, name, value);
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
XMLHttpRequest.prototype.send = function (body) {
|
|
316
|
+
if (this.__bridge) {
|
|
317
|
+
this.__bridge.startTime = Date.now();
|
|
318
|
+
if (CAPTURE_BODIES && body) {
|
|
319
|
+
this.__bridge.requestBody = typeof body === "string" ? body.slice(0, MAX_BODY_SIZE) : "[non-string body]";
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
this.addEventListener("loadend", () => {
|
|
323
|
+
const b = this.__bridge;
|
|
324
|
+
if (!b) return;
|
|
325
|
+
|
|
326
|
+
let responseBody = null;
|
|
327
|
+
if (CAPTURE_BODIES) {
|
|
328
|
+
try {
|
|
329
|
+
const text = this.responseText;
|
|
330
|
+
responseBody = text && text.length > MAX_BODY_SIZE ? text.slice(0, MAX_BODY_SIZE) + "...[truncated]" : text;
|
|
331
|
+
} catch {
|
|
332
|
+
responseBody = "[could not read response]";
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
send({
|
|
337
|
+
type: "network",
|
|
338
|
+
method: b.method,
|
|
339
|
+
url: b.url,
|
|
340
|
+
status: this.status,
|
|
341
|
+
statusText: this.statusText,
|
|
342
|
+
duration: Date.now() - b.startTime,
|
|
343
|
+
requestHeaders: Object.keys(b.headers).length > 0 ? b.headers : null,
|
|
344
|
+
responseHeaders: parseXHRHeaders(this.getAllResponseHeaders()),
|
|
345
|
+
requestBody: b.requestBody || null,
|
|
346
|
+
responseBody,
|
|
347
|
+
timestamp: b.startTime,
|
|
348
|
+
initiator: "xhr",
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return XHRSend.call(this, body);
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function parseXHRHeaders(raw) {
|
|
358
|
+
if (!raw) return null;
|
|
359
|
+
const headers = {};
|
|
360
|
+
raw.trim().split("\r\n").forEach((line) => {
|
|
361
|
+
const idx = line.indexOf(":");
|
|
362
|
+
if (idx > 0) headers[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
|
|
363
|
+
});
|
|
364
|
+
return headers;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// ---------------------------------------------------------------------------
|
|
368
|
+
// Error hooking
|
|
369
|
+
// ---------------------------------------------------------------------------
|
|
370
|
+
|
|
371
|
+
function hookErrors() {
|
|
372
|
+
window.addEventListener("error", (event) => {
|
|
373
|
+
send({
|
|
374
|
+
type: "error",
|
|
375
|
+
message: event.message,
|
|
376
|
+
filename: event.filename,
|
|
377
|
+
lineno: event.lineno,
|
|
378
|
+
colno: event.colno,
|
|
379
|
+
stack: event.error?.stack || null,
|
|
380
|
+
timestamp: Date.now(),
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
window.addEventListener("unhandledrejection", (event) => {
|
|
385
|
+
const reason = event.reason;
|
|
386
|
+
send({
|
|
387
|
+
type: "error",
|
|
388
|
+
message: reason?.message || String(reason),
|
|
389
|
+
stack: reason?.stack || null,
|
|
390
|
+
filename: null,
|
|
391
|
+
lineno: null,
|
|
392
|
+
colno: null,
|
|
393
|
+
timestamp: Date.now(),
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// ---------------------------------------------------------------------------
|
|
399
|
+
// Navigation tracking
|
|
400
|
+
// ---------------------------------------------------------------------------
|
|
401
|
+
|
|
402
|
+
function hookNavigation() {
|
|
403
|
+
// Track pushState/replaceState
|
|
404
|
+
const origPushState = history.pushState.bind(history);
|
|
405
|
+
const origReplaceState = history.replaceState.bind(history);
|
|
406
|
+
|
|
407
|
+
history.pushState = function (...args) {
|
|
408
|
+
origPushState(...args);
|
|
409
|
+
setTimeout(sendPageInfo, 0);
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
history.replaceState = function (...args) {
|
|
413
|
+
origReplaceState(...args);
|
|
414
|
+
setTimeout(sendPageInfo, 0);
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
window.addEventListener("popstate", () => setTimeout(sendPageInfo, 0));
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// ---------------------------------------------------------------------------
|
|
421
|
+
// Server request handlers (pull queries from Claude)
|
|
422
|
+
// ---------------------------------------------------------------------------
|
|
423
|
+
|
|
424
|
+
function handleServerRequest(msg) {
|
|
425
|
+
const { id, action, params } = msg;
|
|
426
|
+
|
|
427
|
+
const handlers = {
|
|
428
|
+
getLocalStorage: () => {
|
|
429
|
+
if (params.key) {
|
|
430
|
+
const val = localStorage.getItem(params.key);
|
|
431
|
+
return { key: params.key, value: tryParseJson(val) };
|
|
432
|
+
}
|
|
433
|
+
const all = {};
|
|
434
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
435
|
+
const key = localStorage.key(i);
|
|
436
|
+
all[key] = tryParseJson(localStorage.getItem(key));
|
|
437
|
+
}
|
|
438
|
+
return all;
|
|
439
|
+
},
|
|
440
|
+
|
|
441
|
+
getSessionStorage: () => {
|
|
442
|
+
if (params.key) {
|
|
443
|
+
const val = sessionStorage.getItem(params.key);
|
|
444
|
+
return { key: params.key, value: tryParseJson(val) };
|
|
445
|
+
}
|
|
446
|
+
const all = {};
|
|
447
|
+
for (let i = 0; i < sessionStorage.length; i++) {
|
|
448
|
+
const key = sessionStorage.key(i);
|
|
449
|
+
all[key] = tryParseJson(sessionStorage.getItem(key));
|
|
450
|
+
}
|
|
451
|
+
return all;
|
|
452
|
+
},
|
|
453
|
+
|
|
454
|
+
getCookies: () => {
|
|
455
|
+
return document.cookie.split(";").reduce((acc, cookie) => {
|
|
456
|
+
const [key, ...rest] = cookie.trim().split("=");
|
|
457
|
+
if (key) acc[key.trim()] = decodeURIComponent(rest.join("="));
|
|
458
|
+
return acc;
|
|
459
|
+
}, {});
|
|
460
|
+
},
|
|
461
|
+
|
|
462
|
+
getPageInfo: () => ({
|
|
463
|
+
url: location.href,
|
|
464
|
+
title: document.title,
|
|
465
|
+
readyState: document.readyState,
|
|
466
|
+
viewport: { width: window.innerWidth, height: window.innerHeight },
|
|
467
|
+
scrollPosition: { x: window.scrollX, y: window.scrollY },
|
|
468
|
+
documentSize: { width: document.documentElement.scrollWidth, height: document.documentElement.scrollHeight },
|
|
469
|
+
activeElement: document.activeElement ? {
|
|
470
|
+
tag: document.activeElement.tagName,
|
|
471
|
+
id: document.activeElement.id,
|
|
472
|
+
className: document.activeElement.className,
|
|
473
|
+
} : null,
|
|
474
|
+
}),
|
|
475
|
+
|
|
476
|
+
executeJs: () => {
|
|
477
|
+
try {
|
|
478
|
+
// eslint-disable-next-line no-eval
|
|
479
|
+
const result = eval(params.code);
|
|
480
|
+
return { success: true, result: serializeArg(result) };
|
|
481
|
+
} catch (err) {
|
|
482
|
+
return { success: false, error: err.message, stack: err.stack };
|
|
483
|
+
}
|
|
484
|
+
},
|
|
485
|
+
|
|
486
|
+
getReduxState: () => {
|
|
487
|
+
// Try multiple ways to access Redux store
|
|
488
|
+
let state = null;
|
|
489
|
+
|
|
490
|
+
// Method 1: __REDUX_STORE__
|
|
491
|
+
if (window.__REDUX_STORE__) {
|
|
492
|
+
state = window.__REDUX_STORE__.getState();
|
|
493
|
+
}
|
|
494
|
+
// Method 2: __store__ (common pattern)
|
|
495
|
+
else if (window.__store__) {
|
|
496
|
+
state = window.__store__.getState();
|
|
497
|
+
}
|
|
498
|
+
// Method 3: Try to find it via Redux DevTools
|
|
499
|
+
else {
|
|
500
|
+
try {
|
|
501
|
+
const devTools = window.__REDUX_DEVTOOLS_EXTENSION__;
|
|
502
|
+
if (devTools) {
|
|
503
|
+
state = "[Redux DevTools detected but cannot access state directly. Expose store as window.__REDUX_STORE__ for direct access.]";
|
|
504
|
+
}
|
|
505
|
+
} catch {
|
|
506
|
+
// ignore
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (!state) {
|
|
511
|
+
return { error: "Redux store not found. Add `window.__REDUX_STORE__ = store` in your store setup for direct access." };
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (params.path) {
|
|
515
|
+
const parts = params.path.split(".");
|
|
516
|
+
let current = state;
|
|
517
|
+
for (const part of parts) {
|
|
518
|
+
if (current == null) break;
|
|
519
|
+
current = current[part];
|
|
520
|
+
}
|
|
521
|
+
return { path: params.path, value: current };
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return state;
|
|
525
|
+
},
|
|
526
|
+
|
|
527
|
+
getReactQueryCache: () => {
|
|
528
|
+
// Try to find React Query client
|
|
529
|
+
const queryClient = window.__REACT_QUERY_CLIENT__;
|
|
530
|
+
|
|
531
|
+
if (!queryClient) {
|
|
532
|
+
return { error: "React Query client not found. Add `window.__REACT_QUERY_CLIENT__ = queryClient` in your QueryClientProvider setup." };
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
try {
|
|
536
|
+
const queryCache = queryClient.getQueryCache();
|
|
537
|
+
const queries = queryCache.getAll().map((query) => ({
|
|
538
|
+
queryKey: query.queryKey,
|
|
539
|
+
state: query.state.status,
|
|
540
|
+
dataUpdatedAt: query.state.dataUpdatedAt ? new Date(query.state.dataUpdatedAt).toISOString() : null,
|
|
541
|
+
isStale: query.isStale(),
|
|
542
|
+
data: query.state.data,
|
|
543
|
+
error: query.state.error?.message || null,
|
|
544
|
+
}));
|
|
545
|
+
|
|
546
|
+
if (params.query_key_contains) {
|
|
547
|
+
return queries.filter((q) =>
|
|
548
|
+
JSON.stringify(q.queryKey).includes(params.query_key_contains)
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
return queries;
|
|
553
|
+
} catch (err) {
|
|
554
|
+
return { error: `Failed to read React Query cache: ${err.message}` };
|
|
555
|
+
}
|
|
556
|
+
},
|
|
557
|
+
|
|
558
|
+
getPerformance: () => {
|
|
559
|
+
const nav = performance.getEntriesByType("navigation")[0];
|
|
560
|
+
const paint = performance.getEntriesByType("paint");
|
|
561
|
+
const resources = performance.getEntriesByType("resource").slice(-20);
|
|
562
|
+
|
|
563
|
+
return {
|
|
564
|
+
navigation: nav ? {
|
|
565
|
+
domContentLoaded: Math.round(nav.domContentLoadedEventEnd - nav.startTime),
|
|
566
|
+
loadComplete: Math.round(nav.loadEventEnd - nav.startTime),
|
|
567
|
+
ttfb: Math.round(nav.responseStart - nav.startTime),
|
|
568
|
+
domInteractive: Math.round(nav.domInteractive - nav.startTime),
|
|
569
|
+
} : null,
|
|
570
|
+
paint: paint.map((p) => ({ name: p.name, time: Math.round(p.startTime) })),
|
|
571
|
+
memory: performance.memory ? {
|
|
572
|
+
usedJSHeapSize: `${(performance.memory.usedJSHeapSize / 1048576).toFixed(1)}MB`,
|
|
573
|
+
totalJSHeapSize: `${(performance.memory.totalJSHeapSize / 1048576).toFixed(1)}MB`,
|
|
574
|
+
jsHeapSizeLimit: `${(performance.memory.jsHeapSizeLimit / 1048576).toFixed(1)}MB`,
|
|
575
|
+
} : null,
|
|
576
|
+
recentResources: resources.map((r) => ({
|
|
577
|
+
name: r.name.split("/").pop(),
|
|
578
|
+
type: r.initiatorType,
|
|
579
|
+
duration: `${Math.round(r.duration)}ms`,
|
|
580
|
+
size: r.transferSize ? `${(r.transferSize / 1024).toFixed(1)}KB` : null,
|
|
581
|
+
})),
|
|
582
|
+
};
|
|
583
|
+
},
|
|
584
|
+
|
|
585
|
+
getDomSnapshot: () => {
|
|
586
|
+
const el = document.querySelector(params.selector || "body");
|
|
587
|
+
if (!el) return { error: `Element not found: ${params.selector}` };
|
|
588
|
+
|
|
589
|
+
function traverse(node, depth) {
|
|
590
|
+
if (depth > (params.max_depth || 5)) return { tag: "...", truncated: true };
|
|
591
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
592
|
+
const text = node.textContent.trim();
|
|
593
|
+
return text ? text : null;
|
|
594
|
+
}
|
|
595
|
+
if (node.nodeType !== Node.ELEMENT_NODE) return null;
|
|
596
|
+
|
|
597
|
+
const result = {
|
|
598
|
+
tag: node.tagName.toLowerCase(),
|
|
599
|
+
...(node.id ? { id: node.id } : {}),
|
|
600
|
+
...(node.className && typeof node.className === "string" ? { class: node.className } : {}),
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
// Include key attributes
|
|
604
|
+
const importantAttrs = ["href", "src", "type", "name", "value", "placeholder", "role", "aria-label", "data-testid"];
|
|
605
|
+
importantAttrs.forEach((attr) => {
|
|
606
|
+
const val = node.getAttribute(attr);
|
|
607
|
+
if (val) result[attr] = val;
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
const children = [];
|
|
611
|
+
for (const child of node.childNodes) {
|
|
612
|
+
const c = traverse(child, depth + 1);
|
|
613
|
+
if (c) children.push(c);
|
|
614
|
+
}
|
|
615
|
+
if (children.length > 0) result.children = children;
|
|
616
|
+
|
|
617
|
+
return result;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
return traverse(el, 0);
|
|
621
|
+
},
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
const handler = handlers[action];
|
|
625
|
+
if (!handler) {
|
|
626
|
+
send({ type: "response", id, data: { error: `Unknown action: ${action}` } });
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
try {
|
|
631
|
+
const data = handler();
|
|
632
|
+
send({ type: "response", id, data });
|
|
633
|
+
} catch (err) {
|
|
634
|
+
send({ type: "response", id, data: { error: err.message } });
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// ---------------------------------------------------------------------------
|
|
639
|
+
// Utilities
|
|
640
|
+
// ---------------------------------------------------------------------------
|
|
641
|
+
|
|
642
|
+
function tryParseJson(str) {
|
|
643
|
+
if (str === null || str === undefined) return str;
|
|
644
|
+
try {
|
|
645
|
+
return JSON.parse(str);
|
|
646
|
+
} catch {
|
|
647
|
+
return str;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function serializeHeaders(headers) {
|
|
652
|
+
if (!headers) return null;
|
|
653
|
+
if (headers instanceof Headers) {
|
|
654
|
+
const obj = {};
|
|
655
|
+
headers.forEach((value, key) => { obj[key] = value; });
|
|
656
|
+
return obj;
|
|
657
|
+
}
|
|
658
|
+
if (typeof headers === "object") return { ...headers };
|
|
659
|
+
return null;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
async function readBody(body) {
|
|
663
|
+
if (!body) return null;
|
|
664
|
+
if (typeof body === "string") return body.slice(0, MAX_BODY_SIZE);
|
|
665
|
+
if (body instanceof FormData) {
|
|
666
|
+
const obj = {};
|
|
667
|
+
body.forEach((val, key) => { obj[key] = val instanceof File ? `[File: ${val.name}]` : val; });
|
|
668
|
+
return obj;
|
|
669
|
+
}
|
|
670
|
+
if (body instanceof URLSearchParams) return body.toString().slice(0, MAX_BODY_SIZE);
|
|
671
|
+
if (body instanceof Blob) {
|
|
672
|
+
try {
|
|
673
|
+
const text = await body.text();
|
|
674
|
+
return text.slice(0, MAX_BODY_SIZE);
|
|
675
|
+
} catch {
|
|
676
|
+
return "[Blob body]";
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
return "[unknown body type]";
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// ---------------------------------------------------------------------------
|
|
683
|
+
// Initialize
|
|
684
|
+
// ---------------------------------------------------------------------------
|
|
685
|
+
|
|
686
|
+
hookConsole();
|
|
687
|
+
hookFetch();
|
|
688
|
+
hookXHR();
|
|
689
|
+
hookErrors();
|
|
690
|
+
hookNavigation();
|
|
691
|
+
|
|
692
|
+
// Wait for body before creating badge
|
|
693
|
+
if (document.body) {
|
|
694
|
+
createBadge();
|
|
695
|
+
} else {
|
|
696
|
+
document.addEventListener("DOMContentLoaded", createBadge);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
connect();
|
|
700
|
+
|
|
701
|
+
// Public API
|
|
702
|
+
window.__browserBridge = {
|
|
703
|
+
disconnect: () => {
|
|
704
|
+
if (ws) ws.close();
|
|
705
|
+
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
706
|
+
if (badge) badge.remove();
|
|
707
|
+
|
|
708
|
+
// Restore originals
|
|
709
|
+
console.log = originalConsole.log;
|
|
710
|
+
console.warn = originalConsole.warn;
|
|
711
|
+
console.error = originalConsole.error;
|
|
712
|
+
console.info = originalConsole.info;
|
|
713
|
+
console.debug = originalConsole.debug;
|
|
714
|
+
window.fetch = originalFetch;
|
|
715
|
+
XMLHttpRequest.prototype.open = XHROpen;
|
|
716
|
+
XMLHttpRequest.prototype.send = XHRSend;
|
|
717
|
+
XMLHttpRequest.prototype.setRequestHeader = XHRSetHeader;
|
|
718
|
+
|
|
719
|
+
window.__browserBridge = null;
|
|
720
|
+
originalConsole.log("[Bridge] Disconnected and hooks restored.");
|
|
721
|
+
},
|
|
722
|
+
isConnected: () => connected,
|
|
723
|
+
reconnect: () => { if (ws) ws.close(); connect(); },
|
|
724
|
+
};
|
|
725
|
+
})();
|