browsertrack 0.0.1 → 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.
Files changed (101) hide show
  1. package/README.md +143 -0
  2. package/dist/chunk-6A7FFDIB.js +221 -0
  3. package/dist/chunk-6A7FFDIB.js.map +1 -0
  4. package/dist/chunk-ANMR5WBH.js +659 -0
  5. package/dist/chunk-ANMR5WBH.js.map +1 -0
  6. package/dist/chunk-BSKNG4V7.js +1199 -0
  7. package/dist/chunk-BSKNG4V7.js.map +1 -0
  8. package/dist/chunk-X7C5CHBO.js +2080 -0
  9. package/dist/chunk-X7C5CHBO.js.map +1 -0
  10. package/dist/chunk-ZLNGOOH2.js +554 -0
  11. package/dist/chunk-ZLNGOOH2.js.map +1 -0
  12. package/dist/cli/index.js +2829 -0
  13. package/dist/cli/index.js.map +1 -0
  14. package/dist/client/index.cjs +2218 -0
  15. package/dist/client/index.d.ts +197 -0
  16. package/dist/client/index.js +20 -0
  17. package/dist/client/index.js.map +1 -0
  18. package/dist/client.iife.js +309 -0
  19. package/dist/core/index.d.ts +78 -0
  20. package/dist/core/index.js +31 -0
  21. package/dist/core/index.js.map +1 -0
  22. package/dist/daemon/index.d.ts +58 -0
  23. package/dist/daemon/index.js +32 -0
  24. package/dist/daemon/index.js.map +1 -0
  25. package/dist/engine-DKloFjvs.d.ts +157 -0
  26. package/dist/index.d.ts +10 -0
  27. package/dist/index.js +50 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/mcp/index.d.ts +11 -0
  30. package/dist/mcp/index.js +12 -0
  31. package/dist/mcp/index.js.map +1 -0
  32. package/dist/notes-CBvN91Wf.d.ts +260 -0
  33. package/dist/projects-CY8ungMt.d.ts +99 -0
  34. package/dist/server-CN-se8td.d.ts +55 -0
  35. package/examples/test-app/index.html +299 -0
  36. package/package.json +52 -7
  37. package/packages/cli/src/index.ts +413 -0
  38. package/packages/client/package.json +28 -0
  39. package/packages/client/src/breadcrumbs.ts +30 -0
  40. package/packages/client/src/client.ts +278 -0
  41. package/packages/client/src/commands/handler.ts +301 -0
  42. package/packages/client/src/config.ts +37 -0
  43. package/packages/client/src/index.ts +50 -0
  44. package/packages/client/src/interceptors/console.ts +68 -0
  45. package/packages/client/src/interceptors/interaction.ts +119 -0
  46. package/packages/client/src/interceptors/navigation.ts +93 -0
  47. package/packages/client/src/interceptors/network.ts +165 -0
  48. package/packages/client/src/interceptors/runtime.ts +65 -0
  49. package/packages/client/src/notes/inspector.ts +943 -0
  50. package/packages/client/src/screenshot/browser-script-driver.ts +192 -0
  51. package/packages/client/src/screenshot/driver.ts +14 -0
  52. package/packages/client/src/transport/websocket.ts +179 -0
  53. package/packages/client/tsconfig.json +8 -0
  54. package/packages/core/dist/index.d.ts +317 -0
  55. package/packages/core/dist/index.js +221 -0
  56. package/packages/core/package.json +23 -0
  57. package/packages/core/src/fingerprint.ts +120 -0
  58. package/packages/core/src/index.ts +9 -0
  59. package/packages/core/src/redaction.ts +139 -0
  60. package/packages/core/src/selector.ts +77 -0
  61. package/packages/core/src/types/commands.ts +101 -0
  62. package/packages/core/src/types/events.ts +108 -0
  63. package/packages/core/src/types/incidents.ts +54 -0
  64. package/packages/core/src/types/notes.ts +106 -0
  65. package/packages/core/src/types/probes.ts +44 -0
  66. package/packages/core/src/types/projects.ts +20 -0
  67. package/packages/core/tsconfig.json +8 -0
  68. package/packages/daemon/src/config.ts +29 -0
  69. package/packages/daemon/src/incidents/engine.ts +211 -0
  70. package/packages/daemon/src/index.ts +18 -0
  71. package/packages/daemon/src/notes/engine.ts +92 -0
  72. package/packages/daemon/src/notes/verification.ts +191 -0
  73. package/packages/daemon/src/server/daemon.ts +112 -0
  74. package/packages/daemon/src/server/http.ts +165 -0
  75. package/packages/daemon/src/server/ws.ts +175 -0
  76. package/packages/daemon/src/session/manager.ts +126 -0
  77. package/packages/daemon/src/storage/db.ts +733 -0
  78. package/packages/daemon/src/storage/screenshot-store.ts +49 -0
  79. package/packages/daemon/src/verification/engine.ts +264 -0
  80. package/packages/mcp/src/handlers.ts +398 -0
  81. package/packages/mcp/src/index.ts +3 -0
  82. package/packages/mcp/src/server.ts +86 -0
  83. package/packages/mcp/src/tools.ts +236 -0
  84. package/src/index.ts +4 -0
  85. package/test/client/interceptors.test.ts +69 -0
  86. package/test/core/fingerprint.test.ts +53 -0
  87. package/test/core/notes.test.ts +66 -0
  88. package/test/core/redaction.test.ts +48 -0
  89. package/test/daemon/incident-engine.test.ts +98 -0
  90. package/test/daemon/notes-storage.test.ts +130 -0
  91. package/test/daemon/notes-verification.test.ts +135 -0
  92. package/test/daemon/storage.test.ts +123 -0
  93. package/test/daemon/verification-engine.test.ts +88 -0
  94. package/test/e2e/daemon-mcp-e2e.test.ts +153 -0
  95. package/test/e2e/visual-notes-e2e.test.ts +205 -0
  96. package/test/mcp/handlers.test.ts +116 -0
  97. package/test/mcp/notes.test.ts +107 -0
  98. package/tsconfig.base.json +17 -0
  99. package/tsconfig.json +26 -0
  100. package/tsup.config.ts +54 -0
  101. package/vitest.config.ts +9 -0
