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,2829 @@
1
+ #!/usr/bin/env node
2
+
3
+ // packages/cli/src/index.ts
4
+ import { Command } from "commander";
5
+ import fs5 from "fs";
6
+ import path6 from "path";
7
+
8
+ // packages/daemon/src/config.ts
9
+ import os from "os";
10
+ import path from "path";
11
+ function getDaemonConfig(overrides = {}) {
12
+ const homeDir = os.homedir();
13
+ const dataDir = overrides.dataDir || path.join(homeDir, ".browsertrack");
14
+ const dbPath = overrides.dbPath || path.join(dataDir, "browsertrack.db");
15
+ const screenshotsDir = overrides.screenshotsDir || path.join(dataDir, "projects");
16
+ return {
17
+ host: overrides.host || "127.0.0.1",
18
+ port: overrides.port || 7331,
19
+ dataDir,
20
+ dbPath,
21
+ screenshotsDir,
22
+ maxEventsPerSession: overrides.maxEventsPerSession || 1e3,
23
+ verbose: !!overrides.verbose
24
+ };
25
+ }
26
+
27
+ // packages/daemon/src/storage/db.ts
28
+ import Database from "better-sqlite3";
29
+ import fs from "fs";
30
+ import path2 from "path";
31
+ var StorageDB = class {
32
+ db;
33
+ constructor(dbPath) {
34
+ const dir = path2.dirname(dbPath);
35
+ fs.mkdirSync(dir, { recursive: true });
36
+ this.db = new Database(dbPath);
37
+ this.db.pragma("journal_mode = WAL");
38
+ this.db.pragma("synchronous = NORMAL");
39
+ this.initTables();
40
+ }
41
+ initTables() {
42
+ this.db.exec(`
43
+ CREATE TABLE IF NOT EXISTS projects (
44
+ id TEXT PRIMARY KEY,
45
+ name TEXT UNIQUE,
46
+ origin TEXT,
47
+ path TEXT,
48
+ created_at TEXT,
49
+ updated_at TEXT
50
+ );
51
+
52
+ CREATE TABLE IF NOT EXISTS sessions (
53
+ id TEXT PRIMARY KEY,
54
+ project_id TEXT,
55
+ origin TEXT,
56
+ url TEXT,
57
+ title TEXT,
58
+ user_agent TEXT,
59
+ connected_at TEXT,
60
+ last_seen_at TEXT,
61
+ active INTEGER DEFAULT 1
62
+ );
63
+
64
+ CREATE TABLE IF NOT EXISTS events (
65
+ id TEXT PRIMARY KEY,
66
+ session_id TEXT,
67
+ event_type TEXT,
68
+ payload TEXT,
69
+ timestamp INTEGER,
70
+ route TEXT,
71
+ url TEXT
72
+ );
73
+
74
+ CREATE TABLE IF NOT EXISTS incidents (
75
+ id TEXT PRIMARY KEY,
76
+ project_id TEXT,
77
+ session_id TEXT,
78
+ type TEXT,
79
+ severity TEXT,
80
+ message TEXT,
81
+ source_file TEXT,
82
+ source_line INTEGER,
83
+ source_col INTEGER,
84
+ fingerprint TEXT UNIQUE,
85
+ route TEXT,
86
+ first_seen TEXT,
87
+ last_seen TEXT,
88
+ occurrences INTEGER DEFAULT 1,
89
+ status TEXT DEFAULT 'OPEN',
90
+ stack TEXT,
91
+ breadcrumbs TEXT,
92
+ network_failures TEXT,
93
+ last_element TEXT,
94
+ screenshot_path TEXT
95
+ );
96
+
97
+ CREATE TABLE IF NOT EXISTS incident_occurrences (
98
+ id TEXT PRIMARY KEY,
99
+ incident_id TEXT,
100
+ session_id TEXT,
101
+ timestamp TEXT,
102
+ route TEXT,
103
+ url TEXT,
104
+ stack TEXT,
105
+ breadcrumbs TEXT,
106
+ last_element TEXT
107
+ );
108
+
109
+ CREATE TABLE IF NOT EXISTS screenshots (
110
+ id TEXT PRIMARY KEY,
111
+ incident_id TEXT,
112
+ file_path TEXT,
113
+ format TEXT,
114
+ width INTEGER,
115
+ height INTEGER,
116
+ created_at TEXT
117
+ );
118
+
119
+ CREATE TABLE IF NOT EXISTS verifications (
120
+ id TEXT PRIMARY KEY,
121
+ incident_id TEXT,
122
+ status TEXT,
123
+ checks TEXT,
124
+ before_screenshot TEXT,
125
+ after_screenshot TEXT,
126
+ message TEXT,
127
+ created_at TEXT
128
+ );
129
+
130
+ CREATE TABLE IF NOT EXISTS notes (
131
+ id TEXT PRIMARY KEY,
132
+ project_id TEXT,
133
+ session_id TEXT,
134
+ type TEXT,
135
+ message TEXT,
136
+ route TEXT,
137
+ url TEXT,
138
+ viewport_json TEXT,
139
+ scroll_json TEXT,
140
+ target_json TEXT,
141
+ element_context_json TEXT,
142
+ region_json TEXT,
143
+ screenshot_path TEXT,
144
+ incident_id TEXT,
145
+ status TEXT DEFAULT 'OPEN',
146
+ created_at TEXT,
147
+ updated_at TEXT,
148
+ resolved_at TEXT
149
+ );
150
+
151
+ CREATE TABLE IF NOT EXISTS note_verifications (
152
+ id TEXT PRIMARY KEY,
153
+ note_id TEXT,
154
+ status TEXT,
155
+ checks TEXT,
156
+ geometry_diff TEXT,
157
+ before_screenshot TEXT,
158
+ after_screenshot TEXT,
159
+ message TEXT,
160
+ created_at TEXT
161
+ );
162
+
163
+ CREATE INDEX IF NOT EXISTS idx_events_session ON events(session_id, timestamp);
164
+ CREATE INDEX IF NOT EXISTS idx_incidents_project ON incidents(project_id, status);
165
+ CREATE INDEX IF NOT EXISTS idx_incidents_fp ON incidents(fingerprint);
166
+ CREATE INDEX IF NOT EXISTS idx_notes_project ON notes(project_id, status);
167
+ `);
168
+ }
169
+ // --- PROJECTS ---
170
+ upsertProject(project) {
171
+ const now = (/* @__PURE__ */ new Date()).toISOString();
172
+ const existing = this.db.prepare("SELECT * FROM projects WHERE name = ? OR origin = ?").get(project.name, project.origin);
173
+ if (existing) {
174
+ this.db.prepare("UPDATE projects SET name = ?, origin = ?, path = COALESCE(?, path), updated_at = ? WHERE id = ?").run(project.name, project.origin, project.path || null, now, existing.id);
175
+ return {
176
+ id: existing.id,
177
+ name: project.name,
178
+ origin: project.origin,
179
+ path: project.path || existing.path,
180
+ createdAt: existing.created_at,
181
+ updatedAt: now
182
+ };
183
+ }
184
+ this.db.prepare("INSERT INTO projects (id, name, origin, path, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)").run(project.id, project.name, project.origin, project.path || null, now, now);
185
+ return {
186
+ id: project.id,
187
+ name: project.name,
188
+ origin: project.origin,
189
+ path: project.path,
190
+ createdAt: now,
191
+ updatedAt: now
192
+ };
193
+ }
194
+ getProject(idOrName) {
195
+ const row = this.db.prepare("SELECT * FROM projects WHERE id = ? OR name = ?").get(idOrName, idOrName);
196
+ if (!row) return null;
197
+ return {
198
+ id: row.id,
199
+ name: row.name,
200
+ origin: row.origin,
201
+ path: row.path,
202
+ createdAt: row.created_at,
203
+ updatedAt: row.updated_at
204
+ };
205
+ }
206
+ getProjectByOrigin(origin) {
207
+ const row = this.db.prepare("SELECT * FROM projects WHERE origin = ?").get(origin);
208
+ if (!row) return null;
209
+ return {
210
+ id: row.id,
211
+ name: row.name,
212
+ origin: row.origin,
213
+ path: row.path,
214
+ createdAt: row.created_at,
215
+ updatedAt: row.updated_at
216
+ };
217
+ }
218
+ listProjects() {
219
+ const rows = this.db.prepare("SELECT * FROM projects ORDER BY updated_at DESC").all();
220
+ return rows.map((row) => ({
221
+ id: row.id,
222
+ name: row.name,
223
+ origin: row.origin,
224
+ path: row.path,
225
+ createdAt: row.created_at,
226
+ updatedAt: row.updated_at
227
+ }));
228
+ }
229
+ // --- SESSIONS ---
230
+ upsertSession(session) {
231
+ const existing = this.db.prepare("SELECT id FROM sessions WHERE id = ?").get(session.id);
232
+ if (existing) {
233
+ this.db.prepare("UPDATE sessions SET url = ?, title = ?, last_seen_at = ?, active = ? WHERE id = ?").run(session.url, session.title, session.lastSeenAt, session.active ? 1 : 0, session.id);
234
+ } else {
235
+ this.db.prepare(
236
+ "INSERT INTO sessions (id, project_id, origin, url, title, user_agent, connected_at, last_seen_at, active) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
237
+ ).run(
238
+ session.id,
239
+ session.projectId,
240
+ session.origin,
241
+ session.url,
242
+ session.title,
243
+ session.userAgent,
244
+ session.connectedAt,
245
+ session.lastSeenAt,
246
+ session.active ? 1 : 0
247
+ );
248
+ }
249
+ }
250
+ getSession(id) {
251
+ const row = this.db.prepare("SELECT * FROM sessions WHERE id = ?").get(id);
252
+ if (!row) return null;
253
+ return {
254
+ id: row.id,
255
+ projectId: row.project_id,
256
+ origin: row.origin,
257
+ url: row.url,
258
+ title: row.title,
259
+ userAgent: row.user_agent,
260
+ connectedAt: row.connected_at,
261
+ lastSeenAt: row.last_seen_at,
262
+ active: row.active === 1
263
+ };
264
+ }
265
+ listSessions(projectId, activeOnly = false) {
266
+ let sql = "SELECT * FROM sessions WHERE 1=1";
267
+ const params = [];
268
+ if (projectId) {
269
+ sql += " AND project_id = ?";
270
+ params.push(projectId);
271
+ }
272
+ if (activeOnly) {
273
+ sql += " AND active = 1";
274
+ }
275
+ sql += " ORDER BY last_seen_at DESC";
276
+ const rows = this.db.prepare(sql).all(...params);
277
+ return rows.map((row) => ({
278
+ id: row.id,
279
+ projectId: row.project_id,
280
+ origin: row.origin,
281
+ url: row.url,
282
+ title: row.title,
283
+ userAgent: row.user_agent,
284
+ connectedAt: row.connected_at,
285
+ lastSeenAt: row.last_seen_at,
286
+ active: row.active === 1
287
+ }));
288
+ }
289
+ deactivateSession(id) {
290
+ this.db.prepare("UPDATE sessions SET active = 0, last_seen_at = ? WHERE id = ?").run((/* @__PURE__ */ new Date()).toISOString(), id);
291
+ }
292
+ // --- EVENTS & RETENTION ---
293
+ insertEvent(event) {
294
+ this.db.prepare("INSERT INTO events (id, session_id, event_type, payload, timestamp, route, url) VALUES (?, ?, ?, ?, ?, ?, ?)").run(
295
+ event.id,
296
+ event.sessionId,
297
+ event.eventType,
298
+ JSON.stringify(event.payload),
299
+ event.timestamp,
300
+ event.route || "",
301
+ event.url || ""
302
+ );
303
+ }
304
+ pruneSessionEvents(sessionId, maxEvents = 1e3) {
305
+ this.db.prepare(
306
+ `DELETE FROM events
307
+ WHERE session_id = ? AND id NOT IN (
308
+ SELECT id FROM events WHERE session_id = ? ORDER BY timestamp DESC LIMIT ?
309
+ )`
310
+ ).run(sessionId, sessionId, maxEvents);
311
+ }
312
+ getEvents(options) {
313
+ let sql = "SELECT * FROM events WHERE 1=1";
314
+ const params = [];
315
+ if (options.sessionId) {
316
+ sql += " AND session_id = ?";
317
+ params.push(options.sessionId);
318
+ }
319
+ if (options.eventType) {
320
+ sql += " AND event_type = ?";
321
+ params.push(options.eventType);
322
+ }
323
+ sql += " ORDER BY timestamp DESC LIMIT ?";
324
+ params.push(options.limit || 50);
325
+ const rows = this.db.prepare(sql).all(...params);
326
+ return rows.map((r) => ({
327
+ id: r.id,
328
+ sessionId: r.session_id,
329
+ eventType: r.event_type,
330
+ payload: JSON.parse(r.payload),
331
+ timestamp: r.timestamp,
332
+ route: r.route,
333
+ url: r.url
334
+ }));
335
+ }
336
+ // --- INCIDENTS ---
337
+ findIncidentByFingerprint(fingerprint) {
338
+ const row = this.db.prepare("SELECT * FROM incidents WHERE fingerprint = ?").get(fingerprint);
339
+ if (!row) return null;
340
+ return this.mapIncidentRow(row);
341
+ }
342
+ getIncident(id) {
343
+ const row = this.db.prepare("SELECT * FROM incidents WHERE id = ?").get(id);
344
+ if (!row) return null;
345
+ return this.mapIncidentRow(row);
346
+ }
347
+ listIncidents(options = {}) {
348
+ let sql = "SELECT * FROM incidents WHERE 1=1";
349
+ const params = [];
350
+ if (options.projectId) {
351
+ sql += " AND project_id = ?";
352
+ params.push(options.projectId);
353
+ }
354
+ if (options.status) {
355
+ sql += " AND status = ?";
356
+ params.push(options.status);
357
+ }
358
+ if (options.severity) {
359
+ sql += " AND severity = ?";
360
+ params.push(options.severity);
361
+ }
362
+ sql += " ORDER BY last_seen DESC LIMIT ?";
363
+ params.push(options.limit || 50);
364
+ const rows = this.db.prepare(sql).all(...params);
365
+ return rows.map((r) => this.mapIncidentRow(r));
366
+ }
367
+ insertIncident(incident) {
368
+ this.db.prepare(
369
+ `INSERT INTO incidents (
370
+ id, project_id, session_id, type, severity, message, source_file, source_line, source_col,
371
+ fingerprint, route, first_seen, last_seen, occurrences, status, stack, breadcrumbs,
372
+ network_failures, last_element, screenshot_path
373
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
374
+ ).run(
375
+ incident.id,
376
+ incident.projectId,
377
+ incident.sessionId,
378
+ incident.type,
379
+ incident.severity,
380
+ incident.message,
381
+ incident.source.file,
382
+ incident.source.line,
383
+ incident.source.column || null,
384
+ incident.fingerprint,
385
+ incident.route,
386
+ incident.firstSeen,
387
+ incident.lastSeen,
388
+ incident.occurrences,
389
+ incident.status,
390
+ incident.stack || null,
391
+ JSON.stringify(incident.breadcrumbs || []),
392
+ JSON.stringify(incident.networkFailures || []),
393
+ incident.lastElement ? JSON.stringify(incident.lastElement) : null,
394
+ incident.screenshots?.error || null
395
+ );
396
+ }
397
+ updateIncidentOccurrence(incidentId, update) {
398
+ this.db.prepare(
399
+ `UPDATE incidents
400
+ SET session_id = ?, last_seen = ?, occurrences = ?, route = ?, breadcrumbs = ?, last_element = COALESCE(?, last_element), stack = COALESCE(?, stack)
401
+ WHERE id = ?`
402
+ ).run(
403
+ update.sessionId,
404
+ update.lastSeen,
405
+ update.occurrences,
406
+ update.route,
407
+ JSON.stringify(update.breadcrumbs || []),
408
+ update.lastElement ? JSON.stringify(update.lastElement) : null,
409
+ update.stack || null,
410
+ incidentId
411
+ );
412
+ }
413
+ updateIncidentStatus(id, status) {
414
+ this.db.prepare("UPDATE incidents SET status = ? WHERE id = ?").run(status, id);
415
+ }
416
+ insertIncidentOccurrence(occurrence) {
417
+ this.db.prepare(
418
+ `INSERT INTO incident_occurrences (id, incident_id, session_id, timestamp, route, url, stack, breadcrumbs, last_element)
419
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
420
+ ).run(
421
+ occurrence.id,
422
+ occurrence.incidentId,
423
+ occurrence.sessionId,
424
+ occurrence.timestamp,
425
+ occurrence.route,
426
+ occurrence.url,
427
+ occurrence.stack || null,
428
+ JSON.stringify(occurrence.breadcrumbs || []),
429
+ occurrence.lastElement ? JSON.stringify(occurrence.lastElement) : null
430
+ );
431
+ }
432
+ // --- VERIFICATIONS ---
433
+ insertVerification(v) {
434
+ this.db.prepare(
435
+ `INSERT INTO verifications (id, incident_id, status, checks, before_screenshot, after_screenshot, message, created_at)
436
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
437
+ ).run(
438
+ v.id,
439
+ v.incidentId,
440
+ v.status,
441
+ JSON.stringify(v.checks),
442
+ v.beforeScreenshot || null,
443
+ v.afterScreenshot || null,
444
+ v.message || null,
445
+ v.createdAt
446
+ );
447
+ }
448
+ getLatestVerification(incidentId) {
449
+ const row = this.db.prepare("SELECT * FROM verifications WHERE incident_id = ? ORDER BY created_at DESC LIMIT 1").get(incidentId);
450
+ if (!row) return null;
451
+ return {
452
+ incidentId: row.incident_id,
453
+ status: row.status,
454
+ checks: JSON.parse(row.checks || "[]"),
455
+ screenshots: {
456
+ before: row.before_screenshot || void 0,
457
+ after: row.after_screenshot || void 0
458
+ },
459
+ timestamp: row.created_at,
460
+ message: row.message || void 0
461
+ };
462
+ }
463
+ // --- VISUAL NOTES ---
464
+ insertNote(note) {
465
+ this.db.prepare(
466
+ `INSERT INTO notes (
467
+ id, project_id, session_id, type, message, route, url,
468
+ viewport_json, scroll_json, target_json, element_context_json, region_json,
469
+ screenshot_path, incident_id, status, created_at, updated_at, resolved_at
470
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
471
+ ).run(
472
+ note.id,
473
+ note.projectId,
474
+ note.sessionId,
475
+ note.type,
476
+ note.message,
477
+ note.route,
478
+ note.url,
479
+ JSON.stringify(note.viewport),
480
+ JSON.stringify(note.scroll),
481
+ note.target ? JSON.stringify(note.target) : null,
482
+ note.elementContext ? JSON.stringify(note.elementContext) : null,
483
+ note.region ? JSON.stringify(note.region) : null,
484
+ note.screenshots?.original || null,
485
+ note.incidentId || null,
486
+ note.status,
487
+ note.createdAt,
488
+ note.updatedAt,
489
+ note.resolvedAt || null
490
+ );
491
+ }
492
+ getNote(id) {
493
+ const row = this.db.prepare("SELECT * FROM notes WHERE id = ?").get(id);
494
+ if (!row) return null;
495
+ return this.mapNoteRow(row);
496
+ }
497
+ listNotes(options = {}) {
498
+ let sql = "SELECT * FROM notes WHERE 1=1";
499
+ const params = [];
500
+ if (options.projectId) {
501
+ sql += " AND project_id = ?";
502
+ params.push(options.projectId);
503
+ }
504
+ if (options.status) {
505
+ sql += " AND status = ?";
506
+ params.push(options.status);
507
+ }
508
+ sql += " ORDER BY created_at DESC LIMIT ?";
509
+ params.push(options.limit || 50);
510
+ const rows = this.db.prepare(sql).all(...params);
511
+ return rows.map((r) => this.mapNoteRow(r));
512
+ }
513
+ updateNoteStatus(id, status) {
514
+ const now = (/* @__PURE__ */ new Date()).toISOString();
515
+ const resolvedAt = status === "RESOLVED" ? now : null;
516
+ this.db.prepare("UPDATE notes SET status = ?, updated_at = ?, resolved_at = COALESCE(?, resolved_at) WHERE id = ?").run(status, now, resolvedAt, id);
517
+ }
518
+ updateNote(id, updates) {
519
+ const now = (/* @__PURE__ */ new Date()).toISOString();
520
+ const current = this.getNote(id);
521
+ if (!current) return;
522
+ this.db.prepare(
523
+ `UPDATE notes
524
+ SET message = COALESCE(?, message), status = COALESCE(?, status),
525
+ screenshot_path = COALESCE(?, screenshot_path), updated_at = ?
526
+ WHERE id = ?`
527
+ ).run(
528
+ updates.message || null,
529
+ updates.status || null,
530
+ updates.screenshots?.original || updates.screenshots?.after || null,
531
+ now,
532
+ id
533
+ );
534
+ }
535
+ insertNoteVerification(v) {
536
+ this.db.prepare(
537
+ `INSERT INTO note_verifications (
538
+ id, note_id, status, checks, geometry_diff, before_screenshot, after_screenshot, message, created_at
539
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
540
+ ).run(
541
+ `nver_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
542
+ v.noteId,
543
+ v.status,
544
+ JSON.stringify(v.checks),
545
+ v.geometryDiff ? JSON.stringify(v.geometryDiff) : null,
546
+ v.screenshots?.before || null,
547
+ v.screenshots?.after || null,
548
+ v.message || null,
549
+ v.timestamp
550
+ );
551
+ }
552
+ getLatestNoteVerification(noteId) {
553
+ const row = this.db.prepare("SELECT * FROM note_verifications WHERE note_id = ? ORDER BY created_at DESC LIMIT 1").get(noteId);
554
+ if (!row) return null;
555
+ return {
556
+ noteId: row.note_id,
557
+ status: row.status,
558
+ checks: JSON.parse(row.checks || "[]"),
559
+ geometryDiff: row.geometry_diff ? JSON.parse(row.geometry_diff) : void 0,
560
+ screenshots: {
561
+ before: row.before_screenshot || void 0,
562
+ after: row.after_screenshot || void 0
563
+ },
564
+ timestamp: row.created_at,
565
+ message: row.message || void 0
566
+ };
567
+ }
568
+ clearAll() {
569
+ this.db.exec(`
570
+ DELETE FROM events;
571
+ DELETE FROM incident_occurrences;
572
+ DELETE FROM incidents;
573
+ DELETE FROM screenshots;
574
+ DELETE FROM verifications;
575
+ DELETE FROM notes;
576
+ DELETE FROM note_verifications;
577
+ DELETE FROM sessions;
578
+ `);
579
+ }
580
+ mapIncidentRow(row) {
581
+ return {
582
+ id: row.id,
583
+ projectId: row.project_id,
584
+ sessionId: row.session_id,
585
+ type: row.type,
586
+ severity: row.severity,
587
+ message: row.message,
588
+ source: {
589
+ file: row.source_file,
590
+ line: row.source_line,
591
+ column: row.source_col || void 0
592
+ },
593
+ fingerprint: row.fingerprint,
594
+ route: row.route,
595
+ firstSeen: row.first_seen,
596
+ lastSeen: row.last_seen,
597
+ occurrences: row.occurrences,
598
+ status: row.status,
599
+ stack: row.stack || void 0,
600
+ breadcrumbs: JSON.parse(row.breadcrumbs || "[]"),
601
+ networkFailures: JSON.parse(row.network_failures || "[]"),
602
+ lastElement: row.last_element ? JSON.parse(row.last_element) : void 0,
603
+ screenshots: row.screenshot_path ? {
604
+ error: row.screenshot_path
605
+ } : void 0
606
+ };
607
+ }
608
+ mapNoteRow(row) {
609
+ return {
610
+ id: row.id,
611
+ projectId: row.project_id,
612
+ sessionId: row.session_id,
613
+ type: row.type,
614
+ message: row.message,
615
+ route: row.route,
616
+ url: row.url,
617
+ viewport: JSON.parse(row.viewport_json || "{}"),
618
+ scroll: JSON.parse(row.scroll_json || "{}"),
619
+ target: row.target_json ? JSON.parse(row.target_json) : void 0,
620
+ elementContext: row.element_context_json ? JSON.parse(row.element_context_json) : void 0,
621
+ region: row.region_json ? JSON.parse(row.region_json) : void 0,
622
+ status: row.status,
623
+ incidentId: row.incident_id || void 0,
624
+ screenshots: row.screenshot_path ? {
625
+ original: row.screenshot_path
626
+ } : void 0,
627
+ createdAt: row.created_at,
628
+ updatedAt: row.updated_at,
629
+ resolvedAt: row.resolved_at || void 0
630
+ };
631
+ }
632
+ close() {
633
+ this.db.close();
634
+ }
635
+ };
636
+
637
+ // packages/daemon/src/storage/screenshot-store.ts
638
+ import fs2 from "fs";
639
+ import path3 from "path";
640
+ var ScreenshotStore = class {
641
+ baseDir;
642
+ constructor(baseDir) {
643
+ this.baseDir = baseDir;
644
+ fs2.mkdirSync(this.baseDir, { recursive: true });
645
+ }
646
+ saveScreenshot(projectId, incidentId, name, dataUrl) {
647
+ try {
648
+ if (!dataUrl || !dataUrl.startsWith("data:image/")) return null;
649
+ const match = dataUrl.match(/^data:image\/([a-zA-Z0-9+]+);base64,(.+)$/);
650
+ if (!match) return null;
651
+ let format = match[1].toLowerCase();
652
+ if (format === "jpeg") format = "jpg";
653
+ const base64Data = match[2];
654
+ const buffer = Buffer.from(base64Data, "base64");
655
+ const targetDir = path3.join(this.baseDir, projectId || "default", "incidents", incidentId);
656
+ fs2.mkdirSync(targetDir, { recursive: true });
657
+ const fileName = `${name}.${format}`;
658
+ const filePath = path3.join(targetDir, fileName);
659
+ fs2.writeFileSync(filePath, buffer);
660
+ return { filePath, format };
661
+ } catch {
662
+ return null;
663
+ }
664
+ }
665
+ getScreenshotPath(projectId, incidentId, name) {
666
+ const targetDir = path3.join(this.baseDir, projectId || "default", "incidents", incidentId);
667
+ if (!fs2.existsSync(targetDir)) return null;
668
+ const files = fs2.readdirSync(targetDir);
669
+ const match = files.find((f) => f.startsWith(`${name}.`));
670
+ if (match) {
671
+ return path3.join(targetDir, match);
672
+ }
673
+ return null;
674
+ }
675
+ };
676
+
677
+ // packages/daemon/src/session/manager.ts
678
+ var SessionManager = class {
679
+ activeSockets = /* @__PURE__ */ new Map();
680
+ db;
681
+ constructor(db) {
682
+ this.db = db;
683
+ }
684
+ registerSocket(sessionId, ws, origin, projectId) {
685
+ this.activeSockets.set(sessionId, {
686
+ sessionId,
687
+ ws,
688
+ origin,
689
+ projectId,
690
+ pendingCommands: /* @__PURE__ */ new Map()
691
+ });
692
+ }
693
+ unregisterSocket(sessionId) {
694
+ const active = this.activeSockets.get(sessionId);
695
+ if (active) {
696
+ for (const pending of active.pendingCommands.values()) {
697
+ clearTimeout(pending.timer);
698
+ pending.reject(new Error("Session disconnected before command completed"));
699
+ }
700
+ active.pendingCommands.clear();
701
+ this.activeSockets.delete(sessionId);
702
+ this.db.deactivateSession(sessionId);
703
+ }
704
+ }
705
+ handleCommandResponse(sessionId, response) {
706
+ const active = this.activeSockets.get(sessionId);
707
+ if (!active) return;
708
+ const pending = active.pendingCommands.get(response.id);
709
+ if (pending) {
710
+ clearTimeout(pending.timer);
711
+ active.pendingCommands.delete(response.id);
712
+ pending.resolve(response);
713
+ }
714
+ }
715
+ async sendCommand(sessionId, command, timeoutMs = 5e3) {
716
+ const active = this.activeSockets.get(sessionId);
717
+ if (!active || active.ws.readyState !== 1) {
718
+ return {
719
+ id: command.id,
720
+ ok: false,
721
+ error: `Session ${sessionId} is not actively connected`
722
+ };
723
+ }
724
+ return new Promise((resolve, reject) => {
725
+ const timer = setTimeout(() => {
726
+ active.pendingCommands.delete(command.id);
727
+ resolve({
728
+ id: command.id,
729
+ ok: false,
730
+ error: `Command ${command.type} timed out after ${timeoutMs}ms`
731
+ });
732
+ }, timeoutMs);
733
+ active.pendingCommands.set(command.id, {
734
+ resolve,
735
+ reject,
736
+ timer
737
+ });
738
+ try {
739
+ active.ws.send(
740
+ JSON.stringify({
741
+ type: "command",
742
+ command
743
+ })
744
+ );
745
+ } catch (err) {
746
+ clearTimeout(timer);
747
+ active.pendingCommands.delete(command.id);
748
+ resolve({
749
+ id: command.id,
750
+ ok: false,
751
+ error: err?.message || "Failed to send command over WebSocket"
752
+ });
753
+ }
754
+ });
755
+ }
756
+ getActiveSessionForProject(projectId) {
757
+ for (const active of this.activeSockets.values()) {
758
+ if (active.projectId === projectId) {
759
+ return this.db.getSession(active.sessionId);
760
+ }
761
+ }
762
+ return null;
763
+ }
764
+ getAnyActiveSession() {
765
+ const first = this.activeSockets.keys().next().value;
766
+ if (!first) return null;
767
+ return this.db.getSession(first);
768
+ }
769
+ getActiveCount() {
770
+ return this.activeSockets.size;
771
+ }
772
+ };
773
+
774
+ // packages/daemon/src/incidents/engine.ts
775
+ import crypto from "crypto";
776
+
777
+ // packages/core/src/fingerprint.ts
778
+ function normalizeErrorMessage(message) {
779
+ if (!message) return "unknown_error";
780
+ return message.trim().replace(/0x[0-9a-fA-F]+/g, "0x<HEX>").replace(/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/g, "<UUID>").replace(/\?[tv]=[\w.-]+/g, "").replace(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z?/g, "<TIMESTAMP>").replace(/#\d+/g, "#<ID>").replace(/\s+/g, " ");
781
+ }
782
+ function normalizeSourceFile(filename) {
783
+ if (!filename) return "unknown_source";
784
+ let cleaned = filename.trim();
785
+ try {
786
+ if (cleaned.startsWith("http://") || cleaned.startsWith("https://")) {
787
+ const url = new URL(cleaned);
788
+ cleaned = url.pathname;
789
+ }
790
+ } catch {
791
+ cleaned = cleaned.replace(/^https?:\/\/[^/]+/, "");
792
+ }
793
+ cleaned = cleaned.split("?")[0].split("#")[0];
794
+ cleaned = cleaned.replace(/\\/g, "/");
795
+ return cleaned || "unknown_source";
796
+ }
797
+ function extractSourceFromStack(stack) {
798
+ if (!stack) return null;
799
+ const lines = stack.split("\n");
800
+ for (const line of lines) {
801
+ const match = line.match(/(?:at\s+(?:.*?\s+\()?)?(https?:\/\/[^\s)]+|file:\/\/[^\s)]+|\/[^\s)]+):(\d+):(\d+)\)?/);
802
+ if (match) {
803
+ return {
804
+ file: normalizeSourceFile(match[1]),
805
+ line: parseInt(match[2], 10),
806
+ column: parseInt(match[3], 10)
807
+ };
808
+ }
809
+ }
810
+ return null;
811
+ }
812
+ function djb2Hash(str) {
813
+ let hash = 5381;
814
+ for (let i = 0; i < str.length; i++) {
815
+ hash = hash * 33 ^ str.charCodeAt(i);
816
+ }
817
+ return (hash >>> 0).toString(16).padStart(8, "0");
818
+ }
819
+ function computeFingerprint(input) {
820
+ const normType = (input.type || "Error").trim().toLowerCase();
821
+ const normMsg = normalizeErrorMessage(input.message);
822
+ let sourceFile = normalizeSourceFile(input.sourceFile);
823
+ let line = input.line || 0;
824
+ if ((sourceFile === "unknown_source" || line === 0) && input.stack) {
825
+ const extracted = extractSourceFromStack(input.stack);
826
+ if (extracted) {
827
+ sourceFile = extracted.file;
828
+ line = extracted.line;
829
+ }
830
+ }
831
+ const rawKey = `${normType}::${normMsg}::${sourceFile}::${line}`;
832
+ const hash = djb2Hash(rawKey);
833
+ return `fp_${hash}`;
834
+ }
835
+
836
+ // packages/core/src/redaction.ts
837
+ var SENSITIVE_KEY_PATTERNS = [
838
+ /^authorization$/i,
839
+ /^cookie$/i,
840
+ /^set-cookie$/i,
841
+ /password/i,
842
+ /token/i,
843
+ /secret/i,
844
+ /api[-_]?key/i,
845
+ /access[-_]?token/i,
846
+ /refresh[-_]?token/i,
847
+ /credentials/i,
848
+ /private[-_]?key/i,
849
+ /ssn/i,
850
+ /credit[-_]?card/i,
851
+ /cvv/i
852
+ ];
853
+ var SENSITIVE_QUERY_PARAMS = [
854
+ "token",
855
+ "auth",
856
+ "key",
857
+ "apikey",
858
+ "api_key",
859
+ "secret",
860
+ "password",
861
+ "access_token",
862
+ "refresh_token",
863
+ "code",
864
+ "signature"
865
+ ];
866
+ var REDACTED_PLACEHOLDER = "[REDACTED]";
867
+ function isSensitiveKey(key) {
868
+ if (!key) return false;
869
+ const cleaned = key.replace(/[-_]/g, "");
870
+ return SENSITIVE_KEY_PATTERNS.some((pattern) => pattern.test(key) || pattern.test(cleaned));
871
+ }
872
+ function redactUrl(rawUrl) {
873
+ if (!rawUrl) return rawUrl;
874
+ try {
875
+ const isRelative = !rawUrl.startsWith("http://") && !rawUrl.startsWith("https://") && !rawUrl.startsWith("ws://") && !rawUrl.startsWith("wss://");
876
+ const base = "http://localhost";
877
+ const parsed = new URL(rawUrl, base);
878
+ let changed = false;
879
+ for (const param of SENSITIVE_QUERY_PARAMS) {
880
+ if (parsed.searchParams.has(param)) {
881
+ parsed.searchParams.set(param, REDACTED_PLACEHOLDER);
882
+ changed = true;
883
+ }
884
+ }
885
+ for (const key of Array.from(parsed.searchParams.keys())) {
886
+ if (isSensitiveKey(key)) {
887
+ parsed.searchParams.set(key, REDACTED_PLACEHOLDER);
888
+ changed = true;
889
+ }
890
+ }
891
+ if (!changed) return rawUrl;
892
+ let result = isRelative ? parsed.pathname + parsed.search + parsed.hash : parsed.toString();
893
+ result = result.replace(/%5BREDACTED%5D/g, REDACTED_PLACEHOLDER);
894
+ return result;
895
+ } catch {
896
+ let safe = rawUrl;
897
+ for (const param of SENSITIVE_QUERY_PARAMS) {
898
+ const reg = new RegExp(`([?&]${param}=)[^&#]+`, "gi");
899
+ safe = safe.replace(reg, `$1${REDACTED_PLACEHOLDER}`);
900
+ }
901
+ return safe;
902
+ }
903
+ }
904
+ function redactSensitiveData(data, maxDepth = 6, currentDepth = 0) {
905
+ if (data === null || data === void 0) return data;
906
+ if (typeof data !== "object") return data;
907
+ if (currentDepth > maxDepth) return "[DEPTH_EXCEEDED]";
908
+ if (Array.isArray(data)) {
909
+ return data.map((item) => redactSensitiveData(item, maxDepth, currentDepth + 1));
910
+ }
911
+ const result = {};
912
+ for (const [key, value] of Object.entries(data)) {
913
+ if (typeof value === "object" && value !== null) {
914
+ result[key] = redactSensitiveData(value, maxDepth, currentDepth + 1);
915
+ } else if (isSensitiveKey(key)) {
916
+ result[key] = REDACTED_PLACEHOLDER;
917
+ } else if (typeof value === "string") {
918
+ if (value.startsWith("http://") || value.startsWith("https://") || value.includes("?")) {
919
+ result[key] = redactUrl(value);
920
+ } else {
921
+ result[key] = value;
922
+ }
923
+ } else {
924
+ result[key] = value;
925
+ }
926
+ }
927
+ return result;
928
+ }
929
+
930
+ // packages/daemon/src/incidents/engine.ts
931
+ var IncidentEngine = class {
932
+ db;
933
+ screenshotStore;
934
+ constructor(db, screenshotStore) {
935
+ this.db = db;
936
+ this.screenshotStore = screenshotStore;
937
+ }
938
+ processClientEvent(message) {
939
+ const session = this.db.getSession(message.sessionId);
940
+ const projectId = session?.projectId || "default";
941
+ const sanitizedBreadcrumbs = (message.breadcrumbs || []).map((b) => redactSensitiveData(b));
942
+ const sanitizedLastElement = message.lastElement ? redactSensitiveData(message.lastElement) : void 0;
943
+ const networkFailures = sanitizedBreadcrumbs.filter((b) => (b.type === "fetch" || b.type === "xhr") && b.level === "error" && b.data).map((b) => ({
944
+ url: b.message.split(" ")[1] || "",
945
+ method: b.message.split(" ")[0] || "GET",
946
+ status: b.data?.status,
947
+ durationMs: b.data?.durationMs || 0,
948
+ error: b.data?.error,
949
+ aborted: b.data?.aborted,
950
+ timestamp: b.timestamp
951
+ }));
952
+ if (message.eventType === "runtime_error" || message.eventType === "unhandled_rejection") {
953
+ const payload = message.payload;
954
+ return this.handleErrorEvent({
955
+ projectId,
956
+ sessionId: message.sessionId,
957
+ type: payload.errorType || "runtime_exception",
958
+ severity: "error",
959
+ message: payload.message || "Unknown runtime error",
960
+ stack: payload.stack,
961
+ sourceFile: payload.filename,
962
+ line: payload.lineno,
963
+ column: payload.colno,
964
+ route: message.route || "/",
965
+ url: message.url,
966
+ breadcrumbs: sanitizedBreadcrumbs,
967
+ networkFailures,
968
+ lastElement: sanitizedLastElement,
969
+ screenshotDataUrl: message.screenshot
970
+ });
971
+ }
972
+ if (message.eventType === "console") {
973
+ const payload = message.payload;
974
+ if (payload.level === "error") {
975
+ return this.handleErrorEvent({
976
+ projectId,
977
+ sessionId: message.sessionId,
978
+ type: "console_error",
979
+ severity: "error",
980
+ message: payload.message,
981
+ stack: payload.stack,
982
+ route: message.route || "/",
983
+ url: message.url,
984
+ breadcrumbs: sanitizedBreadcrumbs,
985
+ networkFailures,
986
+ lastElement: sanitizedLastElement,
987
+ screenshotDataUrl: message.screenshot
988
+ });
989
+ }
990
+ }
991
+ return null;
992
+ }
993
+ handleErrorEvent(input) {
994
+ const extracted = (!input.sourceFile || !input.line) && input.stack ? extractSourceFromStack(input.stack) : null;
995
+ const rawSource = input.sourceFile || extracted?.file || "unknown_source";
996
+ const sourceFile = normalizeSourceFile(rawSource);
997
+ const line = input.line || extracted?.line || 0;
998
+ const column = input.column || extracted?.column;
999
+ const fingerprint = computeFingerprint({
1000
+ type: input.type,
1001
+ message: input.message,
1002
+ sourceFile,
1003
+ line,
1004
+ column,
1005
+ stack: input.stack
1006
+ });
1007
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1008
+ const existing = this.db.findIncidentByFingerprint(fingerprint);
1009
+ if (existing) {
1010
+ const updatedOccurrences = existing.occurrences + 1;
1011
+ this.db.updateIncidentOccurrence(existing.id, {
1012
+ sessionId: input.sessionId,
1013
+ lastSeen: now,
1014
+ occurrences: updatedOccurrences,
1015
+ route: input.route,
1016
+ breadcrumbs: input.breadcrumbs,
1017
+ lastElement: input.lastElement,
1018
+ stack: input.stack
1019
+ });
1020
+ const occurrenceId = `occ_${crypto.randomUUID().slice(0, 8)}`;
1021
+ this.db.insertIncidentOccurrence({
1022
+ id: occurrenceId,
1023
+ incidentId: existing.id,
1024
+ sessionId: input.sessionId,
1025
+ timestamp: now,
1026
+ route: input.route,
1027
+ url: input.url,
1028
+ stack: input.stack,
1029
+ breadcrumbs: input.breadcrumbs,
1030
+ lastElement: input.lastElement
1031
+ });
1032
+ return {
1033
+ ...existing,
1034
+ occurrences: updatedOccurrences,
1035
+ lastSeen: now,
1036
+ breadcrumbs: input.breadcrumbs,
1037
+ lastElement: input.lastElement || existing.lastElement
1038
+ };
1039
+ }
1040
+ const incidentId = `inc_${crypto.randomUUID().slice(0, 8)}`;
1041
+ let screenshotPath;
1042
+ if (input.screenshotDataUrl) {
1043
+ const saved = this.screenshotStore.saveScreenshot(input.projectId, incidentId, "error", input.screenshotDataUrl);
1044
+ if (saved) {
1045
+ screenshotPath = saved.filePath;
1046
+ }
1047
+ }
1048
+ const newIncident = {
1049
+ id: incidentId,
1050
+ projectId: input.projectId,
1051
+ sessionId: input.sessionId,
1052
+ type: input.type,
1053
+ severity: input.severity,
1054
+ message: input.message,
1055
+ source: {
1056
+ file: sourceFile,
1057
+ line,
1058
+ column
1059
+ },
1060
+ fingerprint,
1061
+ route: input.route,
1062
+ firstSeen: now,
1063
+ lastSeen: now,
1064
+ occurrences: 1,
1065
+ status: "OPEN",
1066
+ stack: input.stack,
1067
+ breadcrumbs: input.breadcrumbs,
1068
+ networkFailures: input.networkFailures,
1069
+ lastElement: input.lastElement,
1070
+ screenshots: screenshotPath ? { error: screenshotPath } : void 0
1071
+ };
1072
+ this.db.insertIncident(newIncident);
1073
+ this.db.insertIncidentOccurrence({
1074
+ id: `occ_${crypto.randomUUID().slice(0, 8)}`,
1075
+ incidentId,
1076
+ sessionId: input.sessionId,
1077
+ timestamp: now,
1078
+ route: input.route,
1079
+ url: input.url,
1080
+ stack: input.stack,
1081
+ breadcrumbs: input.breadcrumbs,
1082
+ lastElement: input.lastElement
1083
+ });
1084
+ return newIncident;
1085
+ }
1086
+ };
1087
+
1088
+ // packages/daemon/src/notes/engine.ts
1089
+ import crypto2 from "crypto";
1090
+ import path4 from "path";
1091
+ import fs3 from "fs";
1092
+ var NotesEngine = class {
1093
+ db;
1094
+ screenshotStore;
1095
+ constructor(db, screenshotStore) {
1096
+ this.db = db;
1097
+ this.screenshotStore = screenshotStore;
1098
+ }
1099
+ createNoteFromClient(payload) {
1100
+ const session = this.db.getSession(payload.sessionId);
1101
+ const projectId = session?.projectId || "default";
1102
+ const noteId = `note_${crypto2.randomUUID().slice(0, 8)}`;
1103
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1104
+ let screenshotPath;
1105
+ if (payload.screenshot) {
1106
+ const saved = this.saveNoteScreenshot(projectId, noteId, "original", payload.screenshot);
1107
+ if (saved) {
1108
+ screenshotPath = saved;
1109
+ }
1110
+ }
1111
+ const note = {
1112
+ id: noteId,
1113
+ projectId,
1114
+ sessionId: payload.sessionId,
1115
+ type: payload.noteType || "element",
1116
+ message: payload.message,
1117
+ route: payload.route || "/",
1118
+ url: payload.url || "",
1119
+ viewport: payload.viewport || { width: 1280, height: 800, devicePixelRatio: 1 },
1120
+ scroll: payload.scroll || { scrollX: 0, scrollY: 0 },
1121
+ target: payload.target,
1122
+ elementContext: payload.elementContext,
1123
+ region: payload.region,
1124
+ status: "OPEN",
1125
+ incidentId: payload.incidentId,
1126
+ screenshots: screenshotPath ? { original: screenshotPath } : void 0,
1127
+ createdAt: now,
1128
+ updatedAt: now
1129
+ };
1130
+ this.db.insertNote(note);
1131
+ return note;
1132
+ }
1133
+ saveNoteScreenshot(projectId, noteId, name, dataUrl) {
1134
+ try {
1135
+ if (!dataUrl || !dataUrl.startsWith("data:image/")) return null;
1136
+ const match = dataUrl.match(/^data:image\/([a-zA-Z0-9+]+);base64,(.+)$/);
1137
+ if (!match) return null;
1138
+ let format = match[1].toLowerCase();
1139
+ if (format === "jpeg") format = "jpg";
1140
+ const base64Data = match[2];
1141
+ const buffer = Buffer.from(base64Data, "base64");
1142
+ const targetDir = path4.join(this.screenshotStore.baseDir, projectId || "default", "notes", noteId);
1143
+ fs3.mkdirSync(targetDir, { recursive: true });
1144
+ const fileName = `${name}.${format}`;
1145
+ const filePath = path4.join(targetDir, fileName);
1146
+ fs3.writeFileSync(filePath, buffer);
1147
+ return filePath;
1148
+ } catch {
1149
+ return null;
1150
+ }
1151
+ }
1152
+ };
1153
+
1154
+ // packages/daemon/src/notes/verification.ts
1155
+ import crypto3 from "crypto";
1156
+ var NoteVerificationEngine = class {
1157
+ db;
1158
+ sessionManager;
1159
+ notesEngine;
1160
+ constructor(db, sessionManager, notesEngine) {
1161
+ this.db = db;
1162
+ this.sessionManager = sessionManager;
1163
+ this.notesEngine = notesEngine;
1164
+ }
1165
+ async verifyNote(noteId, options = {}) {
1166
+ const note = this.db.getNote(noteId);
1167
+ if (!note) {
1168
+ return {
1169
+ noteId,
1170
+ status: "FAILED",
1171
+ checks: [{ type: "note_exists", passed: false, details: `Note ${noteId} not found` }],
1172
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1173
+ message: `Note ${noteId} not found in database`
1174
+ };
1175
+ }
1176
+ let session = this.sessionManager.getActiveSessionForProject(note.projectId);
1177
+ if (!session) {
1178
+ session = this.sessionManager.getAnyActiveSession();
1179
+ }
1180
+ if (!session) {
1181
+ const result = {
1182
+ noteId,
1183
+ status: "INCONCLUSIVE",
1184
+ checks: [],
1185
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1186
+ message: "No active browser session currently connected to verify visual note."
1187
+ };
1188
+ this.recordNoteVerification(result, note);
1189
+ return result;
1190
+ }
1191
+ this.db.updateNoteStatus(noteId, "VERIFYING");
1192
+ const pageStateCmd = await this.sessionManager.sendCommand(session.id, {
1193
+ id: `cmd_${crypto3.randomUUID().slice(0, 8)}`,
1194
+ type: "get_page_state"
1195
+ });
1196
+ const currentRoute = pageStateCmd.result?.route || "/";
1197
+ const isRouteMatch = currentRoute === note.route || currentRoute.startsWith(note.route);
1198
+ if (!isRouteMatch && note.route) {
1199
+ await this.sessionManager.sendCommand(session.id, {
1200
+ id: `cmd_${crypto3.randomUUID().slice(0, 8)}`,
1201
+ type: "navigate",
1202
+ params: { url: note.route }
1203
+ });
1204
+ await new Promise((r) => setTimeout(r, options.observationWindowMs || 1e3));
1205
+ }
1206
+ const checks = [];
1207
+ checks.push({
1208
+ type: "route_loaded",
1209
+ passed: true,
1210
+ details: `Route ${note.route || "/"} loaded`
1211
+ });
1212
+ const targetSelector = note.target?.selector || (note.type === "page" ? "body" : "body");
1213
+ let elementExists = true;
1214
+ let elementVisible = true;
1215
+ let currentRect;
1216
+ let overflowResult;
1217
+ if (note.type === "element" && targetSelector) {
1218
+ const queryCmd = await this.sessionManager.sendCommand(session.id, {
1219
+ id: `cmd_${crypto3.randomUUID().slice(0, 8)}`,
1220
+ type: "query_element",
1221
+ params: { selector: targetSelector }
1222
+ });
1223
+ elementExists = queryCmd.ok && !!queryCmd.result?.exists;
1224
+ elementVisible = queryCmd.ok && !!queryCmd.result?.visible;
1225
+ currentRect = queryCmd.result?.boundingRect;
1226
+ checks.push({
1227
+ type: "element_exists",
1228
+ passed: elementExists,
1229
+ details: elementExists ? `Element ${targetSelector} exists in DOM` : `Element ${targetSelector} not found`
1230
+ });
1231
+ checks.push({
1232
+ type: "element_visible",
1233
+ passed: elementVisible,
1234
+ details: elementVisible ? `Element ${targetSelector} is visible` : `Element ${targetSelector} is hidden`
1235
+ });
1236
+ const overflowCmd = await this.sessionManager.sendCommand(session.id, {
1237
+ id: `cmd_${crypto3.randomUUID().slice(0, 8)}`,
1238
+ type: "check_overflow",
1239
+ params: { selector: targetSelector }
1240
+ });
1241
+ if (overflowCmd.ok && overflowCmd.result) {
1242
+ const ovf = overflowCmd.result;
1243
+ overflowResult = ovf;
1244
+ const isOverflowingNow = !!ovf.overflow;
1245
+ checks.push({
1246
+ type: "no_viewport_overflow",
1247
+ passed: !isOverflowingNow,
1248
+ details: !isOverflowingNow ? `Element fits within viewport (width: ${ovf.viewportWidth}px, rect right: ${ovf.rect?.right}px)` : `Element overflows viewport by ${ovf.overflowRightPx}px (rect right: ${ovf.rect?.right}px, viewport: ${ovf.viewportWidth}px)`
1249
+ });
1250
+ }
1251
+ }
1252
+ let afterScreenshotPath;
1253
+ const captureCmd = await this.sessionManager.sendCommand(session.id, {
1254
+ id: `cmd_${crypto3.randomUUID().slice(0, 8)}`,
1255
+ type: "capture_element",
1256
+ params: { selector: targetSelector }
1257
+ });
1258
+ if (captureCmd.ok && captureCmd.result?.dataUrl) {
1259
+ const saved = this.notesEngine.saveNoteScreenshot(note.projectId, note.id, "after", captureCmd.result.dataUrl);
1260
+ if (saved) {
1261
+ afterScreenshotPath = saved;
1262
+ }
1263
+ }
1264
+ const beforeRect = note.target?.boundingRect;
1265
+ const overflowFixed = overflowResult ? !overflowResult.overflow : void 0;
1266
+ const geometryDiff = {
1267
+ before: beforeRect,
1268
+ current: currentRect || overflowResult?.rect,
1269
+ viewportWidth: overflowResult?.viewportWidth || note.viewport.width,
1270
+ overflowFixed,
1271
+ overflowPx: overflowResult?.overflowRightPx || 0
1272
+ };
1273
+ const anyFailed = checks.some((c) => !c.passed);
1274
+ let status = "VERIFIED";
1275
+ if (anyFailed) {
1276
+ status = "FAILED";
1277
+ } else {
1278
+ status = "VERIFIED";
1279
+ }
1280
+ const verificationResult = {
1281
+ noteId,
1282
+ status,
1283
+ checks,
1284
+ geometryDiff,
1285
+ screenshots: {
1286
+ before: note.screenshots?.original,
1287
+ after: afterScreenshotPath
1288
+ },
1289
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1290
+ message: status === "VERIFIED" ? "Visual note verified: target element is present, visible, and layout checks (including viewport overflow) passed." : "Visual note verification failed: target element missing, hidden, or still overflowing."
1291
+ };
1292
+ this.recordNoteVerification(verificationResult, note);
1293
+ return verificationResult;
1294
+ }
1295
+ recordNoteVerification(result, note) {
1296
+ this.db.updateNoteStatus(note.id, result.status);
1297
+ this.db.insertNoteVerification(result);
1298
+ }
1299
+ };
1300
+
1301
+ // packages/daemon/src/verification/engine.ts
1302
+ import crypto4 from "crypto";
1303
+ var VerificationEngine = class {
1304
+ db;
1305
+ sessionManager;
1306
+ screenshotStore;
1307
+ constructor(db, sessionManager, screenshotStore) {
1308
+ this.db = db;
1309
+ this.sessionManager = sessionManager;
1310
+ this.screenshotStore = screenshotStore;
1311
+ }
1312
+ async verifyIncident(incidentId, recipe) {
1313
+ const incident = this.db.getIncident(incidentId);
1314
+ if (!incident) {
1315
+ return {
1316
+ incidentId,
1317
+ status: "FAILED",
1318
+ checks: [{ type: "no_incident", passed: false, details: `Incident ${incidentId} not found` }],
1319
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1320
+ message: `Incident ${incidentId} not found in database`
1321
+ };
1322
+ }
1323
+ let session = this.sessionManager.getActiveSessionForProject(incident.projectId);
1324
+ if (!session) {
1325
+ session = this.sessionManager.getAnyActiveSession();
1326
+ }
1327
+ if (!session) {
1328
+ const result = {
1329
+ incidentId,
1330
+ status: "INCONCLUSIVE",
1331
+ checks: [],
1332
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1333
+ message: "No active browser session currently connected to verify the fix."
1334
+ };
1335
+ this.recordVerification(result, incident);
1336
+ return result;
1337
+ }
1338
+ this.db.updateIncidentStatus(incidentId, "VERIFYING");
1339
+ const baselineOccurrences = incident.occurrences;
1340
+ const targetRoute = recipe?.route || incident.route;
1341
+ const observationMs = recipe?.observationWindowMs || 2e3;
1342
+ if (recipe?.route && recipe.route !== incident.route) {
1343
+ await this.sessionManager.sendCommand(session.id, {
1344
+ id: `cmd_${crypto4.randomUUID().slice(0, 8)}`,
1345
+ type: "navigate",
1346
+ params: { url: recipe.route }
1347
+ });
1348
+ } else {
1349
+ await this.sessionManager.sendCommand(session.id, {
1350
+ id: `cmd_${crypto4.randomUUID().slice(0, 8)}`,
1351
+ type: "reload",
1352
+ params: { force: true }
1353
+ });
1354
+ }
1355
+ await new Promise((resolve) => setTimeout(resolve, observationMs));
1356
+ const freshIncident = this.db.getIncident(incidentId);
1357
+ const hasReoccurred = freshIncident ? freshIncident.occurrences > baselineOccurrences : false;
1358
+ const checks = [];
1359
+ checks.push({
1360
+ type: "no_incident",
1361
+ passed: !hasReoccurred,
1362
+ details: hasReoccurred ? `Incident reoccurred (${freshIncident?.occurrences} occurrences vs baseline ${baselineOccurrences})` : "No recurring error observed during verification window"
1363
+ });
1364
+ const probes = recipe?.expect || [];
1365
+ for (const probe of probes) {
1366
+ const probeRes = await this.evaluateProbe(session.id, probe);
1367
+ checks.push(probeRes);
1368
+ }
1369
+ let afterScreenshotPath;
1370
+ const targetSelector = recipe?.targetSelector || incident.lastElement?.selector;
1371
+ if (targetSelector) {
1372
+ const captureCmd = await this.sessionManager.sendCommand(session.id, {
1373
+ id: `cmd_${crypto4.randomUUID().slice(0, 8)}`,
1374
+ type: "capture_element",
1375
+ params: { selector: targetSelector }
1376
+ });
1377
+ if (captureCmd.ok && captureCmd.result?.dataUrl) {
1378
+ const saved = this.screenshotStore.saveScreenshot(
1379
+ incident.projectId,
1380
+ incident.id,
1381
+ "verified",
1382
+ captureCmd.result.dataUrl
1383
+ );
1384
+ if (saved) {
1385
+ afterScreenshotPath = saved.filePath;
1386
+ }
1387
+ }
1388
+ }
1389
+ let verdict = "VERIFIED";
1390
+ const anyCheckFailed = checks.some((c) => !c.passed);
1391
+ if (anyCheckFailed || hasReoccurred) {
1392
+ verdict = "FAILED";
1393
+ } else {
1394
+ const wasInteractionError = incident.breadcrumbs.some((b) => b.type === "click" || b.type === "submit");
1395
+ if (wasInteractionError && probes.length === 0) {
1396
+ verdict = "INCONCLUSIVE";
1397
+ }
1398
+ }
1399
+ const verificationResult = {
1400
+ incidentId,
1401
+ status: verdict,
1402
+ checks,
1403
+ screenshots: {
1404
+ before: incident.screenshots?.error,
1405
+ after: afterScreenshotPath
1406
+ },
1407
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1408
+ message: verdict === "VERIFIED" ? "Fix verified: Error did not reoccur and all checks passed." : verdict === "FAILED" ? "Verification failed: Error reoccurred or probe expectation was not met." : "Verification inconclusive: Error did not appear on reload, but specific interaction may need manual verification or custom probes."
1409
+ };
1410
+ this.recordVerification(verificationResult, incident);
1411
+ return verificationResult;
1412
+ }
1413
+ async evaluateProbe(sessionId, probe) {
1414
+ try {
1415
+ switch (probe.type) {
1416
+ case "element_exists": {
1417
+ if (!probe.selector) {
1418
+ return { type: probe.type, passed: false, details: "Missing selector in probe" };
1419
+ }
1420
+ const res = await this.sessionManager.sendCommand(sessionId, {
1421
+ id: `cmd_${crypto4.randomUUID().slice(0, 8)}`,
1422
+ type: "query_element",
1423
+ params: { selector: probe.selector }
1424
+ });
1425
+ const exists = res.ok && !!res.result?.exists;
1426
+ return {
1427
+ type: probe.type,
1428
+ passed: exists,
1429
+ details: exists ? `Element ${probe.selector} exists in DOM` : `Element ${probe.selector} does not exist`
1430
+ };
1431
+ }
1432
+ case "element_visible": {
1433
+ if (!probe.selector) {
1434
+ return { type: probe.type, passed: false, details: "Missing selector in probe" };
1435
+ }
1436
+ const res = await this.sessionManager.sendCommand(sessionId, {
1437
+ id: `cmd_${crypto4.randomUUID().slice(0, 8)}`,
1438
+ type: "query_element",
1439
+ params: { selector: probe.selector }
1440
+ });
1441
+ const visible = res.ok && !!res.result?.exists && !!res.result?.visible;
1442
+ return {
1443
+ type: probe.type,
1444
+ passed: visible,
1445
+ details: visible ? `Element ${probe.selector} is visible` : `Element ${probe.selector} is not visible`
1446
+ };
1447
+ }
1448
+ case "text_contains": {
1449
+ if (!probe.selector || !probe.text) {
1450
+ return { type: probe.type, passed: false, details: "Missing selector or text in probe" };
1451
+ }
1452
+ const res = await this.sessionManager.sendCommand(sessionId, {
1453
+ id: `cmd_${crypto4.randomUUID().slice(0, 8)}`,
1454
+ type: "query_element",
1455
+ params: { selector: probe.selector }
1456
+ });
1457
+ const innerText = res.result?.innerText || "";
1458
+ const contains = res.ok && innerText.includes(probe.text);
1459
+ return {
1460
+ type: probe.type,
1461
+ passed: contains,
1462
+ details: contains ? `Element ${probe.selector} contains text "${probe.text}"` : `Element text "${innerText}" did not contain "${probe.text}"`
1463
+ };
1464
+ }
1465
+ case "route_is": {
1466
+ if (!probe.route) {
1467
+ return { type: probe.type, passed: false, details: "Missing route in probe" };
1468
+ }
1469
+ const res = await this.sessionManager.sendCommand(sessionId, {
1470
+ id: `cmd_${crypto4.randomUUID().slice(0, 8)}`,
1471
+ type: "get_page_state"
1472
+ });
1473
+ const currentRoute = res.result?.route || "";
1474
+ const matches = res.ok && (currentRoute === probe.route || currentRoute.startsWith(probe.route));
1475
+ return {
1476
+ type: probe.type,
1477
+ passed: matches,
1478
+ details: matches ? `Current route matches ${probe.route}` : `Current route is "${currentRoute}", expected "${probe.route}"`
1479
+ };
1480
+ }
1481
+ default:
1482
+ return {
1483
+ type: probe.type,
1484
+ passed: true,
1485
+ details: `Probe ${probe.type} evaluated`
1486
+ };
1487
+ }
1488
+ } catch (err) {
1489
+ return {
1490
+ type: probe.type,
1491
+ passed: false,
1492
+ details: err?.message || "Probe execution error"
1493
+ };
1494
+ }
1495
+ }
1496
+ recordVerification(result, incident) {
1497
+ this.db.updateIncidentStatus(incident.id, result.status);
1498
+ this.db.insertVerification({
1499
+ id: `ver_${crypto4.randomUUID().slice(0, 8)}`,
1500
+ incidentId: incident.id,
1501
+ status: result.status,
1502
+ checks: result.checks,
1503
+ beforeScreenshot: result.screenshots?.before,
1504
+ afterScreenshot: result.screenshots?.after,
1505
+ message: result.message,
1506
+ createdAt: result.timestamp
1507
+ });
1508
+ }
1509
+ };
1510
+
1511
+ // packages/daemon/src/server/http.ts
1512
+ import fs4 from "fs";
1513
+ import path5 from "path";
1514
+ import { fileURLToPath } from "url";
1515
+ function createHttpHandler(db, sessionManager, baseScreenshotsDir) {
1516
+ return (req, res) => {
1517
+ const parsedUrl = new URL(req.url || "/", `http://${req.headers.host || "127.0.0.1"}`);
1518
+ const pathname = parsedUrl.pathname;
1519
+ res.setHeader("Access-Control-Allow-Origin", "*");
1520
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
1521
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
1522
+ if (req.method === "OPTIONS") {
1523
+ res.writeHead(204);
1524
+ res.end();
1525
+ return;
1526
+ }
1527
+ if (pathname === "/health" || pathname === "/") {
1528
+ res.writeHead(200, { "Content-Type": "application/json" });
1529
+ res.end(
1530
+ JSON.stringify({
1531
+ status: "ok",
1532
+ name: "browsertrack",
1533
+ version: "0.1.0",
1534
+ activeSessions: sessionManager.getActiveCount(),
1535
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1536
+ })
1537
+ );
1538
+ return;
1539
+ }
1540
+ if (pathname === "/client.js" || pathname === "/browserdiag.js") {
1541
+ let scriptContent = "";
1542
+ try {
1543
+ const __filename = fileURLToPath(import.meta.url);
1544
+ const __dirname = path5.dirname(__filename);
1545
+ const candidatePaths = [
1546
+ path5.resolve(__dirname, "../client.iife.js"),
1547
+ path5.resolve(__dirname, "../../dist/client.iife.js"),
1548
+ path5.resolve(__dirname, "../../../dist/client.iife.js")
1549
+ ];
1550
+ for (const p of candidatePaths) {
1551
+ if (fs4.existsSync(p)) {
1552
+ scriptContent = fs4.readFileSync(p, "utf-8");
1553
+ break;
1554
+ }
1555
+ }
1556
+ } catch {
1557
+ }
1558
+ if (!scriptContent) {
1559
+ scriptContent = `console.warn("[BrowserTrack] Standalone client bundle not built yet. Run 'npm run build'.");`;
1560
+ }
1561
+ res.writeHead(200, {
1562
+ "Content-Type": "application/javascript; charset=utf-8",
1563
+ "Cache-Control": "no-cache"
1564
+ });
1565
+ res.end(scriptContent);
1566
+ return;
1567
+ }
1568
+ if (pathname === "/api/projects") {
1569
+ const projects = db.listProjects();
1570
+ res.writeHead(200, { "Content-Type": "application/json" });
1571
+ res.end(JSON.stringify({ ok: true, projects }));
1572
+ return;
1573
+ }
1574
+ if (pathname === "/api/sessions") {
1575
+ const projectId = parsedUrl.searchParams.get("project") || void 0;
1576
+ const activeOnly = parsedUrl.searchParams.get("active") === "true";
1577
+ const sessions = db.listSessions(projectId, activeOnly);
1578
+ res.writeHead(200, { "Content-Type": "application/json" });
1579
+ res.end(JSON.stringify({ ok: true, sessions }));
1580
+ return;
1581
+ }
1582
+ if (pathname === "/api/incidents") {
1583
+ const projectId = parsedUrl.searchParams.get("project") || void 0;
1584
+ const status = parsedUrl.searchParams.get("status") || void 0;
1585
+ const limit = parseInt(parsedUrl.searchParams.get("limit") || "50", 10);
1586
+ const incidents = db.listIncidents({ projectId, status, limit });
1587
+ res.writeHead(200, { "Content-Type": "application/json" });
1588
+ res.end(JSON.stringify({ ok: true, incidents }));
1589
+ return;
1590
+ }
1591
+ if (pathname.startsWith("/api/incidents/")) {
1592
+ const incidentId = pathname.replace("/api/incidents/", "");
1593
+ const incident = db.getIncident(incidentId);
1594
+ if (!incident) {
1595
+ res.writeHead(404, { "Content-Type": "application/json" });
1596
+ res.end(JSON.stringify({ ok: false, error: "Incident not found" }));
1597
+ return;
1598
+ }
1599
+ res.writeHead(200, { "Content-Type": "application/json" });
1600
+ res.end(JSON.stringify({ ok: true, incident }));
1601
+ return;
1602
+ }
1603
+ if (pathname === "/api/notes") {
1604
+ const projectId = parsedUrl.searchParams.get("project") || void 0;
1605
+ const status = parsedUrl.searchParams.get("status") || void 0;
1606
+ const limit = parseInt(parsedUrl.searchParams.get("limit") || "50", 10);
1607
+ const notes = db.listNotes({ projectId, status, limit });
1608
+ res.writeHead(200, { "Content-Type": "application/json" });
1609
+ res.end(JSON.stringify({ ok: true, notes }));
1610
+ return;
1611
+ }
1612
+ if (pathname.startsWith("/api/notes/")) {
1613
+ const noteId = pathname.replace("/api/notes/", "");
1614
+ const note = db.getNote(noteId);
1615
+ if (!note) {
1616
+ res.writeHead(404, { "Content-Type": "application/json" });
1617
+ res.end(JSON.stringify({ ok: false, error: "Note not found" }));
1618
+ return;
1619
+ }
1620
+ res.writeHead(200, { "Content-Type": "application/json" });
1621
+ res.end(JSON.stringify({ ok: true, note }));
1622
+ return;
1623
+ }
1624
+ if (pathname.startsWith("/screenshots/")) {
1625
+ const relativePath = pathname.replace("/screenshots/", "");
1626
+ const filePath = path5.join(baseScreenshotsDir, relativePath);
1627
+ if (fs4.existsSync(filePath) && fs4.statSync(filePath).isFile()) {
1628
+ const ext = path5.extname(filePath).toLowerCase();
1629
+ const mimeTypes = {
1630
+ ".webp": "image/webp",
1631
+ ".png": "image/png",
1632
+ ".jpg": "image/jpeg",
1633
+ ".jpeg": "image/jpeg"
1634
+ };
1635
+ res.writeHead(200, { "Content-Type": mimeTypes[ext] || "application/octet-stream" });
1636
+ fs4.createReadStream(filePath).pipe(res);
1637
+ return;
1638
+ }
1639
+ res.writeHead(404, { "Content-Type": "application/json" });
1640
+ res.end(JSON.stringify({ ok: false, error: "Screenshot not found" }));
1641
+ return;
1642
+ }
1643
+ res.writeHead(404, { "Content-Type": "application/json" });
1644
+ res.end(JSON.stringify({ ok: false, error: "Not found" }));
1645
+ };
1646
+ }
1647
+
1648
+ // packages/daemon/src/server/ws.ts
1649
+ import crypto5 from "crypto";
1650
+ function setupWebSocketServer(wss, db, sessionManager, incidentEngine, notesEngine, maxEventsPerSession = 1e3, verbose = false) {
1651
+ wss.on("connection", (ws) => {
1652
+ let currentSessionId = null;
1653
+ ws.on("message", (raw) => {
1654
+ try {
1655
+ const text = typeof raw === "string" ? raw : raw.toString("utf-8");
1656
+ const data = JSON.parse(text);
1657
+ if (data.type === "hello") {
1658
+ const hello = data;
1659
+ const sessionId = `sess_${crypto5.randomUUID().slice(0, 8)}`;
1660
+ currentSessionId = sessionId;
1661
+ let project = hello.projectId ? db.getProject(hello.projectId) : null;
1662
+ if (!project && hello.origin) {
1663
+ project = db.getProjectByOrigin(hello.origin);
1664
+ }
1665
+ if (!project) {
1666
+ let projName = "default";
1667
+ try {
1668
+ const url = new URL(hello.origin || "http://localhost");
1669
+ projName = url.port ? `app-${url.port}` : url.hostname;
1670
+ } catch {
1671
+ projName = "app";
1672
+ }
1673
+ project = db.upsertProject({
1674
+ id: `proj_${crypto5.randomUUID().slice(0, 8)}`,
1675
+ name: projName,
1676
+ origin: hello.origin || "http://localhost"
1677
+ });
1678
+ }
1679
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1680
+ db.upsertSession({
1681
+ id: sessionId,
1682
+ projectId: project.id,
1683
+ origin: hello.origin || "",
1684
+ url: hello.url || "",
1685
+ title: hello.title || "",
1686
+ userAgent: hello.userAgent || "",
1687
+ connectedAt: now,
1688
+ lastSeenAt: now,
1689
+ active: true
1690
+ });
1691
+ sessionManager.registerSocket(sessionId, ws, hello.origin || "", project.id);
1692
+ ws.send(
1693
+ JSON.stringify({
1694
+ type: "hello_ack",
1695
+ sessionId,
1696
+ projectId: project.id,
1697
+ projectName: project.name
1698
+ })
1699
+ );
1700
+ if (verbose) {
1701
+ console.log(`[BrowserTrack] New session connected: ${sessionId} (${project.name} @ ${hello.origin})`);
1702
+ }
1703
+ return;
1704
+ }
1705
+ if (data.type === "event") {
1706
+ const eventMsg = data;
1707
+ const sessionId = eventMsg.sessionId || currentSessionId;
1708
+ if (!sessionId) return;
1709
+ const eventId = `evt_${crypto5.randomUUID().slice(0, 8)}`;
1710
+ db.insertEvent({
1711
+ id: eventId,
1712
+ sessionId,
1713
+ eventType: eventMsg.eventType,
1714
+ payload: eventMsg.payload,
1715
+ timestamp: eventMsg.timestamp || Date.now(),
1716
+ route: eventMsg.route,
1717
+ url: eventMsg.url
1718
+ });
1719
+ db.pruneSessionEvents(sessionId, maxEventsPerSession);
1720
+ const incident = incidentEngine.processClientEvent(eventMsg);
1721
+ if (incident && verbose) {
1722
+ console.log(
1723
+ `[BrowserTrack] Incident recorded: ${incident.id} (${incident.type}: ${incident.message}) [${incident.occurrences}x]`
1724
+ );
1725
+ }
1726
+ return;
1727
+ }
1728
+ if (data.type === "create_note" && notesEngine) {
1729
+ const note = notesEngine.createNoteFromClient({
1730
+ sessionId: data.sessionId || currentSessionId || "",
1731
+ noteType: data.noteType,
1732
+ message: data.message,
1733
+ route: data.route,
1734
+ url: data.url,
1735
+ viewport: data.viewport,
1736
+ scroll: data.scroll,
1737
+ target: data.target,
1738
+ elementContext: data.elementContext,
1739
+ region: data.region,
1740
+ screenshot: data.screenshot,
1741
+ incidentId: data.incidentId
1742
+ });
1743
+ if (verbose) {
1744
+ console.log(`[BrowserTrack] Visual note created: ${note.id} on ${note.route} ("${note.message}")`);
1745
+ }
1746
+ ws.send(
1747
+ JSON.stringify({
1748
+ type: "note_created_ack",
1749
+ noteId: note.id,
1750
+ status: note.status
1751
+ })
1752
+ );
1753
+ return;
1754
+ }
1755
+ if (data.type === "command_response") {
1756
+ const res = data.response;
1757
+ const sessionId = data.sessionId || currentSessionId;
1758
+ if (sessionId && res) {
1759
+ sessionManager.handleCommandResponse(sessionId, res);
1760
+ }
1761
+ return;
1762
+ }
1763
+ } catch (err) {
1764
+ if (verbose) {
1765
+ console.error("[BrowserTrack] WebSocket message error:", err?.message);
1766
+ }
1767
+ }
1768
+ });
1769
+ ws.on("close", () => {
1770
+ if (currentSessionId) {
1771
+ sessionManager.unregisterSocket(currentSessionId);
1772
+ if (verbose) {
1773
+ console.log(`[BrowserTrack] Session disconnected: ${currentSessionId}`);
1774
+ }
1775
+ }
1776
+ });
1777
+ ws.on("error", () => {
1778
+ if (currentSessionId) {
1779
+ sessionManager.unregisterSocket(currentSessionId);
1780
+ }
1781
+ });
1782
+ });
1783
+ }
1784
+
1785
+ // packages/daemon/src/server/daemon.ts
1786
+ import http from "http";
1787
+ import { WebSocketServer } from "ws";
1788
+ var BrowserTrackDaemon = class {
1789
+ config;
1790
+ db;
1791
+ screenshotStore;
1792
+ sessionManager;
1793
+ incidentEngine;
1794
+ notesEngine;
1795
+ verificationEngine;
1796
+ noteVerificationEngine;
1797
+ httpServer = null;
1798
+ wss = null;
1799
+ isRunning = false;
1800
+ constructor(config = {}) {
1801
+ this.config = getDaemonConfig(config);
1802
+ this.db = new StorageDB(this.config.dbPath);
1803
+ this.screenshotStore = new ScreenshotStore(this.config.screenshotsDir);
1804
+ this.sessionManager = new SessionManager(this.db);
1805
+ this.incidentEngine = new IncidentEngine(this.db, this.screenshotStore);
1806
+ this.notesEngine = new NotesEngine(this.db, this.screenshotStore);
1807
+ this.verificationEngine = new VerificationEngine(this.db, this.sessionManager, this.screenshotStore);
1808
+ this.noteVerificationEngine = new NoteVerificationEngine(this.db, this.sessionManager, this.notesEngine);
1809
+ }
1810
+ async start() {
1811
+ if (this.isRunning) return;
1812
+ const httpHandler = createHttpHandler(this.db, this.sessionManager, this.config.screenshotsDir);
1813
+ this.httpServer = http.createServer(httpHandler);
1814
+ this.wss = new WebSocketServer({ server: this.httpServer });
1815
+ setupWebSocketServer(
1816
+ this.wss,
1817
+ this.db,
1818
+ this.sessionManager,
1819
+ this.incidentEngine,
1820
+ this.notesEngine,
1821
+ this.config.maxEventsPerSession,
1822
+ this.config.verbose
1823
+ );
1824
+ await new Promise((resolve, reject) => {
1825
+ this.httpServer.listen(this.config.port, this.config.host, () => {
1826
+ this.isRunning = true;
1827
+ if (this.config.verbose) {
1828
+ console.log(`[BrowserTrack] Daemon running at http://${this.config.host}:${this.config.port}`);
1829
+ }
1830
+ resolve();
1831
+ });
1832
+ this.httpServer.once("error", (err) => {
1833
+ reject(err);
1834
+ });
1835
+ });
1836
+ }
1837
+ async stop() {
1838
+ if (!this.isRunning) return;
1839
+ if (this.wss) {
1840
+ for (const client of this.wss.clients) {
1841
+ try {
1842
+ client.terminate();
1843
+ } catch {
1844
+ }
1845
+ }
1846
+ await new Promise((resolve) => {
1847
+ this.wss.close(() => resolve());
1848
+ });
1849
+ this.wss = null;
1850
+ }
1851
+ if (this.httpServer) {
1852
+ if (typeof this.httpServer.closeAllConnections === "function") {
1853
+ this.httpServer.closeAllConnections();
1854
+ }
1855
+ await new Promise((resolve) => {
1856
+ this.httpServer.close(() => resolve());
1857
+ });
1858
+ this.httpServer = null;
1859
+ }
1860
+ try {
1861
+ this.db.close();
1862
+ } catch {
1863
+ }
1864
+ this.isRunning = false;
1865
+ }
1866
+ getStatus() {
1867
+ return {
1868
+ isRunning: this.isRunning,
1869
+ host: this.config.host,
1870
+ port: this.config.port,
1871
+ activeSessions: this.sessionManager.getActiveCount(),
1872
+ dbPath: this.config.dbPath
1873
+ };
1874
+ }
1875
+ };
1876
+
1877
+ // packages/daemon/src/index.ts
1878
+ function createDaemon(config = {}) {
1879
+ return new BrowserTrackDaemon(config);
1880
+ }
1881
+
1882
+ // packages/mcp/src/server.ts
1883
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
1884
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
1885
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
1886
+
1887
+ // packages/mcp/src/handlers.ts
1888
+ async function handleToolCall(name, args, ctx) {
1889
+ const { db, sessionManager, verificationEngine, noteVerificationEngine } = ctx;
1890
+ switch (name) {
1891
+ case "list_projects": {
1892
+ const projects = db.listProjects();
1893
+ return {
1894
+ projects: projects.map((p) => ({
1895
+ id: p.id,
1896
+ name: p.name,
1897
+ origin: p.origin,
1898
+ path: p.path,
1899
+ updatedAt: p.updatedAt
1900
+ }))
1901
+ };
1902
+ }
1903
+ case "list_sessions": {
1904
+ const activeOnly = args.activeOnly !== false;
1905
+ const sessions = db.listSessions(args.projectId, activeOnly);
1906
+ return {
1907
+ count: sessions.length,
1908
+ sessions: sessions.map((s) => ({
1909
+ id: s.id,
1910
+ projectId: s.projectId,
1911
+ origin: s.origin,
1912
+ url: s.url,
1913
+ title: s.title,
1914
+ active: s.active,
1915
+ lastSeenAt: s.lastSeenAt
1916
+ }))
1917
+ };
1918
+ }
1919
+ case "list_incidents": {
1920
+ const incidents = db.listIncidents({
1921
+ projectId: args.projectId,
1922
+ status: args.status,
1923
+ severity: args.severity,
1924
+ limit: args.limit || 20
1925
+ });
1926
+ return {
1927
+ total: incidents.length,
1928
+ incidents: incidents.map((inc) => ({
1929
+ id: inc.id,
1930
+ type: inc.type,
1931
+ severity: inc.severity,
1932
+ message: inc.message,
1933
+ source: `${inc.source.file}:${inc.source.line}`,
1934
+ route: inc.route,
1935
+ status: inc.status,
1936
+ occurrences: inc.occurrences,
1937
+ firstSeen: inc.firstSeen,
1938
+ lastSeen: inc.lastSeen
1939
+ }))
1940
+ };
1941
+ }
1942
+ case "get_incident": {
1943
+ const incident = db.getIncident(args.incidentId);
1944
+ if (!incident) {
1945
+ throw new Error(`Incident '${args.incidentId}' not found.`);
1946
+ }
1947
+ const breadcrumbsTimeline = incident.breadcrumbs.slice(-15).map((b) => {
1948
+ const time = new Date(b.timestamp).toISOString().split("T")[1]?.slice(0, 8);
1949
+ return `[${time}] ${b.message}`;
1950
+ });
1951
+ return {
1952
+ id: incident.id,
1953
+ status: incident.status,
1954
+ type: incident.type,
1955
+ severity: incident.severity,
1956
+ message: incident.message,
1957
+ source: {
1958
+ file: incident.source.file,
1959
+ line: incident.source.line,
1960
+ column: incident.source.column
1961
+ },
1962
+ route: incident.route,
1963
+ occurrences: incident.occurrences,
1964
+ firstSeen: incident.firstSeen,
1965
+ lastSeen: incident.lastSeen,
1966
+ stack: incident.stack,
1967
+ lastInteractedElement: incident.lastElement ? {
1968
+ selector: incident.lastElement.selector,
1969
+ tag: incident.lastElement.tag,
1970
+ visible: incident.lastElement.visible,
1971
+ innerText: incident.lastElement.innerText,
1972
+ outerHTML: incident.lastElement.outerHTML
1973
+ } : void 0,
1974
+ recentBreadcrumbs: breadcrumbsTimeline,
1975
+ networkFailures: incident.networkFailures,
1976
+ screenshot: incident.screenshots?.error
1977
+ };
1978
+ }
1979
+ case "get_console": {
1980
+ const limit = args.limit || 30;
1981
+ const events = db.getEvents({
1982
+ sessionId: args.sessionId,
1983
+ eventType: "console",
1984
+ limit
1985
+ });
1986
+ return {
1987
+ logs: events.map((e) => ({
1988
+ level: e.payload.level,
1989
+ message: e.payload.message,
1990
+ timestamp: new Date(e.timestamp).toISOString(),
1991
+ route: e.route
1992
+ }))
1993
+ };
1994
+ }
1995
+ case "get_network_failures": {
1996
+ const limit = args.limit || 20;
1997
+ const events = db.getEvents({
1998
+ sessionId: args.sessionId,
1999
+ limit: 100
2000
+ });
2001
+ const failures = events.filter((e) => (e.eventType === "fetch" || e.eventType === "xhr") && (e.payload.status >= 400 || e.payload.error)).slice(0, limit).map((e) => ({
2002
+ url: e.payload.url,
2003
+ method: e.payload.method,
2004
+ status: e.payload.status,
2005
+ error: e.payload.error,
2006
+ durationMs: e.payload.durationMs,
2007
+ timestamp: new Date(e.timestamp).toISOString()
2008
+ }));
2009
+ return { failures };
2010
+ }
2011
+ case "get_breadcrumbs": {
2012
+ if (args.incidentId) {
2013
+ const incident = db.getIncident(args.incidentId);
2014
+ if (!incident) {
2015
+ throw new Error(`Incident '${args.incidentId}' not found.`);
2016
+ }
2017
+ return {
2018
+ incidentId: args.incidentId,
2019
+ breadcrumbs: incident.breadcrumbs.slice(-(args.limit || 50))
2020
+ };
2021
+ }
2022
+ const events = db.getEvents({
2023
+ sessionId: args.sessionId,
2024
+ limit: args.limit || 50
2025
+ });
2026
+ return {
2027
+ breadcrumbs: events.map((e) => ({
2028
+ type: e.eventType,
2029
+ message: e.payload.message || `${e.eventType} on ${e.route}`,
2030
+ timestamp: new Date(e.timestamp).toISOString(),
2031
+ route: e.route
2032
+ }))
2033
+ };
2034
+ }
2035
+ case "get_page_state": {
2036
+ if (!sessionManager) {
2037
+ throw new Error("Live browser connection not available: Daemon session manager not attached.");
2038
+ }
2039
+ let session = args.sessionId ? db.getSession(args.sessionId) : sessionManager.getAnyActiveSession();
2040
+ if (!session) {
2041
+ throw new Error("No active browser session connected.");
2042
+ }
2043
+ const cmdRes = await sessionManager.sendCommand(session.id, {
2044
+ id: `cmd_mcp_${Date.now()}`,
2045
+ type: "get_page_state"
2046
+ });
2047
+ if (!cmdRes.ok) {
2048
+ throw new Error(cmdRes.error || "Failed to retrieve page state from browser.");
2049
+ }
2050
+ return cmdRes.result;
2051
+ }
2052
+ case "capture_element": {
2053
+ if (!sessionManager) {
2054
+ throw new Error("Live browser connection not available: Daemon session manager not attached.");
2055
+ }
2056
+ let session = args.sessionId ? db.getSession(args.sessionId) : sessionManager.getAnyActiveSession();
2057
+ if (!session) {
2058
+ throw new Error("No active browser session connected.");
2059
+ }
2060
+ const cmdRes = await sessionManager.sendCommand(session.id, {
2061
+ id: `cmd_mcp_${Date.now()}`,
2062
+ type: "capture_element",
2063
+ params: { selector: args.selector }
2064
+ });
2065
+ if (!cmdRes.ok) {
2066
+ throw new Error(cmdRes.error || cmdRes.reason || "Failed to capture element screenshot.");
2067
+ }
2068
+ return {
2069
+ ok: true,
2070
+ format: cmdRes.result?.format || "webp",
2071
+ width: cmdRes.result?.width,
2072
+ height: cmdRes.result?.height,
2073
+ dataUrlPreview: cmdRes.result?.dataUrl ? `${cmdRes.result.dataUrl.slice(0, 100)}...` : void 0
2074
+ };
2075
+ }
2076
+ case "verify_incident": {
2077
+ if (!verificationEngine) {
2078
+ throw new Error("Verification engine not available: Daemon session manager not attached.");
2079
+ }
2080
+ const res = await verificationEngine.verifyIncident(args.incidentId, {
2081
+ route: args.route,
2082
+ targetSelector: args.targetSelector,
2083
+ expect: args.expect,
2084
+ observationWindowMs: args.observationWindowMs
2085
+ });
2086
+ return res;
2087
+ }
2088
+ case "get_verification": {
2089
+ const v = db.getLatestVerification(args.incidentId);
2090
+ if (!v) {
2091
+ throw new Error(`No verification records found for incident '${args.incidentId}'.`);
2092
+ }
2093
+ return v;
2094
+ }
2095
+ case "list_notes": {
2096
+ const notes = db.listNotes({
2097
+ projectId: args.projectId,
2098
+ status: args.status,
2099
+ limit: args.limit || 20
2100
+ });
2101
+ return {
2102
+ total: notes.length,
2103
+ notes: notes.map((n) => ({
2104
+ id: n.id,
2105
+ type: n.type,
2106
+ status: n.status,
2107
+ route: n.route,
2108
+ viewport: `${n.viewport.width} \xD7 ${n.viewport.height}`,
2109
+ target: n.target?.selector || n.type,
2110
+ message: n.message,
2111
+ screenshotAvailable: !!n.screenshots?.original,
2112
+ createdAt: n.createdAt
2113
+ }))
2114
+ };
2115
+ }
2116
+ case "get_note": {
2117
+ const note = db.getNote(args.noteId);
2118
+ if (!note) {
2119
+ throw new Error(`Visual note '${args.noteId}' not found.`);
2120
+ }
2121
+ const project = db.getProject(note.projectId);
2122
+ return {
2123
+ id: note.id,
2124
+ type: note.type,
2125
+ status: note.status,
2126
+ message: note.message,
2127
+ route: note.route,
2128
+ url: note.url,
2129
+ viewport: note.viewport,
2130
+ scroll: note.scroll,
2131
+ target: note.target,
2132
+ elementContext: note.elementContext,
2133
+ region: note.region,
2134
+ screenshot: {
2135
+ available: !!note.screenshots?.original,
2136
+ original: note.screenshots?.original,
2137
+ after: note.screenshots?.after
2138
+ },
2139
+ project: project ? {
2140
+ id: project.id,
2141
+ name: project.name,
2142
+ origin: project.origin,
2143
+ path: project.path
2144
+ } : void 0,
2145
+ relatedIncidentId: note.incidentId,
2146
+ createdAt: note.createdAt,
2147
+ updatedAt: note.updatedAt,
2148
+ resolvedAt: note.resolvedAt
2149
+ };
2150
+ }
2151
+ case "resolve_note": {
2152
+ const note = db.getNote(args.noteId);
2153
+ if (!note) {
2154
+ throw new Error(`Visual note '${args.noteId}' not found.`);
2155
+ }
2156
+ db.updateNoteStatus(args.noteId, "RESOLVED");
2157
+ return {
2158
+ ok: true,
2159
+ noteId: args.noteId,
2160
+ status: "RESOLVED",
2161
+ resolvedAt: (/* @__PURE__ */ new Date()).toISOString()
2162
+ };
2163
+ }
2164
+ case "reopen_note": {
2165
+ const note = db.getNote(args.noteId);
2166
+ if (!note) {
2167
+ throw new Error(`Visual note '${args.noteId}' not found.`);
2168
+ }
2169
+ db.updateNoteStatus(args.noteId, "OPEN");
2170
+ return {
2171
+ ok: true,
2172
+ noteId: args.noteId,
2173
+ status: "OPEN"
2174
+ };
2175
+ }
2176
+ case "verify_note": {
2177
+ if (!noteVerificationEngine) {
2178
+ throw new Error("Note verification engine not available: Daemon session manager not attached.");
2179
+ }
2180
+ const res = await noteVerificationEngine.verifyNote(args.noteId, {
2181
+ observationWindowMs: args.observationWindowMs
2182
+ });
2183
+ return res;
2184
+ }
2185
+ case "get_note_verification": {
2186
+ const v = db.getLatestNoteVerification(args.noteId);
2187
+ if (!v) {
2188
+ throw new Error(`No verification records found for visual note '${args.noteId}'.`);
2189
+ }
2190
+ return v;
2191
+ }
2192
+ case "capture_note_context": {
2193
+ if (!sessionManager) {
2194
+ throw new Error("Live browser connection not available: Daemon session manager not attached.");
2195
+ }
2196
+ let session = args.sessionId ? db.getSession(args.sessionId) : sessionManager.getAnyActiveSession();
2197
+ if (!session) {
2198
+ throw new Error("No active browser session connected.");
2199
+ }
2200
+ const queryCmd = await sessionManager.sendCommand(session.id, {
2201
+ id: `cmd_ctx_${Date.now()}`,
2202
+ type: "query_element",
2203
+ params: { selector: args.selector }
2204
+ });
2205
+ const overflowCmd = await sessionManager.sendCommand(session.id, {
2206
+ id: `cmd_ovf_${Date.now()}`,
2207
+ type: "check_overflow",
2208
+ params: { selector: args.selector }
2209
+ });
2210
+ const styleCmd = await sessionManager.sendCommand(session.id, {
2211
+ id: `cmd_sty_${Date.now()}`,
2212
+ type: "get_element_style",
2213
+ params: { selector: args.selector }
2214
+ });
2215
+ return {
2216
+ selector: args.selector,
2217
+ element: queryCmd.result,
2218
+ overflow: overflowCmd.result,
2219
+ styles: styleCmd.result?.styles
2220
+ };
2221
+ }
2222
+ default:
2223
+ throw new Error(`Unknown MCP tool: ${name}`);
2224
+ }
2225
+ }
2226
+
2227
+ // packages/mcp/src/tools.ts
2228
+ var TOOLS = [
2229
+ {
2230
+ name: "list_projects",
2231
+ description: "List all registered projects tracked by BrowserTrack/BrowserDiag",
2232
+ inputSchema: {
2233
+ type: "object",
2234
+ properties: {}
2235
+ }
2236
+ },
2237
+ {
2238
+ name: "list_sessions",
2239
+ description: "List active and recent browser sessions connected to the local development daemon",
2240
+ inputSchema: {
2241
+ type: "object",
2242
+ properties: {
2243
+ projectId: { type: "string", description: "Filter sessions by project ID or name" },
2244
+ activeOnly: { type: "boolean", description: "Show only currently active WebSocket sessions (default: true)" }
2245
+ }
2246
+ }
2247
+ },
2248
+ {
2249
+ name: "list_incidents",
2250
+ description: "List recorded browser runtime errors, unhandled rejections, and console errors grouped into incidents",
2251
+ inputSchema: {
2252
+ type: "object",
2253
+ properties: {
2254
+ projectId: { type: "string", description: "Filter by project ID or name" },
2255
+ status: {
2256
+ type: "string",
2257
+ enum: ["OPEN", "FIX_ATTEMPTED", "VERIFYING", "VERIFIED", "FAILED", "INCONCLUSIVE"],
2258
+ description: "Filter by incident status"
2259
+ },
2260
+ severity: {
2261
+ type: "string",
2262
+ enum: ["error", "warn", "fatal"],
2263
+ description: "Filter by severity"
2264
+ },
2265
+ limit: { type: "number", description: "Maximum number of incidents to return (default: 20)" }
2266
+ }
2267
+ }
2268
+ },
2269
+ {
2270
+ name: "get_incident",
2271
+ description: "Retrieve compact and high-signal debugging context for a specific error incident (stack trace, breadcrumbs, network failures, last interacted element, error screenshot)",
2272
+ inputSchema: {
2273
+ type: "object",
2274
+ properties: {
2275
+ incidentId: { type: "string", description: "The unique ID of the incident (e.g. inc_42)" }
2276
+ },
2277
+ required: ["incidentId"]
2278
+ }
2279
+ },
2280
+ {
2281
+ name: "get_console",
2282
+ description: "Get recent console logs, warnings, and errors from a browser session",
2283
+ inputSchema: {
2284
+ type: "object",
2285
+ properties: {
2286
+ sessionId: { type: "string", description: "Browser session ID (optional, defaults to active session)" },
2287
+ limit: { type: "number", description: "Number of console logs to retrieve (default: 30)" }
2288
+ }
2289
+ }
2290
+ },
2291
+ {
2292
+ name: "get_network_failures",
2293
+ description: "Get recent failed HTTP network requests (4xx, 5xx, network errors, timeouts, aborts)",
2294
+ inputSchema: {
2295
+ type: "object",
2296
+ properties: {
2297
+ sessionId: { type: "string", description: "Browser session ID (optional)" },
2298
+ limit: { type: "number", description: "Maximum number of failed requests to return (default: 20)" }
2299
+ }
2300
+ }
2301
+ },
2302
+ {
2303
+ name: "get_breadcrumbs",
2304
+ description: "Get the sequence of recent user interactions, navigations, console logs, and network events prior to an error",
2305
+ inputSchema: {
2306
+ type: "object",
2307
+ properties: {
2308
+ sessionId: { type: "string", description: "Browser session ID (optional)" },
2309
+ incidentId: { type: "string", description: "Incident ID to fetch breadcrumbs from (optional)" },
2310
+ limit: { type: "number", description: "Maximum number of breadcrumbs (default: 50)" }
2311
+ }
2312
+ }
2313
+ },
2314
+ {
2315
+ name: "get_page_state",
2316
+ description: "Query the live page state (URL, route, document title, readyState, active element) from the connected browser session",
2317
+ inputSchema: {
2318
+ type: "object",
2319
+ properties: {
2320
+ sessionId: { type: "string", description: "Target session ID (optional, defaults to active)" }
2321
+ }
2322
+ }
2323
+ },
2324
+ {
2325
+ name: "capture_element",
2326
+ description: "Capture a screenshot of a specific DOM element or the entire visible page in the active browser tab",
2327
+ inputSchema: {
2328
+ type: "object",
2329
+ properties: {
2330
+ selector: { type: "string", description: 'CSS selector of the element to capture (e.g. [data-testid="user-card"])' },
2331
+ sessionId: { type: "string", description: "Browser session ID (optional)" }
2332
+ }
2333
+ }
2334
+ },
2335
+ {
2336
+ name: "verify_incident",
2337
+ description: "Trigger closed-loop verification of a bug fix: reloads the browser, checks if the incident reoccurs, evaluates optional verification probes, and records before/after screenshots",
2338
+ inputSchema: {
2339
+ type: "object",
2340
+ properties: {
2341
+ incidentId: { type: "string", description: "The ID of the incident to verify" },
2342
+ route: { type: "string", description: "Optional route to navigate to for verification (defaults to incident route)" },
2343
+ targetSelector: { type: "string", description: "Optional element selector to inspect and capture after-fix screenshot" },
2344
+ expect: {
2345
+ type: "array",
2346
+ description: "Optional verification probes to evaluate after reload",
2347
+ items: {
2348
+ type: "object",
2349
+ properties: {
2350
+ type: {
2351
+ type: "string",
2352
+ enum: ["element_exists", "element_visible", "text_contains", "route_is", "no_incident"]
2353
+ },
2354
+ selector: { type: "string" },
2355
+ text: { type: "string" },
2356
+ route: { type: "string" }
2357
+ },
2358
+ required: ["type"]
2359
+ }
2360
+ },
2361
+ observationWindowMs: { type: "number", description: "Observation window in milliseconds (default: 2000)" }
2362
+ },
2363
+ required: ["incidentId"]
2364
+ }
2365
+ },
2366
+ {
2367
+ name: "get_verification",
2368
+ description: "Retrieve the latest verification result and before/after screenshot artifacts for an incident",
2369
+ inputSchema: {
2370
+ type: "object",
2371
+ properties: {
2372
+ incidentId: { type: "string", description: "The ID of the incident" }
2373
+ },
2374
+ required: ["incidentId"]
2375
+ }
2376
+ },
2377
+ {
2378
+ name: "list_notes",
2379
+ description: "List visual development notes / screen annotations left on elements, regions, or pages during development",
2380
+ inputSchema: {
2381
+ type: "object",
2382
+ properties: {
2383
+ projectId: { type: "string", description: "Filter notes by project ID or name" },
2384
+ status: {
2385
+ type: "string",
2386
+ enum: ["OPEN", "IN_PROGRESS", "VERIFYING", "RESOLVED", "FAILED", "INCONCLUSIVE"],
2387
+ description: "Filter by note status (default: OPEN)"
2388
+ },
2389
+ limit: { type: "number", description: "Maximum number of notes to return (default: 20)" }
2390
+ }
2391
+ }
2392
+ },
2393
+ {
2394
+ name: "get_note",
2395
+ description: "Retrieve full debugging context for a visual note (message, route, viewport dimensions, target element selector, DOM context, screenshot file path, project path)",
2396
+ inputSchema: {
2397
+ type: "object",
2398
+ properties: {
2399
+ noteId: { type: "string", description: "The unique ID of the visual note (e.g. note_42)" }
2400
+ },
2401
+ required: ["noteId"]
2402
+ }
2403
+ },
2404
+ {
2405
+ name: "resolve_note",
2406
+ description: "Mark a visual note as RESOLVED once the layout or styling issue has been fixed",
2407
+ inputSchema: {
2408
+ type: "object",
2409
+ properties: {
2410
+ noteId: { type: "string", description: "The ID of the visual note to resolve" }
2411
+ },
2412
+ required: ["noteId"]
2413
+ }
2414
+ },
2415
+ {
2416
+ name: "reopen_note",
2417
+ description: "Reopen a previously resolved visual note",
2418
+ inputSchema: {
2419
+ type: "object",
2420
+ properties: {
2421
+ noteId: { type: "string", description: "The ID of the visual note to reopen" }
2422
+ },
2423
+ required: ["noteId"]
2424
+ }
2425
+ },
2426
+ {
2427
+ name: "verify_note",
2428
+ description: "Run closed-loop layout & visual verification for a note: checks route, element existence/visibility, evaluates viewport overflow probes, captures after-screenshot, and computes geometry diff",
2429
+ inputSchema: {
2430
+ type: "object",
2431
+ properties: {
2432
+ noteId: { type: "string", description: "The ID of the visual note to verify" },
2433
+ observationWindowMs: { type: "number", description: "Observation window in milliseconds (default: 1000)" }
2434
+ },
2435
+ required: ["noteId"]
2436
+ }
2437
+ },
2438
+ {
2439
+ name: "get_note_verification",
2440
+ description: "Retrieve the latest verification result, before/after screenshot references, and layout geometry diff for a visual note",
2441
+ inputSchema: {
2442
+ type: "object",
2443
+ properties: {
2444
+ noteId: { type: "string", description: "The ID of the visual note" }
2445
+ },
2446
+ required: ["noteId"]
2447
+ }
2448
+ },
2449
+ {
2450
+ name: "capture_note_context",
2451
+ description: "Inspect live DOM context, bounding box, overflow, and styles for a target element in the active browser tab",
2452
+ inputSchema: {
2453
+ type: "object",
2454
+ properties: {
2455
+ selector: { type: "string", description: "CSS selector of the target element" },
2456
+ sessionId: { type: "string", description: "Browser session ID (optional)" }
2457
+ },
2458
+ required: ["selector"]
2459
+ }
2460
+ }
2461
+ ];
2462
+
2463
+ // packages/mcp/src/server.ts
2464
+ function createMcpServer(options = {}) {
2465
+ const config = getDaemonConfig({ dbPath: options.dbPath });
2466
+ const db = options.context?.db || new StorageDB(config.dbPath);
2467
+ const screenshotStore = new ScreenshotStore(config.screenshotsDir);
2468
+ const sessionManager = options.context?.sessionManager || new SessionManager(db);
2469
+ const verificationEngine = options.context?.verificationEngine || new VerificationEngine(db, sessionManager, screenshotStore);
2470
+ const notesEngine = new NotesEngine(db, screenshotStore);
2471
+ const noteVerificationEngine = options.context?.noteVerificationEngine || new NoteVerificationEngine(db, sessionManager, notesEngine);
2472
+ const ctx = {
2473
+ db,
2474
+ sessionManager,
2475
+ verificationEngine,
2476
+ noteVerificationEngine,
2477
+ daemonUrl: `http://${config.host}:${config.port}`,
2478
+ ...options.context
2479
+ };
2480
+ const server = new Server(
2481
+ {
2482
+ name: "browsertrack-mcp",
2483
+ version: "0.1.0"
2484
+ },
2485
+ {
2486
+ capabilities: {
2487
+ tools: {}
2488
+ }
2489
+ }
2490
+ );
2491
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
2492
+ return { tools: TOOLS };
2493
+ });
2494
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2495
+ const { name, arguments: args } = request.params;
2496
+ try {
2497
+ const result = await handleToolCall(name, args || {}, ctx);
2498
+ return {
2499
+ content: [
2500
+ {
2501
+ type: "text",
2502
+ text: JSON.stringify(result, null, 2)
2503
+ }
2504
+ ]
2505
+ };
2506
+ } catch (err) {
2507
+ return {
2508
+ isError: true,
2509
+ content: [
2510
+ {
2511
+ type: "text",
2512
+ text: `Error executing tool ${name}: ${err?.message || String(err)}`
2513
+ }
2514
+ ]
2515
+ };
2516
+ }
2517
+ });
2518
+ return {
2519
+ server,
2520
+ async startStdio() {
2521
+ const transport = new StdioServerTransport();
2522
+ await server.connect(transport);
2523
+ }
2524
+ };
2525
+ }
2526
+
2527
+ // packages/cli/src/index.ts
2528
+ var program = new Command();
2529
+ program.name("browsertrack").description("Local browser diagnostics + MCP bridge for coding agents").version("0.1.0");
2530
+ function getPidFilePath() {
2531
+ const config = getDaemonConfig();
2532
+ return path6.join(config.dataDir, "daemon.pid");
2533
+ }
2534
+ function savePid(pid) {
2535
+ const pidFile = getPidFilePath();
2536
+ fs5.mkdirSync(path6.dirname(pidFile), { recursive: true });
2537
+ fs5.writeFileSync(pidFile, String(pid), "utf-8");
2538
+ }
2539
+ function readPid() {
2540
+ try {
2541
+ const pidFile = getPidFilePath();
2542
+ if (fs5.existsSync(pidFile)) {
2543
+ const pidStr = fs5.readFileSync(pidFile, "utf-8").trim();
2544
+ const pid = parseInt(pidStr, 10);
2545
+ if (!isNaN(pid)) return pid;
2546
+ }
2547
+ } catch {
2548
+ }
2549
+ return null;
2550
+ }
2551
+ function removePid() {
2552
+ try {
2553
+ const pidFile = getPidFilePath();
2554
+ if (fs5.existsSync(pidFile)) {
2555
+ fs5.unlinkSync(pidFile);
2556
+ }
2557
+ } catch {
2558
+ }
2559
+ }
2560
+ function isProcessRunning(pid) {
2561
+ try {
2562
+ process.kill(pid, 0);
2563
+ return true;
2564
+ } catch {
2565
+ return false;
2566
+ }
2567
+ }
2568
+ program.command("start").description("Start the local BrowserTrack daemon and HTTP/WS server").option("-p, --port <number>", "Server port", "7331").option("-h, --host <host>", "Server host", "127.0.0.1").option("-v, --verbose", "Enable verbose logging", false).action(async (options) => {
2569
+ const existingPid = readPid();
2570
+ if (existingPid && isProcessRunning(existingPid)) {
2571
+ console.log(`[BrowserTrack] Daemon is already running (PID: ${existingPid})`);
2572
+ return;
2573
+ }
2574
+ const port = parseInt(options.port, 10);
2575
+ const daemon = createDaemon({
2576
+ port,
2577
+ host: options.host,
2578
+ verbose: options.verbose
2579
+ });
2580
+ try {
2581
+ await daemon.start();
2582
+ savePid(process.pid);
2583
+ console.log("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501");
2584
+ console.log(" \u{1F50D} BrowserTrack Daemon Active");
2585
+ console.log(` \u{1F310} Server: http://${daemon.config.host}:${daemon.config.port}`);
2586
+ console.log(` \u{1F50C} WebSocket: ws://${daemon.config.host}:${daemon.config.port}`);
2587
+ console.log(` \u{1F4E6} Script: <script src="http://${daemon.config.host}:${daemon.config.port}/client.js"></script>`);
2588
+ console.log(` \u{1F4BE} Database: ${daemon.config.dbPath}`);
2589
+ console.log("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501");
2590
+ let isShuttingDown = false;
2591
+ const shutdown = async () => {
2592
+ if (isShuttingDown) {
2593
+ process.exit(0);
2594
+ }
2595
+ isShuttingDown = true;
2596
+ console.log("\n[BrowserTrack] Shutting down daemon...");
2597
+ removePid();
2598
+ const forceExitTimer = setTimeout(() => {
2599
+ process.exit(0);
2600
+ }, 800);
2601
+ forceExitTimer.unref();
2602
+ try {
2603
+ await daemon.stop();
2604
+ } catch {
2605
+ }
2606
+ process.exit(0);
2607
+ };
2608
+ process.on("SIGINT", shutdown);
2609
+ process.on("SIGTERM", shutdown);
2610
+ } catch (err) {
2611
+ console.error("[BrowserTrack] Failed to start daemon:", err.message);
2612
+ process.exit(1);
2613
+ }
2614
+ });
2615
+ program.command("stop").description("Stop the running BrowserTrack daemon").action(async () => {
2616
+ const pid = readPid();
2617
+ if (!pid || !isProcessRunning(pid)) {
2618
+ console.log("[BrowserTrack] Daemon is not currently running.");
2619
+ removePid();
2620
+ return;
2621
+ }
2622
+ try {
2623
+ process.kill(pid, "SIGTERM");
2624
+ removePid();
2625
+ console.log(`[BrowserTrack] Stopped daemon (PID: ${pid}).`);
2626
+ } catch (err) {
2627
+ console.error(`[BrowserTrack] Failed to stop daemon:`, err.message);
2628
+ }
2629
+ });
2630
+ program.command("status").description("Check if the BrowserTrack daemon is running and view active sessions").action(async () => {
2631
+ const pid = readPid();
2632
+ const isRunning = pid ? isProcessRunning(pid) : false;
2633
+ const config = getDaemonConfig();
2634
+ console.log("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501");
2635
+ console.log(` Status: ${isRunning ? "\u{1F7E2} RUNNING" : "\u26AA STOPPED"}${isRunning ? ` (PID: ${pid})` : ""}`);
2636
+ console.log(` Endpoint: http://${config.host}:${config.port}`);
2637
+ console.log(` Database: ${config.dbPath}`);
2638
+ if (fs5.existsSync(config.dbPath)) {
2639
+ try {
2640
+ const db = new StorageDB(config.dbPath);
2641
+ const projects = db.listProjects();
2642
+ const sessions = db.listSessions(void 0, true);
2643
+ const incidents = db.listIncidents({ limit: 100 });
2644
+ const openIncidents = incidents.filter((i) => i.status === "OPEN");
2645
+ console.log(` Projects: ${projects.length}`);
2646
+ console.log(` Live Sessions: ${sessions.length}`);
2647
+ console.log(` Open Errors: ${openIncidents.length} (${incidents.length} total recorded)`);
2648
+ db.close();
2649
+ } catch {
2650
+ }
2651
+ }
2652
+ console.log("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501");
2653
+ });
2654
+ var projectCommand = program.command("project").description("Manage tracked project mappings");
2655
+ projectCommand.command("add <name>").description("Register a project with origin and filesystem path").requiredOption("-o, --origin <origin>", "Project origin (e.g. http://localhost:5173)").option("-p, --path <path>", "Filesystem path (e.g. /path/to/project)").action((name, options) => {
2656
+ const config = getDaemonConfig();
2657
+ const db = new StorageDB(config.dbPath);
2658
+ const resolvedPath = options.path ? path6.resolve(options.path) : void 0;
2659
+ const proj = db.upsertProject({
2660
+ id: `proj_${name}`,
2661
+ name,
2662
+ origin: options.origin,
2663
+ path: resolvedPath
2664
+ });
2665
+ console.log(`[BrowserTrack] Registered project '${proj.name}':`);
2666
+ console.log(` ID: ${proj.id}`);
2667
+ console.log(` Origin: ${proj.origin}`);
2668
+ console.log(` Path: ${proj.path || "(none)"}`);
2669
+ db.close();
2670
+ });
2671
+ program.command("projects").description("List all tracked projects").action(() => {
2672
+ const config = getDaemonConfig();
2673
+ const db = new StorageDB(config.dbPath);
2674
+ const projects = db.listProjects();
2675
+ if (projects.length === 0) {
2676
+ console.log("[BrowserTrack] No projects registered yet. Projects will be auto-detected upon browser connection.");
2677
+ } else {
2678
+ console.log("\nTracked Projects:");
2679
+ for (const p of projects) {
2680
+ console.log(` \u2022 ${p.name.padEnd(16)} | ${p.origin.padEnd(26)} | ${p.path || "(auto-detected)"}`);
2681
+ }
2682
+ console.log("");
2683
+ }
2684
+ db.close();
2685
+ });
2686
+ program.command("errors").description("List recorded runtime errors and incidents").option("-p, --project <project>", "Filter by project name or ID").option("-s, --status <status>", "Filter by status (OPEN, VERIFIED, FAILED, etc.)").option("-l, --limit <number>", "Limit result count", "20").action((options) => {
2687
+ const config = getDaemonConfig();
2688
+ const db = new StorageDB(config.dbPath);
2689
+ const limit = parseInt(options.limit, 10);
2690
+ const incidents = db.listIncidents({
2691
+ projectId: options.project,
2692
+ status: options.status,
2693
+ limit
2694
+ });
2695
+ if (incidents.length === 0) {
2696
+ console.log("[BrowserTrack] No incidents found matching the filter.");
2697
+ } else {
2698
+ console.log(`
2699
+ Incidents (${incidents.length}):`);
2700
+ for (const inc of incidents) {
2701
+ const statusBadge = inc.status === "OPEN" ? "\u{1F534} OPEN" : inc.status === "VERIFIED" ? "\u{1F7E2} VERIFIED" : inc.status === "FAILED" ? "\u274C FAILED" : `\u26AA ${inc.status}`;
2702
+ console.log(` ${inc.id.padEnd(12)} [${statusBadge}] (${inc.occurrences}x) ${inc.type}: ${inc.message}`);
2703
+ console.log(` Source: ${inc.source.file}:${inc.source.line} | Route: ${inc.route}`);
2704
+ }
2705
+ console.log("");
2706
+ }
2707
+ db.close();
2708
+ });
2709
+ var noteCmd = program.command("note").description("Inspect or manage visual development notes");
2710
+ noteCmd.command("show <noteId>").description("Show full context for a specific visual note").action((noteId) => {
2711
+ const config = getDaemonConfig();
2712
+ const db = new StorageDB(config.dbPath);
2713
+ const note = db.getNote(noteId);
2714
+ if (!note) {
2715
+ console.log(`[BrowserTrack] Note '${noteId}' not found.`);
2716
+ } else {
2717
+ console.log("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501");
2718
+ console.log(` \u{1F4DD} Visual Note: ${note.id} [${note.status}]`);
2719
+ console.log(` \u{1F4CD} Route: ${note.route} (${note.url})`);
2720
+ console.log(` \u{1F4D0} Viewport: ${note.viewport.width} \xD7 ${note.viewport.height} (dpr: ${note.viewport.devicePixelRatio})`);
2721
+ if (note.target) {
2722
+ console.log(` \u{1F3AF} Target: ${note.target.selector}`);
2723
+ console.log(` Bounds: x:${note.target.boundingRect.x}, y:${note.target.boundingRect.y}, ${note.target.boundingRect.width}\xD7${note.target.boundingRect.height}`);
2724
+ }
2725
+ console.log(` \u{1F4AC} Note: "${note.message}"`);
2726
+ if (note.screenshots?.original) {
2727
+ console.log(` \u{1F5BC}\uFE0F Screenshot: ${note.screenshots.original}`);
2728
+ }
2729
+ console.log(` \u{1F552} Created: ${note.createdAt}`);
2730
+ console.log("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501");
2731
+ }
2732
+ db.close();
2733
+ });
2734
+ noteCmd.command("resolve <noteId>").description("Mark a visual note as resolved").action((noteId) => {
2735
+ const config = getDaemonConfig();
2736
+ const db = new StorageDB(config.dbPath);
2737
+ const note = db.getNote(noteId);
2738
+ if (!note) {
2739
+ console.log(`[BrowserTrack] Note '${noteId}' not found.`);
2740
+ } else {
2741
+ db.updateNoteStatus(noteId, "RESOLVED");
2742
+ console.log(`[BrowserTrack] Marked note '${noteId}' as RESOLVED.`);
2743
+ }
2744
+ db.close();
2745
+ });
2746
+ noteCmd.command("reopen <noteId>").description("Reopen a visual note").action((noteId) => {
2747
+ const config = getDaemonConfig();
2748
+ const db = new StorageDB(config.dbPath);
2749
+ const note = db.getNote(noteId);
2750
+ if (!note) {
2751
+ console.log(`[BrowserTrack] Note '${noteId}' not found.`);
2752
+ } else {
2753
+ db.updateNoteStatus(noteId, "OPEN");
2754
+ console.log(`[BrowserTrack] Reopened note '${noteId}' (status: OPEN).`);
2755
+ }
2756
+ db.close();
2757
+ });
2758
+ program.command("notes").description("List visual development notes").option("-p, --project <project>", "Filter by project name or ID").option("-s, --status <status>", "Filter by status (OPEN, RESOLVED, etc.)").option("-l, --limit <number>", "Limit result count", "20").action((options) => {
2759
+ const config = getDaemonConfig();
2760
+ const db = new StorageDB(config.dbPath);
2761
+ const limit = parseInt(options.limit, 10);
2762
+ const notes = db.listNotes({
2763
+ projectId: options.project,
2764
+ status: options.status,
2765
+ limit
2766
+ });
2767
+ if (notes.length === 0) {
2768
+ console.log("[BrowserTrack] No visual notes found.");
2769
+ } else {
2770
+ console.log(`
2771
+ Visual Notes (${notes.length}):`);
2772
+ for (const n of notes) {
2773
+ const badge = n.status === "OPEN" ? "\u{1F7E1} OPEN" : n.status === "RESOLVED" ? "\u{1F7E2} RESOLVED" : `\u26AA ${n.status}`;
2774
+ console.log(` ${n.id.padEnd(12)} [${badge}] Route: ${n.route.padEnd(16)} | Target: ${n.target?.selector || n.type}`);
2775
+ console.log(` Note: "${n.message}" (${n.viewport.width}\xD7${n.viewport.height})`);
2776
+ }
2777
+ console.log("");
2778
+ }
2779
+ db.close();
2780
+ });
2781
+ program.command("inbox").description("View combined developer inbox with active runtime errors and visual notes").option("-p, --project <project>", "Filter by project name or ID").action((options) => {
2782
+ const config = getDaemonConfig();
2783
+ const db = new StorageDB(config.dbPath);
2784
+ const incidents = db.listIncidents({ projectId: options.project, status: "OPEN", limit: 20 });
2785
+ const notes = db.listNotes({ projectId: options.project, status: "OPEN", limit: 20 });
2786
+ console.log("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501");
2787
+ console.log(` \u{1F4E5} Browser Development Inbox ${options.project ? `(${options.project})` : ""}`);
2788
+ console.log("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501");
2789
+ if (incidents.length === 0 && notes.length === 0) {
2790
+ console.log(" \u2728 All clear! No open errors or visual notes.");
2791
+ } else {
2792
+ if (incidents.length > 0) {
2793
+ console.log(`
2794
+ \u{1F6A8} Runtime Errors (${incidents.length}):`);
2795
+ for (const inc of incidents) {
2796
+ console.log(` \u2022 ${inc.id} (${inc.occurrences}x) ${inc.type}: ${inc.message}`);
2797
+ console.log(` Route: ${inc.route} | Source: ${inc.source.file}:${inc.source.line}`);
2798
+ }
2799
+ }
2800
+ if (notes.length > 0) {
2801
+ console.log(`
2802
+ \u{1F4DD} Visual Notes (${notes.length}):`);
2803
+ for (const n of notes) {
2804
+ console.log(` \u2022 ${n.id} on ${n.route} (${n.viewport.width}\xD7${n.viewport.height})`);
2805
+ console.log(` Target: ${n.target?.selector || n.type} | Note: "${n.message}"`);
2806
+ }
2807
+ }
2808
+ }
2809
+ console.log("\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501");
2810
+ db.close();
2811
+ });
2812
+ program.command("clear").description("Clear all stored incidents, events, and sessions from the database").action(() => {
2813
+ const config = getDaemonConfig();
2814
+ const db = new StorageDB(config.dbPath);
2815
+ db.clearAll();
2816
+ console.log("[BrowserTrack] Database cleared.");
2817
+ db.close();
2818
+ });
2819
+ program.command("mcp").description("Launch the Model Context Protocol (MCP) server over stdio").action(async () => {
2820
+ try {
2821
+ const server = createMcpServer();
2822
+ await server.startStdio();
2823
+ } catch (err) {
2824
+ console.error("[BrowserTrack] MCP Server error:", err?.message);
2825
+ process.exit(1);
2826
+ }
2827
+ });
2828
+ program.parse(process.argv);
2829
+ //# sourceMappingURL=index.js.map