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/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
+ })();