@@ -0,0 +1,2218 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
21
+
22
+ // packages/client/src/index.ts
23
+ var src_exports = {};
24
+ __export(src_exports, {
25
+ BreadcrumbBuffer: () => BreadcrumbBuffer,
26
+ BrowserScriptScreenshotDriver: () => BrowserScriptScreenshotDriver,
27
+ BrowserTrackClient: () => BrowserTrackClient,
28
+ DEFAULT_OPTIONS: () => DEFAULT_OPTIONS,
29
+ NoteInspector: () => NoteInspector,
30
+ getClient: () => getClient,
31
+ init: () => init
32
+ });
33
+ module.exports = __toCommonJS(src_exports);
34
+
35
+ // packages/client/src/breadcrumbs.ts
36
+ var BreadcrumbBuffer = class {
37
+ constructor(capacity = 50) {
38
+ __publicField(this, "capacity");
39
+ __publicField(this, "buffer", []);
40
+ this.capacity = capacity;
41
+ }
42
+ add(breadcrumb) {
43
+ const item = {
44
+ ...breadcrumb,
45
+ timestamp: breadcrumb.timestamp || Date.now()
46
+ };
47
+ this.buffer.push(item);
48
+ if (this.buffer.length > this.capacity) {
49
+ this.buffer.shift();
50
+ }
51
+ }
52
+ getRecent() {
53
+ return [...this.buffer];
54
+ }
55
+ clear() {
56
+ this.buffer = [];
57
+ }
58
+ };
59
+
60
+ // packages/core/src/redaction.ts
61
+ var SENSITIVE_KEY_PATTERNS = [
62
+ /^authorization$/i,
63
+ /^cookie$/i,
64
+ /^set-cookie$/i,
65
+ /password/i,
66
+ /token/i,
67
+ /secret/i,
68
+ /api[-_]?key/i,
69
+ /access[-_]?token/i,
70
+ /refresh[-_]?token/i,
71
+ /credentials/i,
72
+ /private[-_]?key/i,
73
+ /ssn/i,
74
+ /credit[-_]?card/i,
75
+ /cvv/i
76
+ ];
77
+ var SENSITIVE_QUERY_PARAMS = [
78
+ "token",
79
+ "auth",
80
+ "key",
81
+ "apikey",
82
+ "api_key",
83
+ "secret",
84
+ "password",
85
+ "access_token",
86
+ "refresh_token",
87
+ "code",
88
+ "signature"
89
+ ];
90
+ var REDACTED_PLACEHOLDER = "[REDACTED]";
91
+ function isSensitiveKey(key) {
92
+ if (!key) return false;
93
+ const cleaned = key.replace(/[-_]/g, "");
94
+ return SENSITIVE_KEY_PATTERNS.some((pattern) => pattern.test(key) || pattern.test(cleaned));
95
+ }
96
+ function redactUrl(rawUrl) {
97
+ if (!rawUrl) return rawUrl;
98
+ try {
99
+ const isRelative = !rawUrl.startsWith("http://") && !rawUrl.startsWith("https://") && !rawUrl.startsWith("ws://") && !rawUrl.startsWith("wss://");
100
+ const base = "http://localhost";
101
+ const parsed = new URL(rawUrl, base);
102
+ let changed = false;
103
+ for (const param of SENSITIVE_QUERY_PARAMS) {
104
+ if (parsed.searchParams.has(param)) {
105
+ parsed.searchParams.set(param, REDACTED_PLACEHOLDER);
106
+ changed = true;
107
+ }
108
+ }
109
+ for (const key of Array.from(parsed.searchParams.keys())) {
110
+ if (isSensitiveKey(key)) {
111
+ parsed.searchParams.set(key, REDACTED_PLACEHOLDER);
112
+ changed = true;
113
+ }
114
+ }
115
+ if (!changed) return rawUrl;
116
+ let result = isRelative ? parsed.pathname + parsed.search + parsed.hash : parsed.toString();
117
+ result = result.replace(/%5BREDACTED%5D/g, REDACTED_PLACEHOLDER);
118
+ return result;
119
+ } catch {
120
+ let safe = rawUrl;
121
+ for (const param of SENSITIVE_QUERY_PARAMS) {
122
+ const reg = new RegExp(`([?&]${param}=)[^&#]+`, "gi");
123
+ safe = safe.replace(reg, `$1${REDACTED_PLACEHOLDER}`);
124
+ }
125
+ return safe;
126
+ }
127
+ }
128
+
129
+ // packages/core/src/selector.ts
130
+ function getSemanticSelector(element, options = {}) {
131
+ if (!element || !element.tagName) return "unknown";
132
+ const maxClasses = options.maxClasses ?? 2;
133
+ const testId = element.getAttribute("data-testid") || element.getAttribute("data-test") || element.getAttribute("data-cy") || element.getAttribute("data-qa");
134
+ if (testId) {
135
+ return `[data-testid="${testId}"]`;
136
+ }
137
+ const id = element.id;
138
+ if (id && !/^[0-9]+$/.test(id) && !/[0-9a-f]{8}-[0-9a-f]{4}/i.test(id)) {
139
+ return `#${id}`;
140
+ }
141
+ const ariaLabel = element.getAttribute("aria-label");
142
+ if (ariaLabel && ariaLabel.length < 30) {
143
+ return `${element.tagName.toLowerCase()}[aria-label="${ariaLabel}"]`;
144
+ }
145
+ const nameAttr = element.getAttribute("name");
146
+ if (nameAttr) {
147
+ return `${element.tagName.toLowerCase()}[name="${nameAttr}"]`;
148
+ }
149
+ const tag = element.tagName.toLowerCase();
150
+ const classList = Array.from(element.classList || []).filter((c) => !c.startsWith("css-") && !c.startsWith("_") && !/^[a-z0-9]{5,}$/i.test(c)).slice(0, maxClasses);
151
+ if (classList.length > 0) {
152
+ const classStr = classList.map((c) => `.${c}`).join("");
153
+ return `${tag}${classStr}`;
154
+ }
155
+ if (element.parentElement && element.parentElement !== document.body && element.parentElement !== document.documentElement) {
156
+ const parentSelector = getSemanticSelector(element.parentElement, { maxClasses: 1 });
157
+ if (parentSelector && parentSelector !== "unknown" && !parentSelector.includes(">")) {
158
+ return `${parentSelector} > ${tag}`;
159
+ }
160
+ }
161
+ return tag;
162
+ }
163
+ function truncate(str, maxLength = 200) {
164
+ if (!str) return "";
165
+ if (str.length <= maxLength) return str;
166
+ return str.slice(0, maxLength) + "...";
167
+ }
168
+
169
+ // packages/client/src/interceptors/interaction.ts
170
+ function extractElementSummary(el) {
171
+ const selector = getSemanticSelector(el);
172
+ const tag = el.tagName.toLowerCase();
173
+ const id = el.id || void 0;
174
+ const classes = Array.from(el.classList || []);
175
+ let boundingRect = void 0;
176
+ let visible = true;
177
+ try {
178
+ const rect = el.getBoundingClientRect();
179
+ boundingRect = {
180
+ x: Math.round(rect.x),
181
+ y: Math.round(rect.y),
182
+ width: Math.round(rect.width),
183
+ height: Math.round(rect.height),
184
+ top: Math.round(rect.top),
185
+ left: Math.round(rect.left),
186
+ bottom: Math.round(rect.bottom),
187
+ right: Math.round(rect.right)
188
+ };
189
+ const style = window.getComputedStyle(el);
190
+ visible = style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0" && rect.width > 0 && rect.height > 0;
191
+ } catch {
192
+ }
193
+ let innerText;
194
+ if (tag !== "input" && tag !== "textarea" && tag !== "select") {
195
+ innerText = truncate(el.textContent?.trim(), 80);
196
+ }
197
+ const outerHTML = truncate(el.outerHTML?.trim(), 200);
198
+ return {
199
+ selector,
200
+ tag,
201
+ id,
202
+ classes,
203
+ boundingRect,
204
+ visible,
205
+ innerText,
206
+ outerHTML
207
+ };
208
+ }
209
+ function setupInteractionInterceptors(onInteraction) {
210
+ if (typeof document === "undefined") return () => {
211
+ };
212
+ const onClick = (event) => {
213
+ try {
214
+ const target = event.target;
215
+ if (!target || !(target instanceof Element)) return;
216
+ const summary = extractElementSummary(target);
217
+ const message = `click ${summary.selector}${summary.innerText ? ` ("${summary.innerText}")` : ""}`;
218
+ onInteraction(
219
+ {
220
+ type: "click",
221
+ category: "ui",
222
+ message,
223
+ timestamp: Date.now(),
224
+ element: summary
225
+ },
226
+ summary
227
+ );
228
+ } catch {
229
+ }
230
+ };
231
+ const onSubmit = (event) => {
232
+ try {
233
+ const target = event.target;
234
+ if (!target || !(target instanceof Element)) return;
235
+ const summary = extractElementSummary(target);
236
+ const formAction = target.getAttribute("action") || "";
237
+ const message = `submit form ${summary.selector}${formAction ? ` (action: ${formAction})` : ""}`;
238
+ onInteraction(
239
+ {
240
+ type: "submit",
241
+ category: "ui",
242
+ message,
243
+ timestamp: Date.now(),
244
+ element: summary
245
+ },
246
+ summary
247
+ );
248
+ } catch {
249
+ }
250
+ };
251
+ document.addEventListener("click", onClick, { capture: true, passive: true });
252
+ document.addEventListener("submit", onSubmit, { capture: true, passive: true });
253
+ return () => {
254
+ document.removeEventListener("click", onClick, { capture: true });
255
+ document.removeEventListener("submit", onSubmit, { capture: true });
256
+ };
257
+ }
258
+
259
+ // packages/client/src/commands/handler.ts
260
+ var ClientCommandHandler = class {
261
+ constructor(screenshotDriver) {
262
+ __publicField(this, "screenshotDriver");
263
+ this.screenshotDriver = screenshotDriver;
264
+ }
265
+ async executeCommand(command) {
266
+ try {
267
+ switch (command.type) {
268
+ case "reload":
269
+ return this.handleReload(command);
270
+ case "navigate":
271
+ return this.handleNavigate(command);
272
+ case "get_page_state":
273
+ return this.handleGetPageState(command);
274
+ case "query_element":
275
+ return this.handleQueryElement(command);
276
+ case "capture_element":
277
+ return await this.handleCaptureElement(command);
278
+ case "check_overflow":
279
+ return this.handleCheckOverflow(command);
280
+ case "get_element_rect":
281
+ return this.handleGetElementRect(command);
282
+ case "get_element_style":
283
+ return this.handleGetElementStyle(command);
284
+ default:
285
+ return {
286
+ id: command.id,
287
+ ok: false,
288
+ error: `Unsupported command type: ${command.type}`
289
+ };
290
+ }
291
+ } catch (err) {
292
+ return {
293
+ id: command.id,
294
+ ok: false,
295
+ error: err?.message || "Command execution failed"
296
+ };
297
+ }
298
+ }
299
+ handleReload(command) {
300
+ if (typeof window !== "undefined" && window.location) {
301
+ setTimeout(() => {
302
+ window.location.reload();
303
+ }, 50);
304
+ return { id: command.id, ok: true, result: { message: "Reload initiated" } };
305
+ }
306
+ return { id: command.id, ok: false, error: "No browser window available" };
307
+ }
308
+ handleNavigate(command) {
309
+ const targetUrl = command.params?.url;
310
+ if (!targetUrl) {
311
+ return { id: command.id, ok: false, error: "Missing target url parameter" };
312
+ }
313
+ if (typeof window !== "undefined" && window.location) {
314
+ setTimeout(() => {
315
+ window.location.href = targetUrl;
316
+ }, 50);
317
+ return { id: command.id, ok: true, result: { message: `Navigating to ${targetUrl}` } };
318
+ }
319
+ return { id: command.id, ok: false, error: "No browser window available" };
320
+ }
321
+ handleGetPageState(command) {
322
+ if (typeof window === "undefined" || typeof document === "undefined") {
323
+ return { id: command.id, ok: false, error: "No browser document available" };
324
+ }
325
+ const activeEl = document.activeElement && document.activeElement !== document.body ? extractElementSummary(document.activeElement) : void 0;
326
+ return {
327
+ id: command.id,
328
+ ok: true,
329
+ result: {
330
+ url: window.location.href,
331
+ route: window.location.pathname + window.location.search + window.location.hash,
332
+ title: document.title,
333
+ readyState: document.readyState,
334
+ activeElement: activeEl
335
+ }
336
+ };
337
+ }
338
+ handleQueryElement(command) {
339
+ if (typeof document === "undefined") {
340
+ return { id: command.id, ok: false, error: "No browser document available" };
341
+ }
342
+ const selector = command.params?.selector;
343
+ if (!selector) {
344
+ return { id: command.id, ok: false, error: "Missing selector parameter" };
345
+ }
346
+ const element = document.querySelector(selector);
347
+ if (!element) {
348
+ return {
349
+ id: command.id,
350
+ ok: true,
351
+ result: { exists: false }
352
+ };
353
+ }
354
+ const summary = extractElementSummary(element);
355
+ return {
356
+ id: command.id,
357
+ ok: true,
358
+ result: {
359
+ exists: true,
360
+ visible: summary.visible,
361
+ tag: summary.tag,
362
+ id: summary.id,
363
+ classes: summary.classes,
364
+ boundingRect: summary.boundingRect,
365
+ innerText: summary.innerText,
366
+ outerHTML: summary.outerHTML
367
+ }
368
+ };
369
+ }
370
+ async handleCaptureElement(command) {
371
+ const selector = command.params?.selector;
372
+ let targetEl = null;
373
+ if (selector) {
374
+ targetEl = document.querySelector(selector);
375
+ if (!targetEl) {
376
+ return {
377
+ id: command.id,
378
+ ok: false,
379
+ reason: `ELEMENT_NOT_FOUND: ${selector}`,
380
+ error: `Element not found for selector: ${selector}`
381
+ };
382
+ }
383
+ } else {
384
+ targetEl = document.body || document.documentElement;
385
+ }
386
+ const captureRes = await this.screenshotDriver.captureElement(targetEl);
387
+ if (!captureRes.ok) {
388
+ return {
389
+ id: command.id,
390
+ ok: false,
391
+ reason: captureRes.reason || "CAPTURE_FAILED",
392
+ error: captureRes.reason || "Failed to capture element screenshot"
393
+ };
394
+ }
395
+ return {
396
+ id: command.id,
397
+ ok: true,
398
+ result: {
399
+ dataUrl: captureRes.dataUrl,
400
+ format: captureRes.format,
401
+ width: captureRes.width,
402
+ height: captureRes.height
403
+ }
404
+ };
405
+ }
406
+ handleCheckOverflow(command) {
407
+ if (typeof window === "undefined" || typeof document === "undefined") {
408
+ return { id: command.id, ok: false, error: "No browser window available" };
409
+ }
410
+ const selector = command.params?.selector;
411
+ if (!selector) {
412
+ return { id: command.id, ok: false, error: "Missing selector parameter" };
413
+ }
414
+ const el = document.querySelector(selector);
415
+ if (!el) {
416
+ return { id: command.id, ok: false, error: `Element not found: ${selector}` };
417
+ }
418
+ const rect = el.getBoundingClientRect();
419
+ const vw = window.innerWidth;
420
+ const vh = window.innerHeight;
421
+ const overflowRightPx = Math.max(0, Math.round(rect.right - vw));
422
+ const overflowBottomPx = Math.max(0, Math.round(rect.bottom - vh));
423
+ const isViewportOverflow = overflowRightPx > 0 || rect.left < 0;
424
+ let parentOverflow = false;
425
+ if (el.parentElement) {
426
+ parentOverflow = el.parentElement.scrollWidth > el.parentElement.clientWidth;
427
+ }
428
+ return {
429
+ id: command.id,
430
+ ok: true,
431
+ result: {
432
+ selector,
433
+ overflow: isViewportOverflow || parentOverflow,
434
+ viewportWidth: vw,
435
+ viewportHeight: vh,
436
+ rect: {
437
+ x: Math.round(rect.x),
438
+ y: Math.round(rect.y),
439
+ width: Math.round(rect.width),
440
+ height: Math.round(rect.height),
441
+ top: Math.round(rect.top),
442
+ left: Math.round(rect.left),
443
+ bottom: Math.round(rect.bottom),
444
+ right: Math.round(rect.right)
445
+ },
446
+ overflowRightPx,
447
+ overflowBottomPx,
448
+ parentOverflow
449
+ }
450
+ };
451
+ }
452
+ handleGetElementRect(command) {
453
+ if (typeof document === "undefined") {
454
+ return { id: command.id, ok: false, error: "No browser document available" };
455
+ }
456
+ const selector = command.params?.selector;
457
+ if (!selector) {
458
+ return { id: command.id, ok: false, error: "Missing selector parameter" };
459
+ }
460
+ const el = document.querySelector(selector);
461
+ if (!el) {
462
+ return { id: command.id, ok: false, error: `Element not found: ${selector}` };
463
+ }
464
+ const rect = el.getBoundingClientRect();
465
+ return {
466
+ id: command.id,
467
+ ok: true,
468
+ result: {
469
+ x: Math.round(rect.x),
470
+ y: Math.round(rect.y),
471
+ width: Math.round(rect.width),
472
+ height: Math.round(rect.height),
473
+ top: Math.round(rect.top),
474
+ left: Math.round(rect.left),
475
+ bottom: Math.round(rect.bottom),
476
+ right: Math.round(rect.right)
477
+ }
478
+ };
479
+ }
480
+ handleGetElementStyle(command) {
481
+ if (typeof window === "undefined" || typeof document === "undefined") {
482
+ return { id: command.id, ok: false, error: "No browser window available" };
483
+ }
484
+ const selector = command.params?.selector;
485
+ if (!selector) {
486
+ return { id: command.id, ok: false, error: "Missing selector parameter" };
487
+ }
488
+ const el = document.querySelector(selector);
489
+ if (!el) {
490
+ return { id: command.id, ok: false, error: `Element not found: ${selector}` };
491
+ }
492
+ const computed = window.getComputedStyle(el);
493
+ const properties = command.params?.properties || ["overflow", "overflowX", "overflowY", "width", "maxWidth", "display", "position"];
494
+ const styles = {};
495
+ for (const prop of properties) {
496
+ styles[prop] = computed.getPropertyValue(prop) || computed[prop] || "";
497
+ }
498
+ return {
499
+ id: command.id,
500
+ ok: true,
501
+ result: {
502
+ selector,
503
+ styles
504
+ }
505
+ };
506
+ }
507
+ };
508
+
509
+ // packages/client/src/config.ts
510
+ var DEFAULT_OPTIONS = {
511
+ daemonUrl: "ws://127.0.0.1:7331",
512
+ projectId: "",
513
+ maxBreadcrumbs: 50,
514
+ captureErrors: true,
515
+ captureConsole: true,
516
+ captureNetwork: true,
517
+ captureNavigation: true,
518
+ captureInteractions: true,
519
+ onErrorScreenshot: true,
520
+ notes: {
521
+ enabled: true,
522
+ shortcut: "Alt+Click",
523
+ showBadges: true,
524
+ maskSelectors: ['input[type="password"]', "[data-sensitive]"]
525
+ },
526
+ debug: false
527
+ };
528
+
529
+ // packages/client/src/interceptors/console.ts
530
+ function setupConsoleInterceptors(onConsole) {
531
+ if (typeof console === "undefined") return () => {
532
+ };
533
+ const originalError = console.error;
534
+ const originalWarn = console.warn;
535
+ function formatArgs(args) {
536
+ return args.map((arg) => {
537
+ if (arg instanceof Error) {
538
+ return arg.stack || `${arg.name}: ${arg.message}`;
539
+ }
540
+ if (typeof arg === "object" && arg !== null) {
541
+ try {
542
+ return JSON.stringify(arg);
543
+ } catch {
544
+ return String(arg);
545
+ }
546
+ }
547
+ return String(arg);
548
+ }).join(" ");
549
+ }
550
+ console.error = function(...args) {
551
+ try {
552
+ const message = formatArgs(args);
553
+ const stack = new Error().stack;
554
+ onConsole({
555
+ level: "error",
556
+ message,
557
+ stack,
558
+ timestamp: Date.now()
559
+ });
560
+ } catch {
561
+ }
562
+ return originalError.apply(console, args);
563
+ };
564
+ console.warn = function(...args) {
565
+ try {
566
+ const message = formatArgs(args);
567
+ const stack = new Error().stack;
568
+ onConsole({
569
+ level: "warn",
570
+ message,
571
+ stack,
572
+ timestamp: Date.now()
573
+ });
574
+ } catch {
575
+ }
576
+ return originalWarn.apply(console, args);
577
+ };
578
+ return () => {
579
+ console.error = originalError;
580
+ console.warn = originalWarn;
581
+ };
582
+ }
583
+
584
+ // packages/client/src/interceptors/navigation.ts
585
+ function setupNavigationInterceptors(onNavigation) {
586
+ if (typeof window === "undefined" || typeof history === "undefined") return () => {
587
+ };
588
+ let currentUrl = redactUrl(window.location.href);
589
+ onNavigation({
590
+ to: currentUrl,
591
+ type: "initial",
592
+ timestamp: Date.now()
593
+ });
594
+ const originalPushState = history.pushState;
595
+ const originalReplaceState = history.replaceState;
596
+ history.pushState = function(data, unused, url) {
597
+ const from = currentUrl;
598
+ const result = originalPushState.apply(history, [data, unused, url]);
599
+ try {
600
+ const to = redactUrl(window.location.href);
601
+ currentUrl = to;
602
+ onNavigation({
603
+ from,
604
+ to,
605
+ type: "pushState",
606
+ timestamp: Date.now()
607
+ });
608
+ } catch {
609
+ }
610
+ return result;
611
+ };
612
+ history.replaceState = function(data, unused, url) {
613
+ const from = currentUrl;
614
+ const result = originalReplaceState.apply(history, [data, unused, url]);
615
+ try {
616
+ const to = redactUrl(window.location.href);
617
+ currentUrl = to;
618
+ onNavigation({
619
+ from,
620
+ to,
621
+ type: "replaceState",
622
+ timestamp: Date.now()
623
+ });
624
+ } catch {
625
+ }
626
+ return result;
627
+ };
628
+ const onPopState = () => {
629
+ const from = currentUrl;
630
+ const to = redactUrl(window.location.href);
631
+ currentUrl = to;
632
+ onNavigation({
633
+ from,
634
+ to,
635
+ type: "popstate",
636
+ timestamp: Date.now()
637
+ });
638
+ };
639
+ const onHashChange = () => {
640
+ const from = currentUrl;
641
+ const to = redactUrl(window.location.href);
642
+ currentUrl = to;
643
+ onNavigation({
644
+ from,
645
+ to,
646
+ type: "hashchange",
647
+ timestamp: Date.now()
648
+ });
649
+ };
650
+ window.addEventListener("popstate", onPopState);
651
+ window.addEventListener("hashchange", onHashChange);
652
+ return () => {
653
+ history.pushState = originalPushState;
654
+ history.replaceState = originalReplaceState;
655
+ window.removeEventListener("popstate", onPopState);
656
+ window.removeEventListener("hashchange", onHashChange);
657
+ };
658
+ }
659
+
660
+ // packages/client/src/interceptors/network.ts
661
+ function setupNetworkInterceptors(onNetwork) {
662
+ const cleanups = [];
663
+ if (typeof window !== "undefined" && typeof window.fetch === "function") {
664
+ const originalFetch = window.fetch;
665
+ window.fetch = async function(input, init2) {
666
+ const startTime = Date.now();
667
+ let urlStr = "";
668
+ let method = "GET";
669
+ try {
670
+ if (typeof input === "string") {
671
+ urlStr = input;
672
+ } else if (input instanceof URL) {
673
+ urlStr = input.toString();
674
+ } else if (input && typeof input === "object" && "url" in input) {
675
+ urlStr = input.url;
676
+ method = input.method || "GET";
677
+ }
678
+ if (init2 && init2.method) {
679
+ method = init2.method.toUpperCase();
680
+ }
681
+ } catch {
682
+ urlStr = "unknown_url";
683
+ }
684
+ const safeUrl = redactUrl(urlStr);
685
+ try {
686
+ const response = await originalFetch.apply(window, [input, init2]);
687
+ const durationMs = Date.now() - startTime;
688
+ onNetwork({
689
+ url: safeUrl,
690
+ method,
691
+ status: response.status,
692
+ statusText: response.statusText,
693
+ durationMs,
694
+ timestamp: startTime
695
+ });
696
+ return response;
697
+ } catch (err) {
698
+ const durationMs = Date.now() - startTime;
699
+ const isAbort = err?.name === "AbortError";
700
+ onNetwork({
701
+ url: safeUrl,
702
+ method,
703
+ durationMs,
704
+ error: err?.message || "Network request failed",
705
+ aborted: isAbort,
706
+ timestamp: startTime
707
+ });
708
+ throw err;
709
+ }
710
+ };
711
+ cleanups.push(() => {
712
+ window.fetch = originalFetch;
713
+ });
714
+ }
715
+ if (typeof window !== "undefined" && typeof window.XMLHttpRequest === "function") {
716
+ const originalOpen = XMLHttpRequest.prototype.open;
717
+ const originalSend = XMLHttpRequest.prototype.send;
718
+ const originalAbort = XMLHttpRequest.prototype.abort;
719
+ const xhrMap = /* @__PURE__ */ new WeakMap();
720
+ XMLHttpRequest.prototype.open = function(...args) {
721
+ try {
722
+ const method = String(args[0] || "GET").toUpperCase();
723
+ const rawUrl = String(args[1] || "");
724
+ xhrMap.set(this, {
725
+ url: redactUrl(rawUrl),
726
+ method,
727
+ startTime: Date.now()
728
+ });
729
+ } catch {
730
+ }
731
+ return originalOpen.apply(this, args);
732
+ };
733
+ XMLHttpRequest.prototype.abort = function() {
734
+ const meta = xhrMap.get(this);
735
+ if (meta) {
736
+ meta.aborted = true;
737
+ }
738
+ return originalAbort.apply(this);
739
+ };
740
+ XMLHttpRequest.prototype.send = function(body) {
741
+ const meta = xhrMap.get(this) || { url: "unknown_url", method: "GET", startTime: Date.now() };
742
+ const onComplete = () => {
743
+ try {
744
+ const durationMs = Date.now() - meta.startTime;
745
+ onNetwork({
746
+ url: meta.url,
747
+ method: meta.method,
748
+ status: this.status,
749
+ statusText: this.statusText,
750
+ durationMs,
751
+ aborted: meta.aborted,
752
+ timestamp: meta.startTime
753
+ });
754
+ } catch {
755
+ }
756
+ };
757
+ const onError = () => {
758
+ try {
759
+ const durationMs = Date.now() - meta.startTime;
760
+ onNetwork({
761
+ url: meta.url,
762
+ method: meta.method,
763
+ status: this.status || 0,
764
+ durationMs,
765
+ error: "XHR Network Error",
766
+ aborted: meta.aborted,
767
+ timestamp: meta.startTime
768
+ });
769
+ } catch {
770
+ }
771
+ };
772
+ this.addEventListener("load", onComplete, { once: true });
773
+ this.addEventListener("error", onError, { once: true });
774
+ this.addEventListener("timeout", onError, { once: true });
775
+ return originalSend.apply(this, [body]);
776
+ };
777
+ cleanups.push(() => {
778
+ XMLHttpRequest.prototype.open = originalOpen;
779
+ XMLHttpRequest.prototype.send = originalSend;
780
+ XMLHttpRequest.prototype.abort = originalAbort;
781
+ });
782
+ }
783
+ return () => {
784
+ for (const cleanup of cleanups) {
785
+ try {
786
+ cleanup();
787
+ } catch {
788
+ }
789
+ }
790
+ };
791
+ }
792
+
793
+ // packages/client/src/interceptors/runtime.ts
794
+ function setupRuntimeInterceptors(onRuntimeError) {
795
+ if (typeof window === "undefined") return () => {
796
+ };
797
+ const errorHandler = (event) => {
798
+ try {
799
+ const errorEvent = {
800
+ message: event.message || event.error && event.error.message || "Script error",
801
+ stack: event.error && event.error.stack || void 0,
802
+ filename: event.filename || void 0,
803
+ lineno: event.lineno || void 0,
804
+ colno: event.colno || void 0,
805
+ errorType: event.error && event.error.name || "Error",
806
+ timestamp: Date.now()
807
+ };
808
+ onRuntimeError(errorEvent, event.error);
809
+ } catch {
810
+ }
811
+ };
812
+ const unhandledRejectionHandler = (event) => {
813
+ try {
814
+ const reason = event.reason;
815
+ let message = "Unhandled Promise Rejection";
816
+ let stack;
817
+ let errorType = "UnhandledRejection";
818
+ if (reason instanceof Error) {
819
+ message = reason.message;
820
+ stack = reason.stack;
821
+ errorType = reason.name || "UnhandledRejection";
822
+ } else if (typeof reason === "string") {
823
+ message = reason;
824
+ } else if (reason && typeof reason === "object") {
825
+ try {
826
+ message = JSON.stringify(reason);
827
+ } catch {
828
+ message = String(reason);
829
+ }
830
+ }
831
+ const errorEvent = {
832
+ message,
833
+ stack,
834
+ errorType,
835
+ timestamp: Date.now()
836
+ };
837
+ onRuntimeError(errorEvent, reason);
838
+ } catch {
839
+ }
840
+ };
841
+ window.addEventListener("error", errorHandler);
842
+ window.addEventListener("unhandledrejection", unhandledRejectionHandler);
843
+ return () => {
844
+ window.removeEventListener("error", errorHandler);
845
+ window.removeEventListener("unhandledrejection", unhandledRejectionHandler);
846
+ };
847
+ }
848
+
849
+ // packages/client/src/screenshot/browser-script-driver.ts
850
+ var import_snapdom = require("@zumer/snapdom");
851
+ var BrowserScriptScreenshotDriver = class {
852
+ constructor() {
853
+ __publicField(this, "name", "BrowserScriptScreenshotDriver");
854
+ }
855
+ async captureSelector(selector) {
856
+ try {
857
+ if (typeof document === "undefined") {
858
+ return { ok: false, reason: "NO_DOCUMENT_ENVIRONMENT" };
859
+ }
860
+ const el = document.querySelector(selector);
861
+ if (!el) {
862
+ return { ok: false, reason: `ELEMENT_NOT_FOUND: ${selector}` };
863
+ }
864
+ return await this.captureElement(el);
865
+ } catch (err) {
866
+ return { ok: false, reason: err?.message || "UNKNOWN_CAPTURE_ERROR" };
867
+ }
868
+ }
869
+ async captureElement(element) {
870
+ try {
871
+ if (typeof window === "undefined" || typeof document === "undefined") {
872
+ return { ok: false, reason: "NO_BROWSER_ENVIRONMENT" };
873
+ }
874
+ const rect = element.getBoundingClientRect();
875
+ const width = Math.max(Math.round(rect.width), 10);
876
+ const height = Math.max(Math.round(rect.height), 10);
877
+ const capturePromise = (async () => {
878
+ try {
879
+ let imgElement = void 0;
880
+ let format = "webp";
881
+ try {
882
+ if (typeof import_snapdom.snapdom === "function") {
883
+ const captureRes = await (0, import_snapdom.snapdom)(element, { scale: 1, embedFonts: false });
884
+ if (captureRes && typeof captureRes.toWebp === "function") {
885
+ imgElement = await captureRes.toWebp();
886
+ } else if (captureRes && typeof captureRes.toPng === "function") {
887
+ imgElement = await captureRes.toPng();
888
+ format = "png";
889
+ } else if (captureRes?.url) {
890
+ return {
891
+ ok: true,
892
+ dataUrl: captureRes.url,
893
+ format: "webp",
894
+ width,
895
+ height
896
+ };
897
+ }
898
+ } else if (import_snapdom.snapdom?.toWebp) {
899
+ imgElement = await import_snapdom.snapdom.toWebp(element, {
900
+ scale: 1,
901
+ embedFonts: false
902
+ });
903
+ } else if (import_snapdom.snapdom?.toPng) {
904
+ imgElement = await import_snapdom.snapdom.toPng(element, {
905
+ scale: 1,
906
+ embedFonts: false
907
+ });
908
+ format = "png";
909
+ }
910
+ } catch {
911
+ }
912
+ if (imgElement && imgElement.src) {
913
+ return {
914
+ ok: true,
915
+ dataUrl: imgElement.src,
916
+ format: imgElement.src.startsWith("data:image/webp") ? "webp" : format,
917
+ width: imgElement.naturalWidth || width,
918
+ height: imgElement.naturalHeight || height
919
+ };
920
+ }
921
+ } catch (snapErr) {
922
+ const errMsg = String(snapErr?.message || snapErr);
923
+ if (errMsg.includes("CORS") || errMsg.includes("SecurityError") || errMsg.includes("tainted")) {
924
+ return { ok: false, reason: "CROSS_ORIGIN_RESOURCE" };
925
+ }
926
+ }
927
+ return await this.captureWithSvgFallback(element, width, height);
928
+ })();
929
+ const timeoutPromise = new Promise((resolve) => {
930
+ setTimeout(() => resolve({ ok: false, reason: "CAPTURE_TIMEOUT" }), 3e3);
931
+ });
932
+ return await Promise.race([capturePromise, timeoutPromise]);
933
+ } catch (err) {
934
+ return { ok: false, reason: err?.message || "CAPTURE_EXCEPTION" };
935
+ }
936
+ }
937
+ async captureWithSvgFallback(element, width, height) {
938
+ try {
939
+ const clone = element.cloneNode(true);
940
+ const computed = window.getComputedStyle(element);
941
+ let cssText = "";
942
+ for (let i = 0; i < computed.length; i++) {
943
+ const prop = computed[i];
944
+ cssText += `${prop}:${computed.getPropertyValue(prop)};`;
945
+ }
946
+ clone.style.cssText = cssText;
947
+ clone.style.margin = "0";
948
+ clone.style.position = "static";
949
+ const svg = `
950
+ <svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">
951
+ <foreignObject width="100%" height="100%">
952
+ <div xmlns="http://www.w3.org/1999/xhtml" style="width:${width}px;height:${height}px;">
953
+ ${clone.outerHTML}
954
+ </div>
955
+ </foreignObject>
956
+ </svg>
957
+ `.trim();
958
+ const svgBlob = new Blob([svg], { type: "image/svg+xml;charset=utf-8" });
959
+ const url = URL.createObjectURL(svgBlob);
960
+ return await new Promise((resolve) => {
961
+ const img = new Image();
962
+ img.onload = () => {
963
+ try {
964
+ const canvas = document.createElement("canvas");
965
+ canvas.width = width;
966
+ canvas.height = height;
967
+ const ctx = canvas.getContext("2d");
968
+ if (!ctx) {
969
+ URL.revokeObjectURL(url);
970
+ resolve({ ok: false, reason: "CANVAS_CONTEXT_UNAVAILABLE" });
971
+ return;
972
+ }
973
+ ctx.drawImage(img, 0, 0);
974
+ URL.revokeObjectURL(url);
975
+ let dataUrl;
976
+ try {
977
+ dataUrl = canvas.toDataURL("image/webp", 0.85);
978
+ } catch {
979
+ dataUrl = canvas.toDataURL("image/png");
980
+ }
981
+ resolve({
982
+ ok: true,
983
+ dataUrl,
984
+ format: dataUrl.startsWith("data:image/webp") ? "webp" : "png",
985
+ width,
986
+ height
987
+ });
988
+ } catch (e) {
989
+ URL.revokeObjectURL(url);
990
+ if (e?.name === "SecurityError" || String(e).includes("tainted")) {
991
+ resolve({ ok: false, reason: "CROSS_ORIGIN_RESOURCE" });
992
+ } else {
993
+ resolve({ ok: false, reason: e?.message || "CANVAS_EXPORT_FAILED" });
994
+ }
995
+ }
996
+ };
997
+ img.onerror = () => {
998
+ URL.revokeObjectURL(url);
999
+ resolve({ ok: false, reason: "IMAGE_LOAD_FAILED" });
1000
+ };
1001
+ img.src = url;
1002
+ });
1003
+ } catch (e) {
1004
+ return { ok: false, reason: e?.message || "FALLBACK_CAPTURE_FAILED" };
1005
+ }
1006
+ }
1007
+ };
1008
+
1009
+ // packages/client/src/transport/websocket.ts
1010
+ var WebSocketTransport = class {
1011
+ constructor(options) {
1012
+ __publicField(this, "wsUrl");
1013
+ __publicField(this, "projectId");
1014
+ __publicField(this, "debug");
1015
+ __publicField(this, "ws", null);
1016
+ __publicField(this, "sessionId", null);
1017
+ __publicField(this, "queue", []);
1018
+ __publicField(this, "maxQueueSize", 50);
1019
+ __publicField(this, "reconnectAttempts", 0);
1020
+ __publicField(this, "maxReconnectDelay", 1e4);
1021
+ __publicField(this, "reconnectTimer", null);
1022
+ __publicField(this, "commandHandler");
1023
+ __publicField(this, "isDestroyed", false);
1024
+ this.wsUrl = options.url || "ws://127.0.0.1:7331";
1025
+ this.projectId = options.projectId;
1026
+ this.debug = !!options.debug;
1027
+ }
1028
+ setCommandHandler(handler) {
1029
+ this.commandHandler = handler;
1030
+ }
1031
+ getSessionId() {
1032
+ return this.sessionId;
1033
+ }
1034
+ isConnected() {
1035
+ return this.ws !== null && this.ws.readyState === (typeof WebSocket !== "undefined" ? WebSocket.OPEN : 1);
1036
+ }
1037
+ connect() {
1038
+ if (this.isDestroyed || typeof WebSocket === "undefined") return;
1039
+ if (this.ws && (this.ws.readyState === WebSocket.CONNECTING || this.ws.readyState === WebSocket.OPEN)) {
1040
+ return;
1041
+ }
1042
+ try {
1043
+ this.ws = new WebSocket(this.wsUrl);
1044
+ this.ws.onopen = () => {
1045
+ this.reconnectAttempts = 0;
1046
+ this.sendHello();
1047
+ this.flushQueue();
1048
+ };
1049
+ this.ws.onmessage = async (event) => {
1050
+ try {
1051
+ const raw = typeof event.data === "string" ? event.data : "";
1052
+ if (!raw) return;
1053
+ const msg = JSON.parse(raw);
1054
+ if (msg.type === "hello_ack") {
1055
+ this.sessionId = msg.sessionId;
1056
+ if (this.debug) {
1057
+ console.log("[BrowserTrack] Connected, session ID:", this.sessionId);
1058
+ }
1059
+ return;
1060
+ }
1061
+ if (msg.type === "command" && this.commandHandler) {
1062
+ const command = msg.command;
1063
+ const res = await this.commandHandler.executeCommand(command);
1064
+ this.send({
1065
+ type: "command_response",
1066
+ sessionId: this.sessionId,
1067
+ response: res
1068
+ });
1069
+ }
1070
+ } catch {
1071
+ }
1072
+ };
1073
+ this.ws.onerror = () => {
1074
+ };
1075
+ this.ws.onclose = () => {
1076
+ this.ws = null;
1077
+ this.scheduleReconnect();
1078
+ };
1079
+ } catch {
1080
+ this.scheduleReconnect();
1081
+ }
1082
+ }
1083
+ send(payload) {
1084
+ const raw = JSON.stringify(payload);
1085
+ if (this.isConnected()) {
1086
+ try {
1087
+ this.ws.send(raw);
1088
+ } catch {
1089
+ this.enqueue(raw);
1090
+ }
1091
+ } else {
1092
+ this.enqueue(raw);
1093
+ }
1094
+ }
1095
+ enqueue(raw) {
1096
+ if (this.queue.length >= this.maxQueueSize) {
1097
+ this.queue.shift();
1098
+ }
1099
+ this.queue.push(raw);
1100
+ }
1101
+ flushQueue() {
1102
+ if (!this.isConnected()) return;
1103
+ while (this.queue.length > 0) {
1104
+ const item = this.queue.shift();
1105
+ if (item) {
1106
+ try {
1107
+ this.ws.send(item);
1108
+ } catch {
1109
+ this.queue.unshift(item);
1110
+ break;
1111
+ }
1112
+ }
1113
+ }
1114
+ }
1115
+ sendHello() {
1116
+ if (typeof window === "undefined") return;
1117
+ const hello = {
1118
+ type: "hello",
1119
+ origin: window.location.origin,
1120
+ url: window.location.href,
1121
+ title: document.title,
1122
+ userAgent: navigator.userAgent,
1123
+ timestamp: Date.now(),
1124
+ projectId: this.projectId
1125
+ };
1126
+ try {
1127
+ this.ws.send(JSON.stringify(hello));
1128
+ } catch {
1129
+ }
1130
+ }
1131
+ scheduleReconnect() {
1132
+ if (this.isDestroyed || this.reconnectTimer) return;
1133
+ this.reconnectAttempts++;
1134
+ const delay = Math.min(1e3 * Math.pow(1.5, this.reconnectAttempts), this.maxReconnectDelay);
1135
+ this.reconnectTimer = setTimeout(() => {
1136
+ this.reconnectTimer = null;
1137
+ this.connect();
1138
+ }, delay);
1139
+ }
1140
+ destroy() {
1141
+ this.isDestroyed = true;
1142
+ if (this.reconnectTimer) {
1143
+ clearTimeout(this.reconnectTimer);
1144
+ this.reconnectTimer = null;
1145
+ }
1146
+ if (this.ws) {
1147
+ try {
1148
+ this.ws.close();
1149
+ } catch {
1150
+ }
1151
+ this.ws = null;
1152
+ }
1153
+ this.queue = [];
1154
+ }
1155
+ };
1156
+
1157
+ // packages/client/src/notes/inspector.ts
1158
+ var NoteInspector = class {
1159
+ constructor(transport, screenshotDriver, options = {}) {
1160
+ __publicField(this, "transport");
1161
+ __publicField(this, "screenshotDriver");
1162
+ __publicField(this, "options");
1163
+ __publicField(this, "container", null);
1164
+ __publicField(this, "shadowRoot", null);
1165
+ __publicField(this, "highlightOverlay", null);
1166
+ __publicField(this, "regionOverlay", null);
1167
+ __publicField(this, "regionBox", null);
1168
+ __publicField(this, "toolbarElement", null);
1169
+ __publicField(this, "modalOverlay", null);
1170
+ __publicField(this, "activeMode", "idle");
1171
+ __publicField(this, "hoveredElement", null);
1172
+ __publicField(this, "selectedElement", null);
1173
+ __publicField(this, "selectedRegion", null);
1174
+ __publicField(this, "isDraggingRegion", false);
1175
+ __publicField(this, "dragStartX", 0);
1176
+ __publicField(this, "dragStartY", 0);
1177
+ __publicField(this, "cleanups", []);
1178
+ this.transport = transport;
1179
+ this.screenshotDriver = screenshotDriver;
1180
+ this.options = {
1181
+ shortcut: "Alt+Click",
1182
+ maskSelectors: ['input[type="password"]', "[data-sensitive]"],
1183
+ showToolbar: true,
1184
+ ...options
1185
+ };
1186
+ }
1187
+ init() {
1188
+ if (typeof window === "undefined" || typeof document === "undefined") return;
1189
+ if (document.readyState === "loading") {
1190
+ document.addEventListener("DOMContentLoaded", () => {
1191
+ this.ensureContainer();
1192
+ });
1193
+ } else {
1194
+ this.ensureContainer();
1195
+ }
1196
+ this.setupListeners();
1197
+ }
1198
+ setMode(mode) {
1199
+ this.activeMode = mode;
1200
+ this.updateToolbarState();
1201
+ if (mode === "element") {
1202
+ this.hideRegionOverlay();
1203
+ } else if (mode === "region") {
1204
+ this.hideHighlight();
1205
+ this.showRegionOverlay();
1206
+ } else if (mode === "page") {
1207
+ this.hideRegionOverlay();
1208
+ this.hideHighlight();
1209
+ this.openNoteEditor(document.body, "page");
1210
+ } else {
1211
+ this.hideRegionOverlay();
1212
+ this.hideHighlight();
1213
+ }
1214
+ }
1215
+ ensureContainer() {
1216
+ if (typeof document === "undefined") return null;
1217
+ if (!this.container) {
1218
+ this.container = document.createElement("div");
1219
+ this.container.id = "browsertrack-inspector-host";
1220
+ this.container.style.cssText = "all: initial; position: fixed; top: 0; left: 0; width: 0; height: 0; z-index: 2147483647; pointer-events: none;";
1221
+ this.shadowRoot = this.container.attachShadow({ mode: "open" });
1222
+ const style = document.createElement("style");
1223
+ style.textContent = `
1224
+ :host {
1225
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
1226
+ color-scheme: dark;
1227
+ -webkit-font-smoothing: antialiased;
1228
+ }
1229
+
1230
+ /* 1. Element Highlight Overlay */
1231
+ .bt-highlight {
1232
+ position: fixed;
1233
+ border: 2px solid #3b82f6;
1234
+ background: rgba(59, 130, 246, 0.15);
1235
+ border-radius: 4px;
1236
+ pointer-events: none;
1237
+ transition: all 0.05s ease-out;
1238
+ z-index: 2147483640;
1239
+ box-sizing: border-box;
1240
+ }
1241
+
1242
+ .bt-badge {
1243
+ position: absolute;
1244
+ top: -24px;
1245
+ left: 0;
1246
+ background: #1e293b;
1247
+ color: #38bdf8;
1248
+ border: 1px solid #3b82f6;
1249
+ font-size: 11px;
1250
+ font-weight: 600;
1251
+ padding: 2px 6px;
1252
+ border-radius: 3px;
1253
+ white-space: nowrap;
1254
+ pointer-events: none;
1255
+ box-shadow: 0 2px 4px rgba(0,0,0,0.3);
1256
+ }
1257
+
1258
+ /* 2. Region Selection Overlay */
1259
+ .bt-region-overlay {
1260
+ position: fixed;
1261
+ inset: 0;
1262
+ width: 100vw;
1263
+ height: 100vh;
1264
+ cursor: crosshair;
1265
+ z-index: 2147483642;
1266
+ pointer-events: auto;
1267
+ background: rgba(15, 23, 42, 0.2);
1268
+ }
1269
+
1270
+ .bt-region-box {
1271
+ position: fixed;
1272
+ border: 2px dashed #38bdf8;
1273
+ background: rgba(56, 189, 248, 0.2);
1274
+ box-sizing: border-box;
1275
+ pointer-events: none;
1276
+ z-index: 2147483643;
1277
+ }
1278
+
1279
+ .bt-region-badge {
1280
+ position: absolute;
1281
+ top: -24px;
1282
+ left: 0;
1283
+ background: #0f172a;
1284
+ color: #38bdf8;
1285
+ border: 1px solid #0284c7;
1286
+ font-size: 11px;
1287
+ font-weight: 600;
1288
+ padding: 2px 6px;
1289
+ border-radius: 3px;
1290
+ white-space: nowrap;
1291
+ pointer-events: none;
1292
+ }
1293
+
1294
+ /* 3. Floating Quick Toolbar */
1295
+ .bt-toolbar {
1296
+ position: fixed;
1297
+ bottom: 20px;
1298
+ right: 20px;
1299
+ background: #1e293b;
1300
+ border: 1px solid #334155;
1301
+ border-radius: 30px;
1302
+ padding: 4px 6px;
1303
+ display: flex;
1304
+ align-items: center;
1305
+ gap: 4px;
1306
+ box-shadow: 0 10px 15px -3px rgba(0,0,0,0.5), 0 4px 6px -4px rgba(0,0,0,0.5);
1307
+ pointer-events: auto;
1308
+ z-index: 2147483646;
1309
+ user-select: none;
1310
+ transition: all 0.2s ease;
1311
+ }
1312
+
1313
+ .bt-toolbar-btn {
1314
+ background: transparent;
1315
+ border: none;
1316
+ color: #94a3b8;
1317
+ font-size: 12px;
1318
+ font-weight: 500;
1319
+ padding: 6px 10px;
1320
+ border-radius: 20px;
1321
+ cursor: pointer;
1322
+ display: flex;
1323
+ align-items: center;
1324
+ gap: 5px;
1325
+ transition: all 0.15s ease;
1326
+ }
1327
+
1328
+ .bt-toolbar-btn:hover {
1329
+ background: #334155;
1330
+ color: #f8fafc;
1331
+ }
1332
+
1333
+ .bt-toolbar-btn.active {
1334
+ background: #2563eb;
1335
+ color: #ffffff;
1336
+ font-weight: 600;
1337
+ }
1338
+
1339
+ .bt-toolbar-divider {
1340
+ width: 1px;
1341
+ height: 16px;
1342
+ background: #334155;
1343
+ }
1344
+
1345
+ /* 4. Modal Backdrop & Editor */
1346
+ .bt-modal-backdrop {
1347
+ position: fixed;
1348
+ top: 0;
1349
+ left: 0;
1350
+ width: 100vw;
1351
+ height: 100vh;
1352
+ background: rgba(15, 23, 42, 0.75);
1353
+ backdrop-filter: blur(4px);
1354
+ display: flex;
1355
+ align-items: center;
1356
+ justify-content: center;
1357
+ pointer-events: auto;
1358
+ z-index: 2147483647;
1359
+ opacity: 1;
1360
+ box-sizing: border-box;
1361
+ }
1362
+
1363
+ .bt-modal {
1364
+ background: #1e293b;
1365
+ color: #f8fafc;
1366
+ border: 1px solid #334155;
1367
+ border-radius: 12px;
1368
+ padding: 18px;
1369
+ width: 420px;
1370
+ max-width: 90vw;
1371
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.7), 0 8px 10px -6px rgba(0, 0, 0, 0.7);
1372
+ display: flex;
1373
+ flex-direction: column;
1374
+ gap: 12px;
1375
+ box-sizing: border-box;
1376
+ }
1377
+
1378
+ .bt-modal-header {
1379
+ display: flex;
1380
+ justify-content: space-between;
1381
+ align-items: center;
1382
+ }
1383
+
1384
+ .bt-modal-title {
1385
+ font-size: 14px;
1386
+ font-weight: 600;
1387
+ display: flex;
1388
+ align-items: center;
1389
+ gap: 6px;
1390
+ color: #f8fafc;
1391
+ }
1392
+
1393
+ .bt-mode-badge {
1394
+ font-size: 10px;
1395
+ font-weight: 700;
1396
+ text-transform: uppercase;
1397
+ background: rgba(59, 130, 246, 0.2);
1398
+ color: #60a5fa;
1399
+ border: 1px solid #3b82f6;
1400
+ padding: 2px 6px;
1401
+ border-radius: 4px;
1402
+ }
1403
+
1404
+ .bt-close-btn {
1405
+ background: transparent;
1406
+ border: none;
1407
+ color: #94a3b8;
1408
+ font-size: 16px;
1409
+ cursor: pointer;
1410
+ padding: 2px 6px;
1411
+ border-radius: 4px;
1412
+ }
1413
+ .bt-close-btn:hover {
1414
+ background: #334155;
1415
+ color: #ffffff;
1416
+ }
1417
+
1418
+ .bt-target-pill {
1419
+ background: #0f172a;
1420
+ color: #94a3b8;
1421
+ border: 1px solid #334155;
1422
+ border-radius: 6px;
1423
+ padding: 6px 10px;
1424
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
1425
+ font-size: 11px;
1426
+ overflow: hidden;
1427
+ text-overflow: ellipsis;
1428
+ white-space: nowrap;
1429
+ }
1430
+
1431
+ .bt-textarea {
1432
+ background: #0f172a;
1433
+ color: #f8fafc;
1434
+ border: 1px solid #334155;
1435
+ border-radius: 6px;
1436
+ padding: 10px;
1437
+ font-size: 13px;
1438
+ resize: vertical;
1439
+ min-height: 85px;
1440
+ outline: none;
1441
+ box-sizing: border-box;
1442
+ width: 100%;
1443
+ font-family: inherit;
1444
+ }
1445
+
1446
+ .bt-textarea:focus {
1447
+ border-color: #3b82f6;
1448
+ box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.25);
1449
+ }
1450
+
1451
+ .bt-modal-actions {
1452
+ display: flex;
1453
+ justify-content: flex-end;
1454
+ gap: 8px;
1455
+ }
1456
+
1457
+ .bt-btn {
1458
+ padding: 8px 14px;
1459
+ border-radius: 6px;
1460
+ font-size: 12px;
1461
+ font-weight: 600;
1462
+ cursor: pointer;
1463
+ border: 1px solid transparent;
1464
+ transition: all 0.1s ease;
1465
+ font-family: inherit;
1466
+ }
1467
+
1468
+ .bt-btn-cancel {
1469
+ background: transparent;
1470
+ color: #94a3b8;
1471
+ }
1472
+ .bt-btn-cancel:hover {
1473
+ background: #334155;
1474
+ color: #f8fafc;
1475
+ }
1476
+
1477
+ .bt-btn-save {
1478
+ background: #2563eb;
1479
+ color: #ffffff;
1480
+ }
1481
+ .bt-btn-save:hover {
1482
+ background: #1d4ed8;
1483
+ }
1484
+ .bt-btn-save:disabled {
1485
+ opacity: 0.6;
1486
+ cursor: not-allowed;
1487
+ }
1488
+ `;
1489
+ this.shadowRoot.appendChild(style);
1490
+ this.highlightOverlay = document.createElement("div");
1491
+ this.highlightOverlay.className = "bt-highlight";
1492
+ this.highlightOverlay.style.display = "none";
1493
+ this.shadowRoot.appendChild(this.highlightOverlay);
1494
+ if (this.options.showToolbar) {
1495
+ this.createToolbar();
1496
+ }
1497
+ }
1498
+ if (this.container && !this.container.isConnected) {
1499
+ const mountParent = document.body || document.documentElement;
1500
+ if (mountParent) {
1501
+ mountParent.appendChild(this.container);
1502
+ }
1503
+ }
1504
+ return this.shadowRoot;
1505
+ }
1506
+ createToolbar() {
1507
+ if (!this.shadowRoot || this.toolbarElement) return;
1508
+ this.toolbarElement = document.createElement("div");
1509
+ this.toolbarElement.className = "bt-toolbar";
1510
+ this.toolbarElement.innerHTML = `
1511
+ <button class="bt-toolbar-btn" id="bt-mode-element" title="Inspect element (or hold Alt+Click)">
1512
+ <span>\u{1F3AF}</span> Element
1513
+ </button>
1514
+ <button class="bt-toolbar-btn" id="bt-mode-region" title="Drag to select screen region">
1515
+ <span>\u{1F4D0}</span> Region
1516
+ </button>
1517
+ <button class="bt-toolbar-btn" id="bt-mode-page" title="Leave full-page note">
1518
+ <span>\u{1F4C4}</span> Page
1519
+ </button>
1520
+ `;
1521
+ const btnElement = this.toolbarElement.querySelector("#bt-mode-element");
1522
+ const btnRegion = this.toolbarElement.querySelector("#bt-mode-region");
1523
+ const btnPage = this.toolbarElement.querySelector("#bt-mode-page");
1524
+ btnElement.onclick = () => {
1525
+ this.setMode(this.activeMode === "element" ? "idle" : "element");
1526
+ };
1527
+ btnRegion.onclick = () => {
1528
+ this.setMode(this.activeMode === "region" ? "idle" : "region");
1529
+ };
1530
+ btnPage.onclick = () => {
1531
+ this.setMode("page");
1532
+ };
1533
+ this.shadowRoot.appendChild(this.toolbarElement);
1534
+ }
1535
+ updateToolbarState() {
1536
+ if (!this.toolbarElement) return;
1537
+ const btnElement = this.toolbarElement.querySelector("#bt-mode-element");
1538
+ const btnRegion = this.toolbarElement.querySelector("#bt-mode-region");
1539
+ const btnPage = this.toolbarElement.querySelector("#bt-mode-page");
1540
+ btnElement?.classList.toggle("active", this.activeMode === "element");
1541
+ btnRegion?.classList.toggle("active", this.activeMode === "region");
1542
+ btnPage?.classList.toggle("active", this.activeMode === "page");
1543
+ }
1544
+ setupListeners() {
1545
+ const onMouseMove = (e) => {
1546
+ if (this.modalOverlay || this.isDraggingRegion || this.activeMode === "region") return;
1547
+ if (e.altKey || this.activeMode === "element") {
1548
+ const target = document.elementFromPoint(e.clientX, e.clientY);
1549
+ if (target && target !== this.container && !this.container?.contains(target)) {
1550
+ this.hoveredElement = target;
1551
+ this.updateHighlight(target);
1552
+ return;
1553
+ }
1554
+ }
1555
+ if (!e.altKey && this.activeMode !== "element") {
1556
+ this.hideHighlight();
1557
+ }
1558
+ };
1559
+ const onClick = (e) => {
1560
+ if (this.modalOverlay || this.activeMode === "region") return;
1561
+ if (e.altKey || this.activeMode === "element") {
1562
+ e.preventDefault();
1563
+ e.stopPropagation();
1564
+ const target = this.hoveredElement || document.elementFromPoint(e.clientX, e.clientY);
1565
+ if (target && target !== this.container && !this.container?.contains(target)) {
1566
+ this.selectedElement = target;
1567
+ this.openNoteEditor(target, "element");
1568
+ if (this.activeMode === "element") {
1569
+ this.setMode("idle");
1570
+ }
1571
+ }
1572
+ }
1573
+ };
1574
+ const onKeyUp = (e) => {
1575
+ if (e.key === "Alt" && !this.modalOverlay && this.activeMode !== "element") {
1576
+ this.hideHighlight();
1577
+ }
1578
+ };
1579
+ window.addEventListener("mousemove", onMouseMove, { capture: true, passive: true });
1580
+ window.addEventListener("click", onClick, { capture: true });
1581
+ window.addEventListener("keyup", onKeyUp, { capture: true });
1582
+ this.cleanups.push(() => {
1583
+ window.removeEventListener("mousemove", onMouseMove, { capture: true });
1584
+ window.removeEventListener("click", onClick, { capture: true });
1585
+ window.removeEventListener("keyup", onKeyUp, { capture: true });
1586
+ });
1587
+ }
1588
+ showRegionOverlay() {
1589
+ const root = this.ensureContainer();
1590
+ if (!root) return;
1591
+ if (!this.regionOverlay) {
1592
+ this.regionOverlay = document.createElement("div");
1593
+ this.regionOverlay.className = "bt-region-overlay";
1594
+ this.regionBox = document.createElement("div");
1595
+ this.regionBox.className = "bt-region-box";
1596
+ this.regionBox.style.display = "none";
1597
+ this.regionOverlay.appendChild(this.regionBox);
1598
+ this.regionOverlay.onmousedown = (e) => {
1599
+ this.isDraggingRegion = true;
1600
+ this.dragStartX = e.clientX;
1601
+ this.dragStartY = e.clientY;
1602
+ if (this.regionBox) {
1603
+ this.regionBox.style.display = "block";
1604
+ this.regionBox.style.left = `${e.clientX}px`;
1605
+ this.regionBox.style.top = `${e.clientY}px`;
1606
+ this.regionBox.style.width = "0px";
1607
+ this.regionBox.style.height = "0px";
1608
+ }
1609
+ };
1610
+ this.regionOverlay.onmousemove = (e) => {
1611
+ if (!this.isDraggingRegion || !this.regionBox) return;
1612
+ const currentX = e.clientX;
1613
+ const currentY = e.clientY;
1614
+ const left = Math.min(this.dragStartX, currentX);
1615
+ const top = Math.min(this.dragStartY, currentY);
1616
+ const width = Math.abs(currentX - this.dragStartX);
1617
+ const height = Math.abs(currentY - this.dragStartY);
1618
+ this.regionBox.style.left = `${left}px`;
1619
+ this.regionBox.style.top = `${top}px`;
1620
+ this.regionBox.style.width = `${width}px`;
1621
+ this.regionBox.style.height = `${height}px`;
1622
+ let badge = this.regionBox.querySelector(".bt-region-badge");
1623
+ if (!badge) {
1624
+ badge = document.createElement("div");
1625
+ badge.className = "bt-region-badge";
1626
+ this.regionBox.appendChild(badge);
1627
+ }
1628
+ badge.textContent = `Region: ${Math.round(width)} \xD7 ${Math.round(height)} px`;
1629
+ };
1630
+ this.regionOverlay.onmouseup = (e) => {
1631
+ if (!this.isDraggingRegion) return;
1632
+ this.isDraggingRegion = false;
1633
+ const currentX = e.clientX;
1634
+ const currentY = e.clientY;
1635
+ const left = Math.min(this.dragStartX, currentX);
1636
+ const top = Math.min(this.dragStartY, currentY);
1637
+ const width = Math.abs(currentX - this.dragStartX);
1638
+ const height = Math.abs(currentY - this.dragStartY);
1639
+ if (width > 20 && height > 20) {
1640
+ this.selectedRegion = {
1641
+ x: Math.round(left),
1642
+ y: Math.round(top),
1643
+ width: Math.round(width),
1644
+ height: Math.round(height)
1645
+ };
1646
+ this.hideRegionOverlay();
1647
+ this.setMode("idle");
1648
+ this.openRegionNoteEditor(this.selectedRegion);
1649
+ } else {
1650
+ this.hideRegionOverlay();
1651
+ this.setMode("idle");
1652
+ }
1653
+ };
1654
+ }
1655
+ root.appendChild(this.regionOverlay);
1656
+ }
1657
+ hideRegionOverlay() {
1658
+ if (this.regionOverlay && this.regionOverlay.parentElement) {
1659
+ this.regionOverlay.parentElement.removeChild(this.regionOverlay);
1660
+ }
1661
+ if (this.regionBox) {
1662
+ this.regionBox.style.display = "none";
1663
+ }
1664
+ }
1665
+ updateHighlight(el) {
1666
+ const root = this.ensureContainer();
1667
+ if (!root || !this.highlightOverlay) return;
1668
+ const rect = el.getBoundingClientRect();
1669
+ this.highlightOverlay.style.display = "block";
1670
+ this.highlightOverlay.style.top = `${rect.top}px`;
1671
+ this.highlightOverlay.style.left = `${rect.left}px`;
1672
+ this.highlightOverlay.style.width = `${rect.width}px`;
1673
+ this.highlightOverlay.style.height = `${rect.height}px`;
1674
+ const selector = getSemanticSelector(el);
1675
+ const badgeText = `${selector} \xB7 ${Math.round(rect.width)}\xD7${Math.round(rect.height)}`;
1676
+ let badge = this.highlightOverlay.querySelector(".bt-badge");
1677
+ if (!badge) {
1678
+ badge = document.createElement("div");
1679
+ badge.className = "bt-badge";
1680
+ this.highlightOverlay.appendChild(badge);
1681
+ }
1682
+ badge.textContent = badgeText;
1683
+ }
1684
+ hideHighlight() {
1685
+ if (this.highlightOverlay) {
1686
+ this.highlightOverlay.style.display = "none";
1687
+ }
1688
+ }
1689
+ openNoteEditor(el, noteType = "element") {
1690
+ const root = this.ensureContainer();
1691
+ if (!root) return;
1692
+ const targetEl = el || document.body;
1693
+ this.selectedElement = targetEl;
1694
+ if (noteType === "element") {
1695
+ this.updateHighlight(targetEl);
1696
+ }
1697
+ this.renderNoteModal({
1698
+ title: "\u{1F4DD} Add Visual Note",
1699
+ modeBadge: noteType.toUpperCase(),
1700
+ pillText: noteType === "page" ? `Page Viewport: ${window.innerWidth} \xD7 ${window.innerHeight} px` : `${getSemanticSelector(targetEl)} (${Math.round(targetEl.getBoundingClientRect().width)}\xD7${Math.round(targetEl.getBoundingClientRect().height)}) \xB7 Viewport: ${window.innerWidth}\xD7${window.innerHeight}`,
1701
+ onSave: async (message) => {
1702
+ await this.saveVisualNote(targetEl, message, noteType);
1703
+ }
1704
+ });
1705
+ }
1706
+ openRegionNoteEditor(region) {
1707
+ this.renderNoteModal({
1708
+ title: "\u{1F4DD} Add Region Note",
1709
+ modeBadge: "REGION",
1710
+ pillText: `Selected Area: x:${region.x}, y:${region.y} (${region.width} \xD7 ${region.height} px)`,
1711
+ onSave: async (message) => {
1712
+ await this.saveRegionVisualNote(region, message);
1713
+ }
1714
+ });
1715
+ }
1716
+ renderNoteModal(options) {
1717
+ const root = this.ensureContainer();
1718
+ if (!root) return;
1719
+ if (this.modalOverlay && this.modalOverlay.parentElement) {
1720
+ this.modalOverlay.parentElement.removeChild(this.modalOverlay);
1721
+ this.modalOverlay = null;
1722
+ }
1723
+ const backdrop = document.createElement("div");
1724
+ backdrop.className = "bt-modal-backdrop";
1725
+ this.modalOverlay = backdrop;
1726
+ backdrop.innerHTML = `
1727
+ <div class="bt-modal">
1728
+ <div class="bt-modal-header">
1729
+ <div class="bt-modal-title">
1730
+ <span>${options.title}</span>
1731
+ <span class="bt-mode-badge">${options.modeBadge}</span>
1732
+ </div>
1733
+ <button class="bt-close-btn" id="btn-close" title="Close">\u2715</button>
1734
+ </div>
1735
+ <div class="bt-target-pill" title="${options.pillText}">
1736
+ \u{1F3AF} ${options.pillText}
1737
+ </div>
1738
+ <textarea class="bt-textarea" placeholder="Describe the layout issue, styling bug, or note for AI agent..." autofocus></textarea>
1739
+ <div class="bt-modal-actions">
1740
+ <button class="bt-btn bt-btn-cancel" id="btn-cancel">Cancel</button>
1741
+ <button class="bt-btn bt-btn-save" id="btn-save">Save Note</button>
1742
+ </div>
1743
+ </div>
1744
+ `;
1745
+ const textarea = backdrop.querySelector("textarea");
1746
+ const btnClose = backdrop.querySelector("#btn-close");
1747
+ const btnCancel = backdrop.querySelector("#btn-cancel");
1748
+ const btnSave = backdrop.querySelector("#btn-save");
1749
+ const closeModal = () => {
1750
+ if (this.modalOverlay) {
1751
+ root.removeChild(this.modalOverlay);
1752
+ this.modalOverlay = null;
1753
+ this.hideHighlight();
1754
+ this.hideRegionOverlay();
1755
+ }
1756
+ };
1757
+ btnClose.onclick = closeModal;
1758
+ btnCancel.onclick = closeModal;
1759
+ backdrop.onclick = (e) => {
1760
+ if (e.target === backdrop) {
1761
+ closeModal();
1762
+ }
1763
+ };
1764
+ const submitNote = async () => {
1765
+ const message = textarea.value.trim();
1766
+ if (!message) return;
1767
+ btnSave.textContent = "Saving...";
1768
+ btnSave.disabled = true;
1769
+ try {
1770
+ await options.onSave(message);
1771
+ } finally {
1772
+ closeModal();
1773
+ }
1774
+ };
1775
+ btnSave.onclick = submitNote;
1776
+ textarea.onkeydown = (e) => {
1777
+ if (e.key === "Enter" && (e.metaKey || e.ctrlKey || !e.shiftKey)) {
1778
+ e.preventDefault();
1779
+ submitNote();
1780
+ }
1781
+ if (e.key === "Escape") {
1782
+ closeModal();
1783
+ }
1784
+ };
1785
+ root.appendChild(backdrop);
1786
+ setTimeout(() => textarea?.focus(), 50);
1787
+ }
1788
+ async saveVisualNote(targetEl, message, noteType = "element") {
1789
+ const rect = targetEl.getBoundingClientRect();
1790
+ const selector = noteType === "page" ? "body" : getSemanticSelector(targetEl);
1791
+ let screenshotDataUrl;
1792
+ try {
1793
+ const snap = await this.screenshotDriver.captureElement(targetEl);
1794
+ if (snap.ok) {
1795
+ screenshotDataUrl = snap.dataUrl;
1796
+ }
1797
+ } catch {
1798
+ }
1799
+ const elementContext = this.extractElementContext(targetEl);
1800
+ const target = {
1801
+ selector,
1802
+ boundingRect: {
1803
+ x: Math.round(rect.x),
1804
+ y: Math.round(rect.y),
1805
+ width: Math.round(rect.width),
1806
+ height: Math.round(rect.height),
1807
+ top: Math.round(rect.top),
1808
+ left: Math.round(rect.left),
1809
+ bottom: Math.round(rect.bottom),
1810
+ right: Math.round(rect.right)
1811
+ },
1812
+ visible: rect.width > 0 && rect.height > 0,
1813
+ confidence: selector.startsWith("[data-test") ? "high" : selector.startsWith("#") ? "medium" : "low"
1814
+ };
1815
+ const notePayload = {
1816
+ type: "create_note",
1817
+ sessionId: this.transport.getSessionId() || "",
1818
+ noteType,
1819
+ message,
1820
+ route: window.location.pathname + window.location.search,
1821
+ url: window.location.href,
1822
+ viewport: {
1823
+ width: window.innerWidth,
1824
+ height: window.innerHeight,
1825
+ devicePixelRatio: window.devicePixelRatio || 1
1826
+ },
1827
+ scroll: {
1828
+ scrollX: window.scrollX,
1829
+ scrollY: window.scrollY
1830
+ },
1831
+ target,
1832
+ elementContext,
1833
+ screenshot: screenshotDataUrl,
1834
+ timestamp: Date.now()
1835
+ };
1836
+ this.transport.send(notePayload);
1837
+ if (this.options.onNoteCreated) {
1838
+ this.options.onNoteCreated(notePayload);
1839
+ }
1840
+ }
1841
+ async saveRegionVisualNote(region, message) {
1842
+ let screenshotDataUrl;
1843
+ try {
1844
+ const snap = await this.screenshotDriver.captureElement(document.body || document.documentElement);
1845
+ if (snap.ok && snap.dataUrl) {
1846
+ screenshotDataUrl = await this.cropDataUrl(snap.dataUrl, region);
1847
+ }
1848
+ } catch {
1849
+ }
1850
+ const notePayload = {
1851
+ type: "create_note",
1852
+ sessionId: this.transport.getSessionId() || "",
1853
+ noteType: "region",
1854
+ message,
1855
+ route: window.location.pathname + window.location.search,
1856
+ url: window.location.href,
1857
+ viewport: {
1858
+ width: window.innerWidth,
1859
+ height: window.innerHeight,
1860
+ devicePixelRatio: window.devicePixelRatio || 1
1861
+ },
1862
+ scroll: {
1863
+ scrollX: window.scrollX,
1864
+ scrollY: window.scrollY
1865
+ },
1866
+ region,
1867
+ screenshot: screenshotDataUrl,
1868
+ timestamp: Date.now()
1869
+ };
1870
+ this.transport.send(notePayload);
1871
+ if (this.options.onNoteCreated) {
1872
+ this.options.onNoteCreated(notePayload);
1873
+ }
1874
+ }
1875
+ async cropDataUrl(dataUrl, region) {
1876
+ return new Promise((resolve) => {
1877
+ const img = new Image();
1878
+ img.onload = () => {
1879
+ try {
1880
+ const canvas = document.createElement("canvas");
1881
+ canvas.width = region.width;
1882
+ canvas.height = region.height;
1883
+ const ctx = canvas.getContext("2d");
1884
+ if (!ctx) {
1885
+ resolve(dataUrl);
1886
+ return;
1887
+ }
1888
+ const dpr = window.devicePixelRatio || 1;
1889
+ ctx.drawImage(
1890
+ img,
1891
+ region.x * dpr,
1892
+ region.y * dpr,
1893
+ region.width * dpr,
1894
+ region.height * dpr,
1895
+ 0,
1896
+ 0,
1897
+ region.width,
1898
+ region.height
1899
+ );
1900
+ resolve(canvas.toDataURL("image/webp", 0.9));
1901
+ } catch {
1902
+ resolve(dataUrl);
1903
+ }
1904
+ };
1905
+ img.onerror = () => resolve(dataUrl);
1906
+ img.src = dataUrl;
1907
+ });
1908
+ }
1909
+ extractElementContext(el) {
1910
+ const selector = getSemanticSelector(el);
1911
+ const tag = el.tagName.toLowerCase();
1912
+ const attributes = {};
1913
+ for (let i = 0; i < el.attributes.length; i++) {
1914
+ const attr = el.attributes[i];
1915
+ if (attr.name === "value" && el.type === "password") {
1916
+ attributes[attr.name] = "[REDACTED]";
1917
+ } else {
1918
+ attributes[attr.name] = attr.value;
1919
+ }
1920
+ }
1921
+ let outerHTML = "";
1922
+ try {
1923
+ const clone = el.cloneNode(true);
1924
+ for (const passInput of Array.from(clone.querySelectorAll('input[type="password"]'))) {
1925
+ passInput.setAttribute("value", "[REDACTED]");
1926
+ }
1927
+ outerHTML = truncate(clone.outerHTML, 10240);
1928
+ } catch {
1929
+ outerHTML = truncate(el.outerHTML, 10240);
1930
+ }
1931
+ let parent;
1932
+ if (el.parentElement && el.parentElement !== document.body) {
1933
+ parent = {
1934
+ selector: getSemanticSelector(el.parentElement),
1935
+ tag: el.parentElement.tagName.toLowerCase()
1936
+ };
1937
+ }
1938
+ return {
1939
+ selector,
1940
+ tag,
1941
+ attributes,
1942
+ outerHTML,
1943
+ innerText: truncate(el.textContent?.trim(), 200),
1944
+ parent
1945
+ };
1946
+ }
1947
+ destroy() {
1948
+ for (const cleanup of this.cleanups) {
1949
+ try {
1950
+ cleanup();
1951
+ } catch {
1952
+ }
1953
+ }
1954
+ this.cleanups = [];
1955
+ if (this.container && this.container.parentElement) {
1956
+ this.container.parentElement.removeChild(this.container);
1957
+ }
1958
+ this.container = null;
1959
+ this.shadowRoot = null;
1960
+ this.toolbarElement = null;
1961
+ }
1962
+ };
1963
+
1964
+ // packages/client/src/client.ts
1965
+ var BrowserTrackClient = class {
1966
+ constructor(options = {}) {
1967
+ __publicField(this, "options");
1968
+ __publicField(this, "breadcrumbs");
1969
+ __publicField(this, "transport");
1970
+ __publicField(this, "screenshotDriver");
1971
+ __publicField(this, "commandHandler");
1972
+ __publicField(this, "inspector");
1973
+ __publicField(this, "lastElement");
1974
+ __publicField(this, "currentRoute", "/");
1975
+ __publicField(this, "cleanups", []);
1976
+ __publicField(this, "isInitialized", false);
1977
+ this.options = {
1978
+ ...DEFAULT_OPTIONS,
1979
+ ...options,
1980
+ notes: {
1981
+ ...DEFAULT_OPTIONS.notes,
1982
+ ...options.notes || {}
1983
+ }
1984
+ };
1985
+ this.breadcrumbs = new BreadcrumbBuffer(this.options.maxBreadcrumbs);
1986
+ this.transport = new WebSocketTransport({
1987
+ url: this.options.daemonUrl,
1988
+ projectId: this.options.projectId,
1989
+ debug: this.options.debug
1990
+ });
1991
+ this.screenshotDriver = new BrowserScriptScreenshotDriver();
1992
+ this.commandHandler = new ClientCommandHandler(this.screenshotDriver);
1993
+ this.transport.setCommandHandler(this.commandHandler);
1994
+ if (this.options.notes.enabled) {
1995
+ this.inspector = new NoteInspector(this.transport, this.screenshotDriver, {
1996
+ shortcut: this.options.notes.shortcut,
1997
+ maskSelectors: this.options.notes.maskSelectors
1998
+ });
1999
+ }
2000
+ }
2001
+ init() {
2002
+ if (this.isInitialized || typeof window === "undefined") return this;
2003
+ this.isInitialized = true;
2004
+ try {
2005
+ this.currentRoute = window.location.pathname + window.location.search;
2006
+ } catch {
2007
+ this.currentRoute = "/";
2008
+ }
2009
+ this.transport.connect();
2010
+ if (this.options.captureErrors) {
2011
+ const cleanupRuntime = setupRuntimeInterceptors(async (errEvent) => {
2012
+ await this.handleRuntimeError(errEvent);
2013
+ });
2014
+ this.cleanups.push(cleanupRuntime);
2015
+ }
2016
+ if (this.options.captureConsole) {
2017
+ const cleanupConsole = setupConsoleInterceptors(async (consoleEvent) => {
2018
+ await this.handleConsole(consoleEvent);
2019
+ });
2020
+ this.cleanups.push(cleanupConsole);
2021
+ }
2022
+ if (this.options.captureNetwork) {
2023
+ const cleanupNetwork = setupNetworkInterceptors((netEvent) => {
2024
+ this.handleNetwork(netEvent);
2025
+ });
2026
+ this.cleanups.push(cleanupNetwork);
2027
+ }
2028
+ if (this.options.captureNavigation) {
2029
+ const cleanupNav = setupNavigationInterceptors((navEvent) => {
2030
+ this.handleNavigation(navEvent);
2031
+ });
2032
+ this.cleanups.push(cleanupNav);
2033
+ }
2034
+ if (this.options.captureInteractions) {
2035
+ const cleanupInteraction = setupInteractionInterceptors((breadcrumb, elSummary) => {
2036
+ if (elSummary) {
2037
+ this.lastElement = elSummary;
2038
+ }
2039
+ this.breadcrumbs.add(breadcrumb);
2040
+ });
2041
+ this.cleanups.push(cleanupInteraction);
2042
+ }
2043
+ if (this.inspector) {
2044
+ this.inspector.init();
2045
+ }
2046
+ return this;
2047
+ }
2048
+ openNoteEditor(el, noteType = "element") {
2049
+ if (this.inspector) {
2050
+ this.inspector.openNoteEditor(el, noteType);
2051
+ }
2052
+ }
2053
+ startRegionSelection() {
2054
+ if (this.inspector) {
2055
+ this.inspector.setMode("region");
2056
+ }
2057
+ }
2058
+ startElementSelection() {
2059
+ if (this.inspector) {
2060
+ this.inspector.setMode("element");
2061
+ }
2062
+ }
2063
+ setInspectMode(mode) {
2064
+ if (this.inspector) {
2065
+ this.inspector.setMode(mode);
2066
+ }
2067
+ }
2068
+ async createNote(targetEl, message, noteType = "element") {
2069
+ if (this.inspector) {
2070
+ await this.inspector.saveVisualNote(targetEl, message, noteType);
2071
+ }
2072
+ }
2073
+ async handleRuntimeError(errorEvent) {
2074
+ this.breadcrumbs.add({
2075
+ type: "error",
2076
+ category: "runtime",
2077
+ message: `${errorEvent.errorType}: ${errorEvent.message}`,
2078
+ timestamp: errorEvent.timestamp,
2079
+ level: "error",
2080
+ data: {
2081
+ filename: errorEvent.filename,
2082
+ lineno: errorEvent.lineno,
2083
+ colno: errorEvent.colno
2084
+ }
2085
+ });
2086
+ let screenshotDataUrl;
2087
+ if (this.options.onErrorScreenshot && this.lastElement?.selector) {
2088
+ try {
2089
+ const snap = await this.screenshotDriver.captureSelector(this.lastElement.selector);
2090
+ if (snap.ok) {
2091
+ screenshotDataUrl = snap.dataUrl;
2092
+ }
2093
+ } catch {
2094
+ }
2095
+ }
2096
+ this.sendEvent("runtime_error", errorEvent, screenshotDataUrl);
2097
+ }
2098
+ async handleConsole(consoleEvent) {
2099
+ this.breadcrumbs.add({
2100
+ type: "console",
2101
+ category: "console",
2102
+ message: `console.${consoleEvent.level}: ${consoleEvent.message}`,
2103
+ timestamp: consoleEvent.timestamp,
2104
+ level: consoleEvent.level === "error" ? "error" : consoleEvent.level === "warn" ? "warn" : "info"
2105
+ });
2106
+ if (consoleEvent.level === "error" || consoleEvent.level === "warn") {
2107
+ this.sendEvent("console", consoleEvent);
2108
+ }
2109
+ }
2110
+ handleNetwork(netEvent) {
2111
+ const isError = netEvent.status && netEvent.status >= 400 || !!netEvent.error;
2112
+ const msg = `${netEvent.method} ${netEvent.url} -> ${netEvent.status || "ERR"} (${netEvent.durationMs}ms)`;
2113
+ this.breadcrumbs.add({
2114
+ type: netEvent.id?.startsWith("xhr") ? "xhr" : "fetch",
2115
+ category: "network",
2116
+ message: msg,
2117
+ timestamp: netEvent.timestamp,
2118
+ level: isError ? "error" : "info",
2119
+ data: {
2120
+ status: netEvent.status,
2121
+ durationMs: netEvent.durationMs,
2122
+ error: netEvent.error,
2123
+ aborted: netEvent.aborted
2124
+ }
2125
+ });
2126
+ this.sendEvent("fetch", netEvent);
2127
+ }
2128
+ handleNavigation(navEvent) {
2129
+ this.currentRoute = navEvent.to;
2130
+ this.breadcrumbs.add({
2131
+ type: "navigation",
2132
+ category: "navigation",
2133
+ message: `navigate ${navEvent.to} (${navEvent.type})`,
2134
+ timestamp: navEvent.timestamp,
2135
+ level: "info",
2136
+ data: { from: navEvent.from, to: navEvent.to, type: navEvent.type }
2137
+ });
2138
+ this.sendEvent("navigation", navEvent);
2139
+ }
2140
+ sendEvent(eventType, payload, screenshotDataUrl) {
2141
+ if (typeof window === "undefined") return;
2142
+ const message = {
2143
+ type: "event",
2144
+ sessionId: this.transport.getSessionId() || "",
2145
+ eventType,
2146
+ payload,
2147
+ breadcrumbs: this.breadcrumbs.getRecent(),
2148
+ lastElement: this.lastElement,
2149
+ route: this.currentRoute,
2150
+ url: window.location.href,
2151
+ title: document.title,
2152
+ timestamp: Date.now(),
2153
+ screenshot: screenshotDataUrl
2154
+ };
2155
+ this.transport.send(message);
2156
+ }
2157
+ getBreadcrumbs() {
2158
+ return this.breadcrumbs.getRecent();
2159
+ }
2160
+ getLastElement() {
2161
+ return this.lastElement;
2162
+ }
2163
+ destroy() {
2164
+ for (const cleanup of this.cleanups) {
2165
+ try {
2166
+ cleanup();
2167
+ } catch {
2168
+ }
2169
+ }
2170
+ this.cleanups = [];
2171
+ this.transport.destroy();
2172
+ if (this.inspector) {
2173
+ this.inspector.destroy();
2174
+ }
2175
+ this.breadcrumbs.clear();
2176
+ this.isInitialized = false;
2177
+ }
2178
+ };
2179
+
2180
+ // packages/client/src/index.ts
2181
+ var defaultClient = null;
2182
+ function init(options = {}) {
2183
+ if (defaultClient) {
2184
+ return defaultClient;
2185
+ }
2186
+ defaultClient = new BrowserTrackClient(options);
2187
+ defaultClient.init();
2188
+ return defaultClient;
2189
+ }
2190
+ function getClient() {
2191
+ return defaultClient;
2192
+ }
2193
+ if (typeof window !== "undefined") {
2194
+ window.__BROWSERTRACK__ = {
2195
+ init,
2196
+ getClient
2197
+ };
2198
+ try {
2199
+ const currentScript = document.currentScript;
2200
+ const autoInit = currentScript?.getAttribute("data-auto-init") !== "false";
2201
+ const daemonUrl = currentScript?.getAttribute("data-daemon-url") || void 0;
2202
+ const projectId = currentScript?.getAttribute("data-project-id") || void 0;
2203
+ if (autoInit) {
2204
+ init({ daemonUrl, projectId });
2205
+ }
2206
+ } catch {
2207
+ }
2208
+ }
2209
+ // Annotate the CommonJS export names for ESM import in node:
2210
+ 0 && (module.exports = {
2211
+ BreadcrumbBuffer,
2212
+ BrowserScriptScreenshotDriver,
2213
+ BrowserTrackClient,
2214
+ DEFAULT_OPTIONS,
2215
+ NoteInspector,
2216
+ getClient,
2217
+ init
2218
+ });