browsertrack 0.2.0 → 0.2.2

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 (67) hide show
  1. package/AGENTS.md +11 -6
  2. package/README.md +2 -0
  3. package/dist/{chunk-WB7ZKWK7.js → chunk-4HRLW6YF.js} +509 -109
  4. package/dist/chunk-4HRLW6YF.js.map +1 -0
  5. package/dist/{chunk-3HOXPTM2.js → chunk-AYSVE6NG.js} +808 -53
  6. package/dist/chunk-AYSVE6NG.js.map +1 -0
  7. package/dist/{chunk-6VA7GBAO.js → chunk-QRZ57ME3.js} +70 -2
  8. package/dist/chunk-QRZ57ME3.js.map +1 -0
  9. package/dist/{chunk-UP5JKCFY.js → chunk-TWEYRBDU.js} +281 -44
  10. package/dist/chunk-TWEYRBDU.js.map +1 -0
  11. package/dist/cli/index.js +1040 -546
  12. package/dist/cli/index.js.map +1 -1
  13. package/dist/client/index.cjs +536 -109
  14. package/dist/client/index.d.ts +36 -4
  15. package/dist/client/index.js +8 -4
  16. package/dist/client.iife.js +42 -19
  17. package/dist/{notes-BMnonq46.d.ts → commands-fjuqKzkm.d.ts} +125 -114
  18. package/dist/core/index.d.ts +26 -3
  19. package/dist/core/index.js +9 -1
  20. package/dist/daemon/index.d.ts +4 -4
  21. package/dist/daemon/index.js +6 -8
  22. package/dist/{engine-B43IohQY.d.ts → engine-CmchnMDq.d.ts} +2 -2
  23. package/dist/index.d.ts +5 -5
  24. package/dist/index.js +14 -7
  25. package/dist/mcp/index.d.ts +4 -4
  26. package/dist/mcp/index.js +7 -4
  27. package/dist/{projects-D5J-egVN.d.ts → projects-DB7S312i.d.ts} +1 -1
  28. package/dist/{server-BztYp1Zc.d.ts → server-DjV7RWQM.d.ts} +10 -2
  29. package/docs/cli.md +4 -1
  30. package/docs/component-resolver.md +108 -0
  31. package/docs/getting-started.md +60 -6
  32. package/docs/index.md +1 -0
  33. package/docs/mcp-reference.md +44 -2
  34. package/docs/visual-notes.md +36 -0
  35. package/package.json +1 -1
  36. package/packages/cli/src/index.ts +247 -151
  37. package/packages/client/src/client.ts +18 -1
  38. package/packages/client/src/config.ts +84 -1
  39. package/packages/client/src/index.ts +4 -1
  40. package/packages/client/src/interceptors/interaction.ts +3 -0
  41. package/packages/client/src/interceptors/navigation.ts +38 -26
  42. package/packages/client/src/interceptors/network.ts +22 -17
  43. package/packages/client/src/notes/inspector.ts +186 -51
  44. package/packages/client/src/source/resolver.ts +278 -0
  45. package/packages/client/src/transport/websocket.ts +23 -18
  46. package/packages/core/src/index.ts +1 -0
  47. package/packages/core/src/safety.ts +86 -0
  48. package/packages/core/src/types/events.ts +3 -0
  49. package/packages/core/src/types/notes.ts +11 -0
  50. package/packages/daemon/src/server/daemon.ts +7 -1
  51. package/packages/daemon/src/server/http.ts +125 -5
  52. package/packages/daemon/src/server/ws.ts +33 -29
  53. package/packages/daemon/src/storage/db.ts +57 -35
  54. package/packages/mcp/src/handlers.ts +115 -45
  55. package/packages/mcp/src/server.ts +202 -2
  56. package/test/client/component-resolver.test.ts +141 -0
  57. package/test/client/interceptors.test.ts +56 -0
  58. package/test/core/safety.test.ts +106 -0
  59. package/test/daemon/storage.test.ts +36 -0
  60. package/test/e2e/daemon-mcp-e2e.test.ts +10 -0
  61. package/test/mcp/auto-start.test.ts +87 -0
  62. package/dist/chunk-3HOXPTM2.js.map +0 -1
  63. package/dist/chunk-6VA7GBAO.js.map +0 -1
  64. package/dist/chunk-7OCOQGDN.js +0 -635
  65. package/dist/chunk-7OCOQGDN.js.map +0 -1
  66. package/dist/chunk-UP5JKCFY.js.map +0 -1
  67. package/dist/chunk-WB7ZKWK7.js.map +0 -1
package/dist/cli/index.js CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  // packages/cli/src/index.ts
4
4
  import { Command } from "commander";
5
- import fs5 from "fs";
6
- import path6 from "path";
5
+ import fs6 from "fs";
6
+ import path7 from "path";
7
7
 
8
8
  // packages/daemon/src/config.ts
9
9
  import os from "os";
@@ -28,6 +28,218 @@ function getDaemonConfig(overrides = {}) {
28
28
  import Database from "better-sqlite3";
29
29
  import fs from "fs";
30
30
  import path2 from "path";
31
+
32
+ // packages/core/src/fingerprint.ts
33
+ function normalizeErrorMessage(message) {
34
+ if (!message) return "unknown_error";
35
+ 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, " ");
36
+ }
37
+ function normalizeSourceFile(filename) {
38
+ if (!filename) return "unknown_source";
39
+ let cleaned = filename.trim();
40
+ try {
41
+ if (cleaned.startsWith("http://") || cleaned.startsWith("https://")) {
42
+ const url = new URL(cleaned);
43
+ cleaned = url.pathname;
44
+ }
45
+ } catch {
46
+ cleaned = cleaned.replace(/^https?:\/\/[^/]+/, "");
47
+ }
48
+ cleaned = cleaned.split("?")[0].split("#")[0];
49
+ cleaned = cleaned.replace(/\\/g, "/");
50
+ return cleaned || "unknown_source";
51
+ }
52
+ function extractSourceFromStack(stack) {
53
+ if (!stack) return null;
54
+ const lines = stack.split("\n");
55
+ for (const line of lines) {
56
+ const match = line.match(/(?:at\s+(?:.*?\s+\()?)?(https?:\/\/[^\s)]+|file:\/\/[^\s)]+|\/[^\s)]+):(\d+):(\d+)\)?/);
57
+ if (match) {
58
+ return {
59
+ file: normalizeSourceFile(match[1]),
60
+ line: parseInt(match[2], 10),
61
+ column: parseInt(match[3], 10)
62
+ };
63
+ }
64
+ }
65
+ return null;
66
+ }
67
+ function djb2Hash(str) {
68
+ let hash = 5381;
69
+ for (let i = 0; i < str.length; i++) {
70
+ hash = hash * 33 ^ str.charCodeAt(i);
71
+ }
72
+ return (hash >>> 0).toString(16).padStart(8, "0");
73
+ }
74
+ function computeFingerprint(input) {
75
+ const normType = (input.type || "Error").trim().toLowerCase();
76
+ const normMsg = normalizeErrorMessage(input.message);
77
+ let sourceFile = normalizeSourceFile(input.sourceFile);
78
+ let line = input.line || 0;
79
+ if ((sourceFile === "unknown_source" || line === 0) && input.stack) {
80
+ const extracted = extractSourceFromStack(input.stack);
81
+ if (extracted) {
82
+ sourceFile = extracted.file;
83
+ line = extracted.line;
84
+ }
85
+ }
86
+ const rawKey = `${normType}::${normMsg}::${sourceFile}::${line}`;
87
+ const hash = djb2Hash(rawKey);
88
+ return `fp_${hash}`;
89
+ }
90
+
91
+ // packages/core/src/redaction.ts
92
+ import { redact as visulimaRedact, standardRules } from "@visulima/redact";
93
+ var REDACTED_PLACEHOLDER = "[REDACTED]";
94
+ var SENSITIVE_KEY_PATTERNS = [
95
+ /^authorization$/i,
96
+ /^cookie$/i,
97
+ /^set-cookie$/i,
98
+ /password/i,
99
+ /token/i,
100
+ /secret/i,
101
+ /api[-_]?key/i,
102
+ /access[-_]?token/i,
103
+ /refresh[-_]?token/i,
104
+ /credentials/i,
105
+ /private[-_]?key/i,
106
+ /ssn/i,
107
+ /credit[-_]?card/i,
108
+ /cvv/i
109
+ ];
110
+ var SENSITIVE_QUERY_PARAMS = [
111
+ "token",
112
+ "auth",
113
+ "key",
114
+ "apikey",
115
+ "api_key",
116
+ "secret",
117
+ "password",
118
+ "access_token",
119
+ "refresh_token",
120
+ "code",
121
+ "signature"
122
+ ];
123
+ var BROWSER_SECURITY_RULES = [
124
+ { deep: true, key: "password", replacement: REDACTED_PLACEHOLDER },
125
+ { deep: true, key: "secret", replacement: REDACTED_PLACEHOLDER },
126
+ { deep: true, key: "token", replacement: REDACTED_PLACEHOLDER },
127
+ { deep: true, key: "authorization", replacement: REDACTED_PLACEHOLDER },
128
+ { deep: true, key: "cookie", replacement: REDACTED_PLACEHOLDER },
129
+ { deep: true, key: "set-cookie", replacement: REDACTED_PLACEHOLDER },
130
+ { deep: true, key: "apikey", replacement: REDACTED_PLACEHOLDER },
131
+ { deep: true, key: "api_key", replacement: REDACTED_PLACEHOLDER },
132
+ { deep: true, key: "creditcard", pattern: /(?:\d[ -]*?){13,16}/, replacement: REDACTED_PLACEHOLDER },
133
+ { deep: true, key: "cvv", replacement: REDACTED_PLACEHOLDER },
134
+ { deep: true, key: "ssn", pattern: /\b\d{3}-\d{2}-\d{4}\b/, replacement: REDACTED_PLACEHOLDER },
135
+ { deep: true, key: "awsid", pattern: /\bAKIA[0-9A-Z]{16}\b/, replacement: REDACTED_PLACEHOLDER },
136
+ { deep: true, key: "awskey", pattern: /\b[0-9a-zA-Z/+]{40}\b/, replacement: REDACTED_PLACEHOLDER },
137
+ { deep: true, key: "jwt", pattern: /\beyJ[0-9a-zA-Z_\-]*\.[0-9a-zA-Z_\-]*\.[0-9a-zA-Z_\-]*\b/, replacement: REDACTED_PLACEHOLDER },
138
+ { deep: true, key: "slack_token", pattern: /\bxox[baprs]-[0-9a-zA-Z]{10,48}\b/, replacement: REDACTED_PLACEHOLDER }
139
+ ];
140
+ function isSensitiveKey(key) {
141
+ if (!key) return false;
142
+ const cleaned = key.replace(/[-_]/g, "");
143
+ return SENSITIVE_KEY_PATTERNS.some((pattern) => pattern.test(key) || pattern.test(cleaned));
144
+ }
145
+ function redactUrl(rawUrl) {
146
+ if (!rawUrl) return rawUrl;
147
+ try {
148
+ const isRelative = !rawUrl.startsWith("http://") && !rawUrl.startsWith("https://") && !rawUrl.startsWith("ws://") && !rawUrl.startsWith("wss://");
149
+ const base = "http://localhost";
150
+ const parsed = new URL(rawUrl, base);
151
+ let changed = false;
152
+ for (const param of SENSITIVE_QUERY_PARAMS) {
153
+ if (parsed.searchParams.has(param)) {
154
+ parsed.searchParams.set(param, REDACTED_PLACEHOLDER);
155
+ changed = true;
156
+ }
157
+ }
158
+ for (const key of Array.from(parsed.searchParams.keys())) {
159
+ if (isSensitiveKey(key)) {
160
+ parsed.searchParams.set(key, REDACTED_PLACEHOLDER);
161
+ changed = true;
162
+ }
163
+ }
164
+ if (!changed) return rawUrl;
165
+ let result = isRelative ? parsed.pathname + parsed.search + parsed.hash : parsed.toString();
166
+ result = result.replace(/%5BREDACTED%5D/g, REDACTED_PLACEHOLDER);
167
+ return result;
168
+ } catch {
169
+ let safe = rawUrl;
170
+ for (const param of SENSITIVE_QUERY_PARAMS) {
171
+ const reg = new RegExp(`([?&]${param}=)[^&#]+`, "gi");
172
+ safe = safe.replace(reg, `$1${REDACTED_PLACEHOLDER}`);
173
+ }
174
+ return safe;
175
+ }
176
+ }
177
+ function redactSensitiveData(data, maxDepth = 6, currentDepth = 0) {
178
+ if (data === null || data === void 0) return data;
179
+ if (typeof data !== "object") return data;
180
+ if (currentDepth > maxDepth) return "[DEPTH_EXCEEDED]";
181
+ if (Array.isArray(data)) {
182
+ return data.map((item) => redactSensitiveData(item, maxDepth, currentDepth + 1));
183
+ }
184
+ const result = {};
185
+ for (const [key, value] of Object.entries(data)) {
186
+ if (typeof value === "object" && value !== null) {
187
+ result[key] = redactSensitiveData(value, maxDepth, currentDepth + 1);
188
+ } else if (isSensitiveKey(key)) {
189
+ result[key] = REDACTED_PLACEHOLDER;
190
+ } else if (typeof value === "string") {
191
+ if (value.startsWith("http://") || value.startsWith("https://") || value.includes("?")) {
192
+ result[key] = redactUrl(value);
193
+ } else {
194
+ try {
195
+ result[key] = visulimaRedact(value, BROWSER_SECURITY_RULES);
196
+ } catch {
197
+ result[key] = value;
198
+ }
199
+ }
200
+ } else {
201
+ result[key] = value;
202
+ }
203
+ }
204
+ return result;
205
+ }
206
+
207
+ // packages/core/src/safety.ts
208
+ function safeJsonParse(raw, fallback) {
209
+ if (raw === null || raw === void 0) return fallback;
210
+ if (typeof raw !== "string") return typeof raw === "object" ? raw : fallback;
211
+ try {
212
+ return JSON.parse(raw);
213
+ } catch {
214
+ return fallback;
215
+ }
216
+ }
217
+ function safeJsonStringify(val, fallback = "{}") {
218
+ if (val === void 0) return fallback;
219
+ try {
220
+ const seen = /* @__PURE__ */ new WeakSet();
221
+ return JSON.stringify(val, (key, value) => {
222
+ if (typeof value === "object" && value !== null) {
223
+ if (seen.has(value)) {
224
+ return "[Circular]";
225
+ }
226
+ seen.add(value);
227
+ }
228
+ if (typeof value === "bigint") {
229
+ return value.toString();
230
+ }
231
+ return value;
232
+ });
233
+ } catch {
234
+ try {
235
+ return JSON.stringify(String(val));
236
+ } catch {
237
+ return fallback;
238
+ }
239
+ }
240
+ }
241
+
242
+ // packages/daemon/src/storage/db.ts
31
243
  var StorageDB = class {
32
244
  db;
33
245
  constructor(dbPath) {
@@ -36,6 +248,7 @@ var StorageDB = class {
36
248
  this.db = new Database(dbPath);
37
249
  this.db.pragma("journal_mode = WAL");
38
250
  this.db.pragma("synchronous = NORMAL");
251
+ this.db.pragma("busy_timeout = 5000");
39
252
  this.initTables();
40
253
  }
41
254
  initTables() {
@@ -167,18 +380,36 @@ var StorageDB = class {
167
380
  CREATE INDEX IF NOT EXISTS idx_incidents_project ON incidents(project_id, status);
168
381
  CREATE INDEX IF NOT EXISTS idx_incidents_fp ON incidents(fingerprint);
169
382
  CREATE INDEX IF NOT EXISTS idx_notes_project ON notes(project_id, status);
170
- CREATE INDEX IF NOT EXISTS idx_notes_scenario ON notes(scenario_id, step_number);
171
383
  `);
172
384
  try {
173
- this.db.exec("ALTER TABLE notes ADD COLUMN scenario_id TEXT;");
174
- } catch {
175
- }
176
- try {
177
- this.db.exec("ALTER TABLE notes ADD COLUMN step_number INTEGER;");
385
+ const columns = this.db.prepare("PRAGMA table_info(notes)").all().map(
386
+ (c) => c.name
387
+ );
388
+ if (!columns.includes("scenario_id")) {
389
+ this.db.exec("ALTER TABLE notes ADD COLUMN scenario_id TEXT;");
390
+ }
391
+ if (!columns.includes("step_number")) {
392
+ this.db.exec("ALTER TABLE notes ADD COLUMN step_number INTEGER;");
393
+ }
394
+ if (!columns.includes("scenario_title")) {
395
+ this.db.exec("ALTER TABLE notes ADD COLUMN scenario_title TEXT;");
396
+ }
178
397
  } catch {
398
+ try {
399
+ this.db.exec("ALTER TABLE notes ADD COLUMN scenario_id TEXT;");
400
+ } catch {
401
+ }
402
+ try {
403
+ this.db.exec("ALTER TABLE notes ADD COLUMN step_number INTEGER;");
404
+ } catch {
405
+ }
406
+ try {
407
+ this.db.exec("ALTER TABLE notes ADD COLUMN scenario_title TEXT;");
408
+ } catch {
409
+ }
179
410
  }
180
411
  try {
181
- this.db.exec("ALTER TABLE notes ADD COLUMN scenario_title TEXT;");
412
+ this.db.exec("CREATE INDEX IF NOT EXISTS idx_notes_scenario ON notes(scenario_id, step_number);");
182
413
  } catch {
183
414
  }
184
415
  }
@@ -343,7 +574,7 @@ var StorageDB = class {
343
574
  id: r.id,
344
575
  sessionId: r.session_id,
345
576
  eventType: r.event_type,
346
- payload: JSON.parse(r.payload),
577
+ payload: safeJsonParse(r.payload, {}),
347
578
  timestamp: r.timestamp,
348
579
  route: r.route,
349
580
  url: r.url
@@ -404,9 +635,9 @@ var StorageDB = class {
404
635
  incident.occurrences,
405
636
  incident.status,
406
637
  incident.stack || null,
407
- JSON.stringify(incident.breadcrumbs || []),
408
- JSON.stringify(incident.networkFailures || []),
409
- incident.lastElement ? JSON.stringify(incident.lastElement) : null,
638
+ safeJsonStringify(incident.breadcrumbs || []),
639
+ safeJsonStringify(incident.networkFailures || []),
640
+ incident.lastElement ? safeJsonStringify(incident.lastElement) : null,
410
641
  incident.screenshots?.error || null
411
642
  );
412
643
  }
@@ -420,8 +651,8 @@ var StorageDB = class {
420
651
  update.lastSeen,
421
652
  update.occurrences,
422
653
  update.route,
423
- JSON.stringify(update.breadcrumbs || []),
424
- update.lastElement ? JSON.stringify(update.lastElement) : null,
654
+ safeJsonStringify(update.breadcrumbs || []),
655
+ update.lastElement ? safeJsonStringify(update.lastElement) : null,
425
656
  update.stack || null,
426
657
  incidentId
427
658
  );
@@ -441,8 +672,8 @@ var StorageDB = class {
441
672
  occurrence.route,
442
673
  occurrence.url,
443
674
  occurrence.stack || null,
444
- JSON.stringify(occurrence.breadcrumbs || []),
445
- occurrence.lastElement ? JSON.stringify(occurrence.lastElement) : null
675
+ safeJsonStringify(occurrence.breadcrumbs || []),
676
+ occurrence.lastElement ? safeJsonStringify(occurrence.lastElement) : null
446
677
  );
447
678
  }
448
679
  // --- VERIFICATIONS ---
@@ -454,7 +685,7 @@ var StorageDB = class {
454
685
  v.id,
455
686
  v.incidentId,
456
687
  v.status,
457
- JSON.stringify(v.checks),
688
+ safeJsonStringify(v.checks),
458
689
  v.beforeScreenshot || null,
459
690
  v.afterScreenshot || null,
460
691
  v.message || null,
@@ -467,7 +698,7 @@ var StorageDB = class {
467
698
  return {
468
699
  incidentId: row.incident_id,
469
700
  status: row.status,
470
- checks: JSON.parse(row.checks || "[]"),
701
+ checks: safeJsonParse(row.checks, []),
471
702
  screenshots: {
472
703
  before: row.before_screenshot || void 0,
473
704
  after: row.after_screenshot || void 0
@@ -492,11 +723,11 @@ var StorageDB = class {
492
723
  note.message,
493
724
  note.route,
494
725
  note.url,
495
- JSON.stringify(note.viewport),
496
- JSON.stringify(note.scroll),
497
- note.target ? JSON.stringify(note.target) : null,
498
- note.elementContext ? JSON.stringify(note.elementContext) : null,
499
- note.region ? JSON.stringify(note.region) : null,
726
+ safeJsonStringify(note.viewport),
727
+ safeJsonStringify(note.scroll),
728
+ note.target ? safeJsonStringify(note.target) : null,
729
+ note.elementContext ? safeJsonStringify(note.elementContext) : null,
730
+ note.region ? safeJsonStringify(note.region) : null,
500
731
  note.screenshots?.original || null,
501
732
  note.incidentId || null,
502
733
  note.scenarioId || null,
@@ -637,8 +868,8 @@ var StorageDB = class {
637
868
  `nver_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
638
869
  v.noteId,
639
870
  v.status,
640
- JSON.stringify(v.checks),
641
- v.geometryDiff ? JSON.stringify(v.geometryDiff) : null,
871
+ safeJsonStringify(v.checks),
872
+ v.geometryDiff ? safeJsonStringify(v.geometryDiff) : null,
642
873
  v.screenshots?.before || null,
643
874
  v.screenshots?.after || null,
644
875
  v.message || null,
@@ -651,8 +882,8 @@ var StorageDB = class {
651
882
  return {
652
883
  noteId: row.note_id,
653
884
  status: row.status,
654
- checks: JSON.parse(row.checks || "[]"),
655
- geometryDiff: row.geometry_diff ? JSON.parse(row.geometry_diff) : void 0,
885
+ checks: safeJsonParse(row.checks, []),
886
+ geometryDiff: safeJsonParse(row.geometry_diff, void 0),
656
887
  screenshots: {
657
888
  before: row.before_screenshot || void 0,
658
889
  after: row.after_screenshot || void 0
@@ -693,9 +924,9 @@ var StorageDB = class {
693
924
  occurrences: row.occurrences,
694
925
  status: row.status,
695
926
  stack: row.stack || void 0,
696
- breadcrumbs: JSON.parse(row.breadcrumbs || "[]"),
697
- networkFailures: JSON.parse(row.network_failures || "[]"),
698
- lastElement: row.last_element ? JSON.parse(row.last_element) : void 0,
927
+ breadcrumbs: safeJsonParse(row.breadcrumbs, []),
928
+ networkFailures: safeJsonParse(row.network_failures, []),
929
+ lastElement: safeJsonParse(row.last_element, void 0),
699
930
  screenshots: row.screenshot_path ? {
700
931
  error: row.screenshot_path
701
932
  } : void 0
@@ -710,11 +941,11 @@ var StorageDB = class {
710
941
  message: row.message,
711
942
  route: row.route,
712
943
  url: row.url,
713
- viewport: JSON.parse(row.viewport_json || "{}"),
714
- scroll: JSON.parse(row.scroll_json || "{}"),
715
- target: row.target_json ? JSON.parse(row.target_json) : void 0,
716
- elementContext: row.element_context_json ? JSON.parse(row.element_context_json) : void 0,
717
- region: row.region_json ? JSON.parse(row.region_json) : void 0,
944
+ viewport: safeJsonParse(row.viewport_json, { width: 0, height: 0, devicePixelRatio: 1 }),
945
+ scroll: safeJsonParse(row.scroll_json, { scrollX: 0, scrollY: 0 }),
946
+ target: safeJsonParse(row.target_json, void 0),
947
+ elementContext: safeJsonParse(row.element_context_json, void 0),
948
+ region: safeJsonParse(row.region_json, void 0),
718
949
  status: row.status,
719
950
  incidentId: row.incident_id || void 0,
720
951
  scenarioId: row.scenario_id || void 0,
@@ -893,183 +1124,6 @@ var SessionManager = class {
893
1124
 
894
1125
  // packages/daemon/src/incidents/engine.ts
895
1126
  import crypto from "crypto";
896
-
897
- // packages/core/src/fingerprint.ts
898
- function normalizeErrorMessage(message) {
899
- if (!message) return "unknown_error";
900
- 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, " ");
901
- }
902
- function normalizeSourceFile(filename) {
903
- if (!filename) return "unknown_source";
904
- let cleaned = filename.trim();
905
- try {
906
- if (cleaned.startsWith("http://") || cleaned.startsWith("https://")) {
907
- const url = new URL(cleaned);
908
- cleaned = url.pathname;
909
- }
910
- } catch {
911
- cleaned = cleaned.replace(/^https?:\/\/[^/]+/, "");
912
- }
913
- cleaned = cleaned.split("?")[0].split("#")[0];
914
- cleaned = cleaned.replace(/\\/g, "/");
915
- return cleaned || "unknown_source";
916
- }
917
- function extractSourceFromStack(stack) {
918
- if (!stack) return null;
919
- const lines = stack.split("\n");
920
- for (const line of lines) {
921
- const match = line.match(/(?:at\s+(?:.*?\s+\()?)?(https?:\/\/[^\s)]+|file:\/\/[^\s)]+|\/[^\s)]+):(\d+):(\d+)\)?/);
922
- if (match) {
923
- return {
924
- file: normalizeSourceFile(match[1]),
925
- line: parseInt(match[2], 10),
926
- column: parseInt(match[3], 10)
927
- };
928
- }
929
- }
930
- return null;
931
- }
932
- function djb2Hash(str) {
933
- let hash = 5381;
934
- for (let i = 0; i < str.length; i++) {
935
- hash = hash * 33 ^ str.charCodeAt(i);
936
- }
937
- return (hash >>> 0).toString(16).padStart(8, "0");
938
- }
939
- function computeFingerprint(input) {
940
- const normType = (input.type || "Error").trim().toLowerCase();
941
- const normMsg = normalizeErrorMessage(input.message);
942
- let sourceFile = normalizeSourceFile(input.sourceFile);
943
- let line = input.line || 0;
944
- if ((sourceFile === "unknown_source" || line === 0) && input.stack) {
945
- const extracted = extractSourceFromStack(input.stack);
946
- if (extracted) {
947
- sourceFile = extracted.file;
948
- line = extracted.line;
949
- }
950
- }
951
- const rawKey = `${normType}::${normMsg}::${sourceFile}::${line}`;
952
- const hash = djb2Hash(rawKey);
953
- return `fp_${hash}`;
954
- }
955
-
956
- // packages/core/src/redaction.ts
957
- import { redact as visulimaRedact, standardRules } from "@visulima/redact";
958
- var REDACTED_PLACEHOLDER = "[REDACTED]";
959
- var SENSITIVE_KEY_PATTERNS = [
960
- /^authorization$/i,
961
- /^cookie$/i,
962
- /^set-cookie$/i,
963
- /password/i,
964
- /token/i,
965
- /secret/i,
966
- /api[-_]?key/i,
967
- /access[-_]?token/i,
968
- /refresh[-_]?token/i,
969
- /credentials/i,
970
- /private[-_]?key/i,
971
- /ssn/i,
972
- /credit[-_]?card/i,
973
- /cvv/i
974
- ];
975
- var SENSITIVE_QUERY_PARAMS = [
976
- "token",
977
- "auth",
978
- "key",
979
- "apikey",
980
- "api_key",
981
- "secret",
982
- "password",
983
- "access_token",
984
- "refresh_token",
985
- "code",
986
- "signature"
987
- ];
988
- var BROWSER_SECURITY_RULES = [
989
- { deep: true, key: "password", replacement: REDACTED_PLACEHOLDER },
990
- { deep: true, key: "secret", replacement: REDACTED_PLACEHOLDER },
991
- { deep: true, key: "token", replacement: REDACTED_PLACEHOLDER },
992
- { deep: true, key: "authorization", replacement: REDACTED_PLACEHOLDER },
993
- { deep: true, key: "cookie", replacement: REDACTED_PLACEHOLDER },
994
- { deep: true, key: "set-cookie", replacement: REDACTED_PLACEHOLDER },
995
- { deep: true, key: "apikey", replacement: REDACTED_PLACEHOLDER },
996
- { deep: true, key: "api_key", replacement: REDACTED_PLACEHOLDER },
997
- { deep: true, key: "creditcard", pattern: /(?:\d[ -]*?){13,16}/, replacement: REDACTED_PLACEHOLDER },
998
- { deep: true, key: "cvv", replacement: REDACTED_PLACEHOLDER },
999
- { deep: true, key: "ssn", pattern: /\b\d{3}-\d{2}-\d{4}\b/, replacement: REDACTED_PLACEHOLDER },
1000
- { deep: true, key: "awsid", pattern: /\bAKIA[0-9A-Z]{16}\b/, replacement: REDACTED_PLACEHOLDER },
1001
- { deep: true, key: "awskey", pattern: /\b[0-9a-zA-Z/+]{40}\b/, replacement: REDACTED_PLACEHOLDER },
1002
- { deep: true, key: "jwt", pattern: /\beyJ[0-9a-zA-Z_\-]*\.[0-9a-zA-Z_\-]*\.[0-9a-zA-Z_\-]*\b/, replacement: REDACTED_PLACEHOLDER },
1003
- { deep: true, key: "slack_token", pattern: /\bxox[baprs]-[0-9a-zA-Z]{10,48}\b/, replacement: REDACTED_PLACEHOLDER }
1004
- ];
1005
- function isSensitiveKey(key) {
1006
- if (!key) return false;
1007
- const cleaned = key.replace(/[-_]/g, "");
1008
- return SENSITIVE_KEY_PATTERNS.some((pattern) => pattern.test(key) || pattern.test(cleaned));
1009
- }
1010
- function redactUrl(rawUrl) {
1011
- if (!rawUrl) return rawUrl;
1012
- try {
1013
- const isRelative = !rawUrl.startsWith("http://") && !rawUrl.startsWith("https://") && !rawUrl.startsWith("ws://") && !rawUrl.startsWith("wss://");
1014
- const base = "http://localhost";
1015
- const parsed = new URL(rawUrl, base);
1016
- let changed = false;
1017
- for (const param of SENSITIVE_QUERY_PARAMS) {
1018
- if (parsed.searchParams.has(param)) {
1019
- parsed.searchParams.set(param, REDACTED_PLACEHOLDER);
1020
- changed = true;
1021
- }
1022
- }
1023
- for (const key of Array.from(parsed.searchParams.keys())) {
1024
- if (isSensitiveKey(key)) {
1025
- parsed.searchParams.set(key, REDACTED_PLACEHOLDER);
1026
- changed = true;
1027
- }
1028
- }
1029
- if (!changed) return rawUrl;
1030
- let result = isRelative ? parsed.pathname + parsed.search + parsed.hash : parsed.toString();
1031
- result = result.replace(/%5BREDACTED%5D/g, REDACTED_PLACEHOLDER);
1032
- return result;
1033
- } catch {
1034
- let safe = rawUrl;
1035
- for (const param of SENSITIVE_QUERY_PARAMS) {
1036
- const reg = new RegExp(`([?&]${param}=)[^&#]+`, "gi");
1037
- safe = safe.replace(reg, `$1${REDACTED_PLACEHOLDER}`);
1038
- }
1039
- return safe;
1040
- }
1041
- }
1042
- function redactSensitiveData(data, maxDepth = 6, currentDepth = 0) {
1043
- if (data === null || data === void 0) return data;
1044
- if (typeof data !== "object") return data;
1045
- if (currentDepth > maxDepth) return "[DEPTH_EXCEEDED]";
1046
- if (Array.isArray(data)) {
1047
- return data.map((item) => redactSensitiveData(item, maxDepth, currentDepth + 1));
1048
- }
1049
- const result = {};
1050
- for (const [key, value] of Object.entries(data)) {
1051
- if (typeof value === "object" && value !== null) {
1052
- result[key] = redactSensitiveData(value, maxDepth, currentDepth + 1);
1053
- } else if (isSensitiveKey(key)) {
1054
- result[key] = REDACTED_PLACEHOLDER;
1055
- } else if (typeof value === "string") {
1056
- if (value.startsWith("http://") || value.startsWith("https://") || value.includes("?")) {
1057
- result[key] = redactUrl(value);
1058
- } else {
1059
- try {
1060
- result[key] = visulimaRedact(value, BROWSER_SECURITY_RULES);
1061
- } catch {
1062
- result[key] = value;
1063
- }
1064
- }
1065
- } else {
1066
- result[key] = value;
1067
- }
1068
- }
1069
- return result;
1070
- }
1071
-
1072
- // packages/daemon/src/incidents/engine.ts
1073
1127
  var IncidentEngine = class {
1074
1128
  db;
1075
1129
  screenshotStore;
@@ -1657,141 +1711,254 @@ var VerificationEngine = class {
1657
1711
  import fs4 from "fs";
1658
1712
  import path5 from "path";
1659
1713
  import { fileURLToPath } from "url";
1660
- function createHttpHandler(db, sessionManager, baseScreenshotsDir) {
1661
- return (req, res) => {
1662
- const parsedUrl = new URL(req.url || "/", `http://${req.headers.host || "127.0.0.1"}`);
1663
- const pathname = parsedUrl.pathname;
1664
- res.setHeader("Access-Control-Allow-Origin", "*");
1665
- res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
1666
- res.setHeader("Access-Control-Allow-Headers", "Content-Type");
1667
- if (req.method === "OPTIONS") {
1668
- res.writeHead(204);
1669
- res.end();
1670
- return;
1671
- }
1672
- if (pathname === "/health" || pathname === "/") {
1673
- res.writeHead(200, { "Content-Type": "application/json" });
1674
- res.end(
1675
- JSON.stringify({
1676
- status: "ok",
1677
- name: "browsertrack",
1678
- version: "0.1.0",
1679
- activeSessions: sessionManager.getActiveCount(),
1680
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
1681
- })
1682
- );
1683
- return;
1684
- }
1685
- if (pathname === "/client.js" || pathname === "/browserdiag.js") {
1686
- let scriptContent = "";
1714
+ function readJsonBody(req, res, maxSizeBytes = 10 * 1024 * 1024) {
1715
+ return new Promise((resolve, reject) => {
1716
+ let body = "";
1717
+ let isTooLarge = false;
1718
+ req.on("data", (chunk) => {
1719
+ if (isTooLarge) return;
1720
+ body += chunk;
1721
+ if (body.length > maxSizeBytes) {
1722
+ isTooLarge = true;
1723
+ if (!res.headersSent) {
1724
+ res.writeHead(413, { "Content-Type": "application/json" });
1725
+ res.end(JSON.stringify({ ok: false, error: "Payload too large" }));
1726
+ }
1727
+ req.destroy();
1728
+ reject(new Error("Payload too large"));
1729
+ }
1730
+ });
1731
+ req.on("end", () => {
1732
+ if (isTooLarge) return;
1687
1733
  try {
1688
- const __filename = fileURLToPath(import.meta.url);
1689
- const __dirname = path5.dirname(__filename);
1690
- const candidatePaths = [
1691
- path5.resolve(__dirname, "../client.iife.js"),
1692
- path5.resolve(__dirname, "../../dist/client.iife.js"),
1693
- path5.resolve(__dirname, "../../../dist/client.iife.js")
1694
- ];
1695
- for (const p of candidatePaths) {
1696
- if (fs4.existsSync(p)) {
1697
- scriptContent = fs4.readFileSync(p, "utf-8");
1698
- break;
1734
+ const parsed = body ? JSON.parse(body) : {};
1735
+ resolve(parsed);
1736
+ } catch (err) {
1737
+ if (!res.headersSent) {
1738
+ res.writeHead(400, { "Content-Type": "application/json" });
1739
+ res.end(JSON.stringify({ ok: false, error: "Malformed JSON payload" }));
1740
+ }
1741
+ reject(err);
1742
+ }
1743
+ });
1744
+ req.on("error", (err) => {
1745
+ reject(err);
1746
+ });
1747
+ });
1748
+ }
1749
+ function createHttpHandler(db, sessionManager, baseScreenshotsDir, verificationEngine, noteVerificationEngine) {
1750
+ return async (req, res) => {
1751
+ try {
1752
+ const parsedUrl = new URL(req.url || "/", `http://${req.headers.host || "127.0.0.1"}`);
1753
+ const pathname = parsedUrl.pathname;
1754
+ res.setHeader("Access-Control-Allow-Origin", "*");
1755
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
1756
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
1757
+ if (req.method === "OPTIONS") {
1758
+ res.writeHead(204);
1759
+ res.end();
1760
+ return;
1761
+ }
1762
+ if (pathname === "/health" || pathname === "/") {
1763
+ res.writeHead(200, { "Content-Type": "application/json" });
1764
+ res.end(
1765
+ JSON.stringify({
1766
+ status: "ok",
1767
+ name: "browsertrack",
1768
+ version: "0.1.0",
1769
+ activeSessions: sessionManager.getActiveCount(),
1770
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1771
+ })
1772
+ );
1773
+ return;
1774
+ }
1775
+ if (pathname === "/client.js" || pathname === "/browserdiag.js") {
1776
+ let scriptContent = "";
1777
+ try {
1778
+ const __filename = fileURLToPath(import.meta.url);
1779
+ const __dirname = path5.dirname(__filename);
1780
+ const candidatePaths = [
1781
+ path5.resolve(__dirname, "../client.iife.js"),
1782
+ path5.resolve(__dirname, "../../dist/client.iife.js"),
1783
+ path5.resolve(__dirname, "../../../dist/client.iife.js")
1784
+ ];
1785
+ for (const p of candidatePaths) {
1786
+ if (fs4.existsSync(p)) {
1787
+ scriptContent = fs4.readFileSync(p, "utf-8");
1788
+ break;
1789
+ }
1699
1790
  }
1791
+ } catch {
1700
1792
  }
1701
- } catch {
1793
+ if (!scriptContent) {
1794
+ scriptContent = `console.warn("[BrowserTrack] Standalone client bundle not built yet. Run 'npm run build'.");`;
1795
+ }
1796
+ res.writeHead(200, {
1797
+ "Content-Type": "application/javascript; charset=utf-8",
1798
+ "Cache-Control": "no-cache"
1799
+ });
1800
+ res.end(scriptContent);
1801
+ return;
1702
1802
  }
1703
- if (!scriptContent) {
1704
- scriptContent = `console.warn("[BrowserTrack] Standalone client bundle not built yet. Run 'npm run build'.");`;
1803
+ if (pathname === "/api/projects") {
1804
+ const projects = db.listProjects();
1805
+ res.writeHead(200, { "Content-Type": "application/json" });
1806
+ res.end(JSON.stringify({ ok: true, projects }));
1807
+ return;
1705
1808
  }
1706
- res.writeHead(200, {
1707
- "Content-Type": "application/javascript; charset=utf-8",
1708
- "Cache-Control": "no-cache"
1709
- });
1710
- res.end(scriptContent);
1711
- return;
1712
- }
1713
- if (pathname === "/api/projects") {
1714
- const projects = db.listProjects();
1715
- res.writeHead(200, { "Content-Type": "application/json" });
1716
- res.end(JSON.stringify({ ok: true, projects }));
1717
- return;
1718
- }
1719
- if (pathname === "/api/sessions") {
1720
- const projectId = parsedUrl.searchParams.get("project") || void 0;
1721
- const activeOnly = parsedUrl.searchParams.get("active") === "true";
1722
- const sessions = db.listSessions(projectId, activeOnly);
1723
- res.writeHead(200, { "Content-Type": "application/json" });
1724
- res.end(JSON.stringify({ ok: true, sessions }));
1725
- return;
1726
- }
1727
- if (pathname === "/api/incidents") {
1728
- const projectId = parsedUrl.searchParams.get("project") || void 0;
1729
- const status = parsedUrl.searchParams.get("status") || void 0;
1730
- const limit = parseInt(parsedUrl.searchParams.get("limit") || "50", 10);
1731
- const incidents = db.listIncidents({ projectId, status, limit });
1732
- res.writeHead(200, { "Content-Type": "application/json" });
1733
- res.end(JSON.stringify({ ok: true, incidents }));
1734
- return;
1735
- }
1736
- if (pathname.startsWith("/api/incidents/")) {
1737
- const incidentId = pathname.replace("/api/incidents/", "");
1738
- const incident = db.getIncident(incidentId);
1739
- if (!incident) {
1740
- res.writeHead(404, { "Content-Type": "application/json" });
1741
- res.end(JSON.stringify({ ok: false, error: "Incident not found" }));
1809
+ if (pathname === "/api/sessions") {
1810
+ const projectId = parsedUrl.searchParams.get("project") || void 0;
1811
+ const activeOnly = parsedUrl.searchParams.get("active") === "true";
1812
+ const sessions = db.listSessions(projectId, activeOnly);
1813
+ res.writeHead(200, { "Content-Type": "application/json" });
1814
+ res.end(JSON.stringify({ ok: true, sessions }));
1742
1815
  return;
1743
1816
  }
1744
- res.writeHead(200, { "Content-Type": "application/json" });
1745
- res.end(JSON.stringify({ ok: true, incident }));
1746
- return;
1747
- }
1748
- if (pathname === "/api/notes") {
1749
- const projectId = parsedUrl.searchParams.get("project") || void 0;
1750
- const status = parsedUrl.searchParams.get("status") || void 0;
1751
- const limit = parseInt(parsedUrl.searchParams.get("limit") || "50", 10);
1752
- const notes = db.listNotes({ projectId, status, limit });
1753
- res.writeHead(200, { "Content-Type": "application/json" });
1754
- res.end(JSON.stringify({ ok: true, notes }));
1755
- return;
1756
- }
1757
- if (pathname.startsWith("/api/notes/")) {
1758
- const noteId = pathname.replace("/api/notes/", "");
1759
- const note = db.getNote(noteId);
1760
- if (!note) {
1817
+ if (pathname === "/api/incidents") {
1818
+ const projectId = parsedUrl.searchParams.get("project") || void 0;
1819
+ const status = parsedUrl.searchParams.get("status") || void 0;
1820
+ const limit = parseInt(parsedUrl.searchParams.get("limit") || "50", 10);
1821
+ const incidents = db.listIncidents({ projectId, status, limit });
1822
+ res.writeHead(200, { "Content-Type": "application/json" });
1823
+ res.end(JSON.stringify({ ok: true, incidents }));
1824
+ return;
1825
+ }
1826
+ if (pathname.startsWith("/api/incidents/")) {
1827
+ const incidentId = pathname.replace("/api/incidents/", "");
1828
+ const incident = db.getIncident(incidentId);
1829
+ if (!incident) {
1830
+ res.writeHead(404, { "Content-Type": "application/json" });
1831
+ res.end(JSON.stringify({ ok: false, error: "Incident not found" }));
1832
+ return;
1833
+ }
1834
+ res.writeHead(200, { "Content-Type": "application/json" });
1835
+ res.end(JSON.stringify({ ok: true, incident }));
1836
+ return;
1837
+ }
1838
+ if (pathname === "/api/notes") {
1839
+ const projectId = parsedUrl.searchParams.get("project") || void 0;
1840
+ const status = parsedUrl.searchParams.get("status") || void 0;
1841
+ const limit = parseInt(parsedUrl.searchParams.get("limit") || "50", 10);
1842
+ const notes = db.listNotes({ projectId, status, limit });
1843
+ res.writeHead(200, { "Content-Type": "application/json" });
1844
+ res.end(JSON.stringify({ ok: true, notes }));
1845
+ return;
1846
+ }
1847
+ if (pathname.startsWith("/api/notes/")) {
1848
+ const noteId = pathname.replace("/api/notes/", "");
1849
+ const note = db.getNote(noteId);
1850
+ if (!note) {
1851
+ res.writeHead(404, { "Content-Type": "application/json" });
1852
+ res.end(JSON.stringify({ ok: false, error: "Note not found" }));
1853
+ return;
1854
+ }
1855
+ res.writeHead(200, { "Content-Type": "application/json" });
1856
+ res.end(JSON.stringify({ ok: true, note }));
1857
+ return;
1858
+ }
1859
+ if (pathname.startsWith("/screenshots/")) {
1860
+ const relativePath = pathname.replace("/screenshots/", "");
1861
+ const filePath = path5.join(baseScreenshotsDir, relativePath);
1862
+ if (fs4.existsSync(filePath) && fs4.statSync(filePath).isFile()) {
1863
+ const ext = path5.extname(filePath).toLowerCase();
1864
+ const mimeTypes = {
1865
+ ".webp": "image/webp",
1866
+ ".png": "image/png",
1867
+ ".jpg": "image/jpeg",
1868
+ ".jpeg": "image/jpeg"
1869
+ };
1870
+ res.writeHead(200, { "Content-Type": mimeTypes[ext] || "application/octet-stream" });
1871
+ fs4.createReadStream(filePath).pipe(res);
1872
+ return;
1873
+ }
1761
1874
  res.writeHead(404, { "Content-Type": "application/json" });
1762
- res.end(JSON.stringify({ ok: false, error: "Note not found" }));
1875
+ res.end(JSON.stringify({ ok: false, error: "Screenshot not found" }));
1876
+ return;
1877
+ }
1878
+ if (pathname === "/api/command" && req.method === "POST") {
1879
+ try {
1880
+ const data = await readJsonBody(req, res);
1881
+ const sessionId = data.sessionId || sessionManager.getAnyActiveSession()?.id;
1882
+ if (!sessionId) {
1883
+ res.writeHead(400, { "Content-Type": "application/json" });
1884
+ res.end(JSON.stringify({ ok: false, error: "No active browser session connected" }));
1885
+ return;
1886
+ }
1887
+ const cmdRes = await sessionManager.sendCommand(sessionId, data.command, data.timeoutMs || 5e3);
1888
+ res.writeHead(200, { "Content-Type": "application/json" });
1889
+ res.end(JSON.stringify(cmdRes));
1890
+ } catch (err) {
1891
+ if (!res.headersSent) {
1892
+ res.writeHead(500, { "Content-Type": "application/json" });
1893
+ res.end(JSON.stringify({ ok: false, error: err?.message || String(err) }));
1894
+ }
1895
+ }
1896
+ return;
1897
+ }
1898
+ if (pathname === "/api/verify/incident" && req.method === "POST") {
1899
+ try {
1900
+ if (!verificationEngine) {
1901
+ res.writeHead(500, { "Content-Type": "application/json" });
1902
+ res.end(JSON.stringify({ ok: false, error: "Verification engine not attached to daemon" }));
1903
+ return;
1904
+ }
1905
+ const data = await readJsonBody(req, res);
1906
+ const result = await verificationEngine.verifyIncident(data.incidentId, data.options);
1907
+ res.writeHead(200, { "Content-Type": "application/json" });
1908
+ res.end(JSON.stringify({ ok: true, result }));
1909
+ } catch (err) {
1910
+ if (!res.headersSent) {
1911
+ res.writeHead(500, { "Content-Type": "application/json" });
1912
+ res.end(JSON.stringify({ ok: false, error: err?.message || String(err) }));
1913
+ }
1914
+ }
1763
1915
  return;
1764
1916
  }
1765
- res.writeHead(200, { "Content-Type": "application/json" });
1766
- res.end(JSON.stringify({ ok: true, note }));
1767
- return;
1768
- }
1769
- if (pathname.startsWith("/screenshots/")) {
1770
- const relativePath = pathname.replace("/screenshots/", "");
1771
- const filePath = path5.join(baseScreenshotsDir, relativePath);
1772
- if (fs4.existsSync(filePath) && fs4.statSync(filePath).isFile()) {
1773
- const ext = path5.extname(filePath).toLowerCase();
1774
- const mimeTypes = {
1775
- ".webp": "image/webp",
1776
- ".png": "image/png",
1777
- ".jpg": "image/jpeg",
1778
- ".jpeg": "image/jpeg"
1779
- };
1780
- res.writeHead(200, { "Content-Type": mimeTypes[ext] || "application/octet-stream" });
1781
- fs4.createReadStream(filePath).pipe(res);
1917
+ if (pathname === "/api/verify/note" && req.method === "POST") {
1918
+ try {
1919
+ if (!noteVerificationEngine) {
1920
+ res.writeHead(500, { "Content-Type": "application/json" });
1921
+ res.end(JSON.stringify({ ok: false, error: "Note verification engine not attached to daemon" }));
1922
+ return;
1923
+ }
1924
+ const data = await readJsonBody(req, res);
1925
+ const result = await noteVerificationEngine.verifyNote(data.noteId, data.options);
1926
+ res.writeHead(200, { "Content-Type": "application/json" });
1927
+ res.end(JSON.stringify({ ok: true, result }));
1928
+ } catch (err) {
1929
+ if (!res.headersSent) {
1930
+ res.writeHead(500, { "Content-Type": "application/json" });
1931
+ res.end(JSON.stringify({ ok: false, error: err?.message || String(err) }));
1932
+ }
1933
+ }
1782
1934
  return;
1783
1935
  }
1784
1936
  res.writeHead(404, { "Content-Type": "application/json" });
1785
- res.end(JSON.stringify({ ok: false, error: "Screenshot not found" }));
1786
- return;
1937
+ res.end(JSON.stringify({ ok: false, error: "Not found" }));
1938
+ } catch (err) {
1939
+ if (!res.headersSent) {
1940
+ try {
1941
+ res.writeHead(500, { "Content-Type": "application/json" });
1942
+ res.end(JSON.stringify({ ok: false, error: err?.message || "Internal Server Error" }));
1943
+ } catch {
1944
+ }
1945
+ }
1787
1946
  }
1788
- res.writeHead(404, { "Content-Type": "application/json" });
1789
- res.end(JSON.stringify({ ok: false, error: "Not found" }));
1790
1947
  };
1791
1948
  }
1792
1949
 
1793
1950
  // packages/daemon/src/server/ws.ts
1794
1951
  import crypto5 from "crypto";
1952
+ function safeWsSend(ws, payload) {
1953
+ if (ws.readyState !== 1) return false;
1954
+ try {
1955
+ const raw = typeof payload === "string" ? payload : safeJsonStringify(payload);
1956
+ ws.send(raw);
1957
+ return true;
1958
+ } catch {
1959
+ return false;
1960
+ }
1961
+ }
1795
1962
  function setupWebSocketServer(wss, db, sessionManager, incidentEngine, notesEngine, maxEventsPerSession = 1e3, verbose = false) {
1796
1963
  wss.on("connection", (ws) => {
1797
1964
  let currentSessionId = null;
@@ -1834,21 +2001,17 @@ function setupWebSocketServer(wss, db, sessionManager, incidentEngine, notesEngi
1834
2001
  active: true
1835
2002
  });
1836
2003
  sessionManager.registerSocket(sessionId, ws, hello.origin || "", project.id);
1837
- ws.send(
1838
- JSON.stringify({
1839
- type: "hello_ack",
1840
- sessionId,
1841
- projectId: project.id,
1842
- projectName: project.name
1843
- })
1844
- );
2004
+ safeWsSend(ws, {
2005
+ type: "hello_ack",
2006
+ sessionId,
2007
+ projectId: project.id,
2008
+ projectName: project.name
2009
+ });
1845
2010
  const existingNotes = db.listNotes({ projectId: project.id, limit: 100 });
1846
- ws.send(
1847
- JSON.stringify({
1848
- type: "notes_sync",
1849
- notes: existingNotes
1850
- })
1851
- );
2011
+ safeWsSend(ws, {
2012
+ type: "notes_sync",
2013
+ notes: existingNotes
2014
+ });
1852
2015
  if (verbose) {
1853
2016
  console.log(`[BrowserTrack] New session connected: ${sessionId} (${project.name} @ ${hello.origin})`);
1854
2017
  }
@@ -1900,15 +2063,13 @@ function setupWebSocketServer(wss, db, sessionManager, incidentEngine, notesEngi
1900
2063
  `[BrowserTrack] Visual note created: ${note.id} on ${note.route} ("${note.message}")${note.scenarioId ? ` [Scenario: ${note.scenarioTitle || note.scenarioId} Step ${note.stepNumber}]` : ""}`
1901
2064
  );
1902
2065
  }
1903
- ws.send(
1904
- JSON.stringify({
1905
- type: "note_created_ack",
1906
- noteId: note.id,
1907
- status: note.status,
1908
- scenarioId: note.scenarioId,
1909
- stepNumber: note.stepNumber
1910
- })
1911
- );
2066
+ safeWsSend(ws, {
2067
+ type: "note_created_ack",
2068
+ noteId: note.id,
2069
+ status: note.status,
2070
+ scenarioId: note.scenarioId,
2071
+ stepNumber: note.stepNumber
2072
+ });
1912
2073
  const allNotes = db.listNotes({ projectId: note.projectId, limit: 100 });
1913
2074
  sessionManager.broadcastToProject(note.projectId, {
1914
2075
  type: "notes_sync",
@@ -1969,12 +2130,10 @@ function setupWebSocketServer(wss, db, sessionManager, incidentEngine, notesEngi
1969
2130
  const projectId = data.projectId || session?.projectId;
1970
2131
  if (projectId) {
1971
2132
  const allNotes = db.listNotes({ projectId, limit: 100 });
1972
- ws.send(
1973
- JSON.stringify({
1974
- type: "notes_sync",
1975
- notes: allNotes
1976
- })
1977
- );
2133
+ safeWsSend(ws, {
2134
+ type: "notes_sync",
2135
+ notes: allNotes
2136
+ });
1978
2137
  }
1979
2138
  return;
1980
2139
  }
@@ -2035,7 +2194,13 @@ var BrowserTrackDaemon = class {
2035
2194
  }
2036
2195
  async start() {
2037
2196
  if (this.isRunning) return;
2038
- const httpHandler = createHttpHandler(this.db, this.sessionManager, this.config.screenshotsDir);
2197
+ const httpHandler = createHttpHandler(
2198
+ this.db,
2199
+ this.sessionManager,
2200
+ this.config.screenshotsDir,
2201
+ this.verificationEngine,
2202
+ this.noteVerificationEngine
2203
+ );
2039
2204
  this.httpServer = http.createServer(httpHandler);
2040
2205
  this.wss = new WebSocketServer({ server: this.httpServer });
2041
2206
  setupWebSocketServer(
@@ -2106,11 +2271,111 @@ function createDaemon(config = {}) {
2106
2271
  }
2107
2272
 
2108
2273
  // packages/mcp/src/server.ts
2274
+ import fs5 from "fs";
2275
+ import path6 from "path";
2276
+ import { fileURLToPath as fileURLToPath2 } from "url";
2277
+ import { spawn } from "child_process";
2109
2278
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2110
2279
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2111
2280
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
2112
2281
 
2113
2282
  // packages/mcp/src/handlers.ts
2283
+ async function sendSessionCommand(ctx, sessionId, command, timeoutMs = 5e3) {
2284
+ if (ctx.sessionManager && ctx.sessionManager.getActiveCount() > 0) {
2285
+ const targetSession = sessionId ? ctx.db.getSession(sessionId) : ctx.sessionManager.getAnyActiveSession();
2286
+ if (targetSession) {
2287
+ const res = await ctx.sessionManager.sendCommand(targetSession.id, command, timeoutMs);
2288
+ if (!res.ok) {
2289
+ throw new Error(res.error || res.reason || `Command ${command.type} failed`);
2290
+ }
2291
+ return res;
2292
+ }
2293
+ }
2294
+ if (ctx.daemonUrl) {
2295
+ try {
2296
+ const resp = await fetch(`${ctx.daemonUrl}/api/command`, {
2297
+ method: "POST",
2298
+ headers: { "Content-Type": "application/json" },
2299
+ body: JSON.stringify({ sessionId, command, timeoutMs }),
2300
+ signal: AbortSignal.timeout(timeoutMs + 2e3)
2301
+ });
2302
+ if (resp.ok) {
2303
+ const res = await resp.json();
2304
+ if (!res.ok) {
2305
+ throw new Error(res.error || res.reason || `Command ${command.type} failed`);
2306
+ }
2307
+ return res;
2308
+ }
2309
+ const errJson = await resp.json().catch(() => ({}));
2310
+ if (errJson.error) {
2311
+ throw new Error(errJson.error);
2312
+ }
2313
+ } catch (err) {
2314
+ if (err.message && !err.message.includes("fetch failed") && !err.message.includes("ECONNREFUSED")) {
2315
+ throw err;
2316
+ }
2317
+ }
2318
+ }
2319
+ throw new Error(
2320
+ 'No active browser session connected. Please ensure the BrowserTrack daemon is running ("browsertrack start") and your application tab is open in the browser.'
2321
+ );
2322
+ }
2323
+ async function runVerifyIncident(ctx, incidentId, options) {
2324
+ if (ctx.verificationEngine && ctx.sessionManager && ctx.sessionManager.getActiveCount() > 0) {
2325
+ return await ctx.verificationEngine.verifyIncident(incidentId, options);
2326
+ }
2327
+ if (ctx.daemonUrl) {
2328
+ try {
2329
+ const timeoutMs = (options?.observationWindowMs || 3e3) + 7e3;
2330
+ const resp = await fetch(`${ctx.daemonUrl}/api/verify/incident`, {
2331
+ method: "POST",
2332
+ headers: { "Content-Type": "application/json" },
2333
+ body: JSON.stringify({ incidentId, options }),
2334
+ signal: AbortSignal.timeout(timeoutMs)
2335
+ });
2336
+ if (resp.ok) {
2337
+ const data = await resp.json();
2338
+ if (data.ok) return data.result;
2339
+ throw new Error(data.error);
2340
+ }
2341
+ } catch (err) {
2342
+ if (err.message && !err.message.includes("fetch failed") && !err.message.includes("ECONNREFUSED")) {
2343
+ throw err;
2344
+ }
2345
+ }
2346
+ }
2347
+ throw new Error(
2348
+ 'Verification failed: No active browser session connected. Please ensure the BrowserTrack daemon is running ("browsertrack start") and your application tab is open in the browser.'
2349
+ );
2350
+ }
2351
+ async function runVerifyNote(ctx, noteId, options) {
2352
+ if (ctx.noteVerificationEngine && ctx.sessionManager && ctx.sessionManager.getActiveCount() > 0) {
2353
+ return await ctx.noteVerificationEngine.verifyNote(noteId, options);
2354
+ }
2355
+ if (ctx.daemonUrl) {
2356
+ try {
2357
+ const timeoutMs = (options?.observationWindowMs || 3e3) + 7e3;
2358
+ const resp = await fetch(`${ctx.daemonUrl}/api/verify/note`, {
2359
+ method: "POST",
2360
+ headers: { "Content-Type": "application/json" },
2361
+ body: JSON.stringify({ noteId, options }),
2362
+ signal: AbortSignal.timeout(timeoutMs)
2363
+ });
2364
+ if (resp.ok) {
2365
+ const data = await resp.json();
2366
+ if (data.ok) return data.result;
2367
+ throw new Error(data.error);
2368
+ }
2369
+ } catch (err) {
2370
+ if (err.message && !err.message.includes("fetch failed") && !err.message.includes("ECONNREFUSED")) {
2371
+ throw err;
2372
+ }
2373
+ }
2374
+ }
2375
+ throw new Error(
2376
+ 'Verification failed: No active browser session connected. Please ensure the BrowserTrack daemon is running ("browsertrack start") and your application tab is open in the browser.'
2377
+ );
2378
+ }
2114
2379
  async function handleToolCall(name, args, ctx) {
2115
2380
  const { db, sessionManager, verificationEngine, noteVerificationEngine } = ctx;
2116
2381
  switch (name) {
@@ -2195,7 +2460,8 @@ async function handleToolCall(name, args, ctx) {
2195
2460
  tag: incident.lastElement.tag,
2196
2461
  visible: incident.lastElement.visible,
2197
2462
  innerText: incident.lastElement.innerText,
2198
- outerHTML: incident.lastElement.outerHTML
2463
+ outerHTML: incident.lastElement.outerHTML,
2464
+ componentSource: incident.lastElement.componentSource
2199
2465
  } : void 0,
2200
2466
  recentBreadcrumbs: breadcrumbsTimeline,
2201
2467
  networkFailures: incident.networkFailures,
@@ -2259,38 +2525,18 @@ async function handleToolCall(name, args, ctx) {
2259
2525
  };
2260
2526
  }
2261
2527
  case "get_page_state": {
2262
- if (!sessionManager) {
2263
- throw new Error("Live browser connection not available: Daemon session manager not attached.");
2264
- }
2265
- let session = args.sessionId ? db.getSession(args.sessionId) : sessionManager.getAnyActiveSession();
2266
- if (!session) {
2267
- throw new Error("No active browser session connected.");
2268
- }
2269
- const cmdRes = await sessionManager.sendCommand(session.id, {
2528
+ const cmdRes = await sendSessionCommand(ctx, args.sessionId, {
2270
2529
  id: `cmd_mcp_${Date.now()}`,
2271
2530
  type: "get_page_state"
2272
2531
  });
2273
- if (!cmdRes.ok) {
2274
- throw new Error(cmdRes.error || "Failed to retrieve page state from browser.");
2275
- }
2276
2532
  return cmdRes.result;
2277
2533
  }
2278
2534
  case "capture_element": {
2279
- if (!sessionManager) {
2280
- throw new Error("Live browser connection not available: Daemon session manager not attached.");
2281
- }
2282
- let session = args.sessionId ? db.getSession(args.sessionId) : sessionManager.getAnyActiveSession();
2283
- if (!session) {
2284
- throw new Error("No active browser session connected.");
2285
- }
2286
- const cmdRes = await sessionManager.sendCommand(session.id, {
2535
+ const cmdRes = await sendSessionCommand(ctx, args.sessionId, {
2287
2536
  id: `cmd_mcp_${Date.now()}`,
2288
2537
  type: "capture_element",
2289
2538
  params: { selector: args.selector }
2290
2539
  });
2291
- if (!cmdRes.ok) {
2292
- throw new Error(cmdRes.error || cmdRes.reason || "Failed to capture element screenshot.");
2293
- }
2294
2540
  return {
2295
2541
  ok: true,
2296
2542
  format: cmdRes.result?.format || "webp",
@@ -2300,10 +2546,7 @@ async function handleToolCall(name, args, ctx) {
2300
2546
  };
2301
2547
  }
2302
2548
  case "verify_incident": {
2303
- if (!verificationEngine) {
2304
- throw new Error("Verification engine not available: Daemon session manager not attached.");
2305
- }
2306
- const res = await verificationEngine.verifyIncident(args.incidentId, {
2549
+ const res = await runVerifyIncident(ctx, args.incidentId, {
2307
2550
  route: args.route,
2308
2551
  targetSelector: args.targetSelector,
2309
2552
  expect: args.expect,
@@ -2454,10 +2697,7 @@ async function handleToolCall(name, args, ctx) {
2454
2697
  };
2455
2698
  }
2456
2699
  case "verify_note": {
2457
- if (!noteVerificationEngine) {
2458
- throw new Error("Note verification engine not available: Daemon session manager not attached.");
2459
- }
2460
- const res = await noteVerificationEngine.verifyNote(args.noteId, {
2700
+ const res = await runVerifyNote(ctx, args.noteId, {
2461
2701
  observationWindowMs: args.observationWindowMs
2462
2702
  });
2463
2703
  return res;
@@ -2470,24 +2710,17 @@ async function handleToolCall(name, args, ctx) {
2470
2710
  return v;
2471
2711
  }
2472
2712
  case "capture_note_context": {
2473
- if (!sessionManager) {
2474
- throw new Error("Live browser connection not available: Daemon session manager not attached.");
2475
- }
2476
- let session = args.sessionId ? db.getSession(args.sessionId) : sessionManager.getAnyActiveSession();
2477
- if (!session) {
2478
- throw new Error("No active browser session connected.");
2479
- }
2480
- const queryCmd = await sessionManager.sendCommand(session.id, {
2713
+ const queryCmd = await sendSessionCommand(ctx, args.sessionId, {
2481
2714
  id: `cmd_ctx_${Date.now()}`,
2482
2715
  type: "query_element",
2483
2716
  params: { selector: args.selector }
2484
2717
  });
2485
- const overflowCmd = await sessionManager.sendCommand(session.id, {
2718
+ const overflowCmd = await sendSessionCommand(ctx, args.sessionId, {
2486
2719
  id: `cmd_ovf_${Date.now()}`,
2487
2720
  type: "check_overflow",
2488
2721
  params: { selector: args.selector }
2489
2722
  });
2490
- const styleCmd = await sessionManager.sendCommand(session.id, {
2723
+ const styleCmd = await sendSessionCommand(ctx, args.sessionId, {
2491
2724
  id: `cmd_sty_${Date.now()}`,
2492
2725
  type: "get_element_style",
2493
2726
  params: { selector: args.selector }
@@ -2769,8 +3002,76 @@ var TOOLS = [
2769
3002
  ];
2770
3003
 
2771
3004
  // packages/mcp/src/server.ts
3005
+ async function isDaemonRunning(host, port) {
3006
+ try {
3007
+ const res = await fetch(`http://${host}:${port}/health`, {
3008
+ signal: AbortSignal.timeout(600)
3009
+ });
3010
+ if (res.ok) {
3011
+ const data = await res.json().catch(() => ({}));
3012
+ return data?.name === "browsertrack";
3013
+ }
3014
+ } catch {
3015
+ }
3016
+ return false;
3017
+ }
3018
+ function resolveCliPath() {
3019
+ if (process.argv[1]) {
3020
+ const candidate = process.argv[1];
3021
+ if (candidate.endsWith("cli/index.js") || candidate.endsWith("browsertrack") || candidate.endsWith("bin/browsertrack.js") || candidate.endsWith("dist/cli/index.js")) {
3022
+ if (fs5.existsSync(candidate)) return candidate;
3023
+ }
3024
+ }
3025
+ try {
3026
+ const currentDir = path6.dirname(fileURLToPath2(import.meta.url));
3027
+ const paths = [
3028
+ path6.resolve(currentDir, "../cli/index.js"),
3029
+ path6.resolve(currentDir, "../../cli/index.js"),
3030
+ path6.resolve(currentDir, "../../dist/cli/index.js")
3031
+ ];
3032
+ for (const p of paths) {
3033
+ if (fs5.existsSync(p)) return p;
3034
+ }
3035
+ } catch {
3036
+ }
3037
+ return null;
3038
+ }
3039
+ function acquireBootLock(lockFile) {
3040
+ try {
3041
+ fs5.mkdirSync(path6.dirname(lockFile), { recursive: true });
3042
+ const fd = fs5.openSync(lockFile, "wx");
3043
+ fs5.writeSync(fd, String(process.pid));
3044
+ fs5.closeSync(fd);
3045
+ return true;
3046
+ } catch {
3047
+ try {
3048
+ const stats = fs5.statSync(lockFile);
3049
+ if (Date.now() - stats.mtimeMs > 5e3) {
3050
+ fs5.unlinkSync(lockFile);
3051
+ const fd = fs5.openSync(lockFile, "wx");
3052
+ fs5.writeSync(fd, String(process.pid));
3053
+ fs5.closeSync(fd);
3054
+ return true;
3055
+ }
3056
+ } catch {
3057
+ }
3058
+ return false;
3059
+ }
3060
+ }
3061
+ function releaseBootLock(lockFile) {
3062
+ try {
3063
+ if (fs5.existsSync(lockFile)) {
3064
+ fs5.unlinkSync(lockFile);
3065
+ }
3066
+ } catch {
3067
+ }
3068
+ }
2772
3069
  function createMcpServer(options = {}) {
2773
- const config = getDaemonConfig({ dbPath: options.dbPath });
3070
+ const config = getDaemonConfig({
3071
+ dbPath: options.dbPath,
3072
+ port: options.port,
3073
+ host: options.host
3074
+ });
2774
3075
  const db = options.context?.db || new StorageDB(config.dbPath);
2775
3076
  const screenshotStore = new ScreenshotStore(config.screenshotsDir);
2776
3077
  const sessionManager = options.context?.sessionManager || new SessionManager(db);
@@ -2785,6 +3086,96 @@ function createMcpServer(options = {}) {
2785
3086
  daemonUrl: `http://${config.host}:${config.port}`,
2786
3087
  ...options.context
2787
3088
  };
3089
+ let embeddedDaemon = null;
3090
+ async function ensureDaemon() {
3091
+ if (options.autoStartDaemon === false || options.context?.sessionManager) {
3092
+ return;
3093
+ }
3094
+ if (await isDaemonRunning(config.host, config.port)) {
3095
+ console.error(`[BrowserTrack MCP] Connected to active singleton daemon at http://${config.host}:${config.port}`);
3096
+ return;
3097
+ }
3098
+ const lockFile = path6.join(config.dataDir, "daemon_boot.lock");
3099
+ const hasLock = acquireBootLock(lockFile);
3100
+ if (!hasLock) {
3101
+ for (let i = 0; i < 30; i++) {
3102
+ await new Promise((r) => setTimeout(r, 100));
3103
+ if (await isDaemonRunning(config.host, config.port)) {
3104
+ console.error(`[BrowserTrack MCP] Connected to shared singleton daemon at http://${config.host}:${config.port}`);
3105
+ return;
3106
+ }
3107
+ }
3108
+ }
3109
+ try {
3110
+ if (await isDaemonRunning(config.host, config.port)) {
3111
+ console.error(`[BrowserTrack MCP] Connected to shared singleton daemon at http://${config.host}:${config.port}`);
3112
+ return;
3113
+ }
3114
+ if (options.detached !== false) {
3115
+ const cliPath = resolveCliPath();
3116
+ if (cliPath && fs5.existsSync(cliPath)) {
3117
+ try {
3118
+ const child = spawn(
3119
+ process.execPath,
3120
+ [cliPath, "start", "--port", String(config.port), "--host", config.host],
3121
+ {
3122
+ detached: true,
3123
+ stdio: "ignore",
3124
+ env: { ...process.env, BROWSERTRACK_DAEMON_DETACHED: "1" }
3125
+ }
3126
+ );
3127
+ child.unref();
3128
+ for (let i = 0; i < 30; i++) {
3129
+ await new Promise((r) => setTimeout(r, 100));
3130
+ if (await isDaemonRunning(config.host, config.port)) {
3131
+ console.error(
3132
+ `[BrowserTrack MCP] Started singleton background daemon on http://${config.host}:${config.port}`
3133
+ );
3134
+ return;
3135
+ }
3136
+ }
3137
+ } catch (spawnErr) {
3138
+ console.error(
3139
+ `[BrowserTrack MCP] Detached daemon spawn failed (${spawnErr?.message}), falling back to in-process daemon.`
3140
+ );
3141
+ }
3142
+ }
3143
+ }
3144
+ embeddedDaemon = createDaemon({
3145
+ host: config.host,
3146
+ port: config.port,
3147
+ dbPath: config.dbPath,
3148
+ screenshotsDir: config.screenshotsDir,
3149
+ verbose: false
3150
+ });
3151
+ await embeddedDaemon.start();
3152
+ console.error(`[BrowserTrack MCP] Started singleton daemon on http://${config.host}:${config.port}`);
3153
+ ctx.db = embeddedDaemon.db;
3154
+ ctx.sessionManager = embeddedDaemon.sessionManager;
3155
+ ctx.verificationEngine = embeddedDaemon.verificationEngine;
3156
+ ctx.noteVerificationEngine = embeddedDaemon.noteVerificationEngine;
3157
+ const cleanup = () => {
3158
+ if (embeddedDaemon) {
3159
+ try {
3160
+ embeddedDaemon.stop();
3161
+ } catch {
3162
+ }
3163
+ embeddedDaemon = null;
3164
+ }
3165
+ };
3166
+ process.on("exit", cleanup);
3167
+ process.on("SIGINT", cleanup);
3168
+ process.on("SIGTERM", cleanup);
3169
+ } catch (err) {
3170
+ console.error(
3171
+ `[BrowserTrack MCP] Note: Daemon startup skipped (${err?.message}). Running MCP in standalone database mode.`
3172
+ );
3173
+ } finally {
3174
+ if (hasLock) {
3175
+ releaseBootLock(lockFile);
3176
+ }
3177
+ }
3178
+ }
2788
3179
  const server = new Server(
2789
3180
  {
2790
3181
  name: "browsertrack-mcp",
@@ -2825,7 +3216,16 @@ function createMcpServer(options = {}) {
2825
3216
  });
2826
3217
  return {
2827
3218
  server,
3219
+ ctx,
3220
+ ensureDaemon,
3221
+ async stopDaemon() {
3222
+ if (embeddedDaemon) {
3223
+ await embeddedDaemon.stop();
3224
+ embeddedDaemon = null;
3225
+ }
3226
+ },
2828
3227
  async startStdio() {
3228
+ await ensureDaemon();
2829
3229
  const transport = new StdioServerTransport();
2830
3230
  await server.connect(transport);
2831
3231
  }
@@ -2833,22 +3233,28 @@ function createMcpServer(options = {}) {
2833
3233
  }
2834
3234
 
2835
3235
  // packages/cli/src/index.ts
3236
+ process.on("uncaughtException", (err) => {
3237
+ console.error("[BrowserTrack] Uncaught Exception:", err?.message || err);
3238
+ });
3239
+ process.on("unhandledRejection", (reason) => {
3240
+ console.error("[BrowserTrack] Unhandled Rejection:", reason?.message || reason);
3241
+ });
2836
3242
  var program = new Command();
2837
3243
  program.name("browsertrack").description("Local browser diagnostics + MCP bridge for coding agents").version("0.1.0");
2838
3244
  function getPidFilePath() {
2839
3245
  const config = getDaemonConfig();
2840
- return path6.join(config.dataDir, "daemon.pid");
3246
+ return path7.join(config.dataDir, "daemon.pid");
2841
3247
  }
2842
3248
  function savePid(pid) {
2843
3249
  const pidFile = getPidFilePath();
2844
- fs5.mkdirSync(path6.dirname(pidFile), { recursive: true });
2845
- fs5.writeFileSync(pidFile, String(pid), "utf-8");
3250
+ fs6.mkdirSync(path7.dirname(pidFile), { recursive: true });
3251
+ fs6.writeFileSync(pidFile, String(pid), "utf-8");
2846
3252
  }
2847
3253
  function readPid() {
2848
3254
  try {
2849
3255
  const pidFile = getPidFilePath();
2850
- if (fs5.existsSync(pidFile)) {
2851
- const pidStr = fs5.readFileSync(pidFile, "utf-8").trim();
3256
+ if (fs6.existsSync(pidFile)) {
3257
+ const pidStr = fs6.readFileSync(pidFile, "utf-8").trim();
2852
3258
  const pid = parseInt(pidStr, 10);
2853
3259
  if (!isNaN(pid)) return pid;
2854
3260
  }
@@ -2859,8 +3265,8 @@ function readPid() {
2859
3265
  function removePid() {
2860
3266
  try {
2861
3267
  const pidFile = getPidFilePath();
2862
- if (fs5.existsSync(pidFile)) {
2863
- fs5.unlinkSync(pidFile);
3268
+ if (fs6.existsSync(pidFile)) {
3269
+ fs6.unlinkSync(pidFile);
2864
3270
  }
2865
3271
  } catch {
2866
3272
  }
@@ -2880,6 +3286,11 @@ program.command("start").description("Start the local BrowserTrack daemon and HT
2880
3286
  return;
2881
3287
  }
2882
3288
  const port = parseInt(options.port, 10);
3289
+ const isRunningHttp = await isDaemonRunning(options.host, port);
3290
+ if (isRunningHttp) {
3291
+ console.log(`[BrowserTrack] Daemon is already running on http://${options.host}:${port} (active via MCP Server).`);
3292
+ return;
3293
+ }
2883
3294
  const daemon = createDaemon({
2884
3295
  port,
2885
3296
  host: options.host,
@@ -2923,6 +3334,13 @@ program.command("start").description("Start the local BrowserTrack daemon and HT
2923
3334
  program.command("stop").description("Stop the running BrowserTrack daemon").action(async () => {
2924
3335
  const pid = readPid();
2925
3336
  if (!pid || !isProcessRunning(pid)) {
3337
+ const config = getDaemonConfig();
3338
+ const isRunningHttp = await isDaemonRunning(config.host, config.port);
3339
+ if (isRunningHttp) {
3340
+ console.log("[BrowserTrack] Daemon is actively maintained by an MCP Server process in your IDE and will shut down when your editor session ends.");
3341
+ removePid();
3342
+ return;
3343
+ }
2926
3344
  console.log("[BrowserTrack] Daemon is not currently running.");
2927
3345
  removePid();
2928
3346
  return;
@@ -2937,13 +3355,21 @@ program.command("stop").description("Stop the running BrowserTrack daemon").acti
2937
3355
  });
2938
3356
  program.command("status").description("Check if the BrowserTrack daemon is running and view active sessions").action(async () => {
2939
3357
  const pid = readPid();
2940
- const isRunning = pid ? isProcessRunning(pid) : false;
2941
3358
  const config = getDaemonConfig();
3359
+ let isRunning = pid ? isProcessRunning(pid) : false;
3360
+ let detail = isRunning ? ` (PID: ${pid})` : "";
3361
+ if (!isRunning) {
3362
+ const isRunningHttp = await isDaemonRunning(config.host, config.port);
3363
+ if (isRunningHttp) {
3364
+ isRunning = true;
3365
+ detail = " (Active via MCP Server)";
3366
+ }
3367
+ }
2942
3368
  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");
2943
- console.log(` Status: ${isRunning ? "\u{1F7E2} RUNNING" : "\u26AA STOPPED"}${isRunning ? ` (PID: ${pid})` : ""}`);
3369
+ console.log(` Status: ${isRunning ? "\u{1F7E2} RUNNING" : "\u26AA STOPPED"}${detail}`);
2944
3370
  console.log(` Endpoint: http://${config.host}:${config.port}`);
2945
3371
  console.log(` Database: ${config.dbPath}`);
2946
- if (fs5.existsSync(config.dbPath)) {
3372
+ if (fs6.existsSync(config.dbPath)) {
2947
3373
  try {
2948
3374
  const db = new StorageDB(config.dbPath);
2949
3375
  const projects = db.listProjects();
@@ -2961,172 +3387,240 @@ program.command("status").description("Check if the BrowserTrack daemon is runni
2961
3387
  });
2962
3388
  var projectCommand = program.command("project").description("Manage tracked project mappings");
2963
3389
  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) => {
2964
- const config = getDaemonConfig();
2965
- const db = new StorageDB(config.dbPath);
2966
- const resolvedPath = options.path ? path6.resolve(options.path) : void 0;
2967
- const proj = db.upsertProject({
2968
- id: `proj_${name}`,
2969
- name,
2970
- origin: options.origin,
2971
- path: resolvedPath
2972
- });
2973
- console.log(`[BrowserTrack] Registered project '${proj.name}':`);
2974
- console.log(` ID: ${proj.id}`);
2975
- console.log(` Origin: ${proj.origin}`);
2976
- console.log(` Path: ${proj.path || "(none)"}`);
2977
- db.close();
3390
+ try {
3391
+ const config = getDaemonConfig();
3392
+ const db = new StorageDB(config.dbPath);
3393
+ try {
3394
+ const resolvedPath = options.path ? path7.resolve(options.path) : void 0;
3395
+ const proj = db.upsertProject({
3396
+ id: `proj_${name}`,
3397
+ name,
3398
+ origin: options.origin,
3399
+ path: resolvedPath
3400
+ });
3401
+ console.log(`[BrowserTrack] Registered project '${proj.name}':`);
3402
+ console.log(` ID: ${proj.id}`);
3403
+ console.log(` Origin: ${proj.origin}`);
3404
+ console.log(` Path: ${proj.path || "(none)"}`);
3405
+ } finally {
3406
+ db.close();
3407
+ }
3408
+ } catch (err) {
3409
+ console.error("[BrowserTrack] Failed to register project:", err?.message || err);
3410
+ }
2978
3411
  });
2979
3412
  program.command("projects").description("List all tracked projects").action(() => {
2980
- const config = getDaemonConfig();
2981
- const db = new StorageDB(config.dbPath);
2982
- const projects = db.listProjects();
2983
- if (projects.length === 0) {
2984
- console.log("[BrowserTrack] No projects registered yet. Projects will be auto-detected upon browser connection.");
2985
- } else {
2986
- console.log("\nTracked Projects:");
2987
- for (const p of projects) {
2988
- console.log(` \u2022 ${p.name.padEnd(16)} | ${p.origin.padEnd(26)} | ${p.path || "(auto-detected)"}`);
2989
- }
2990
- console.log("");
2991
- }
2992
- db.close();
3413
+ try {
3414
+ const config = getDaemonConfig();
3415
+ const db = new StorageDB(config.dbPath);
3416
+ try {
3417
+ const projects = db.listProjects();
3418
+ if (projects.length === 0) {
3419
+ console.log("[BrowserTrack] No projects registered yet. Projects will be auto-detected upon browser connection.");
3420
+ } else {
3421
+ console.log("\nTracked Projects:");
3422
+ for (const p of projects) {
3423
+ console.log(` \u2022 ${p.name.padEnd(16)} | ${p.origin.padEnd(26)} | ${p.path || "(auto-detected)"}`);
3424
+ }
3425
+ console.log("");
3426
+ }
3427
+ } finally {
3428
+ db.close();
3429
+ }
3430
+ } catch (err) {
3431
+ console.error("[BrowserTrack] Failed to list projects:", err?.message || err);
3432
+ }
2993
3433
  });
2994
3434
  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) => {
2995
- const config = getDaemonConfig();
2996
- const db = new StorageDB(config.dbPath);
2997
- const limit = parseInt(options.limit, 10);
2998
- const incidents = db.listIncidents({
2999
- projectId: options.project,
3000
- status: options.status,
3001
- limit
3002
- });
3003
- if (incidents.length === 0) {
3004
- console.log("[BrowserTrack] No incidents found matching the filter.");
3005
- } else {
3006
- console.log(`
3435
+ try {
3436
+ const config = getDaemonConfig();
3437
+ const db = new StorageDB(config.dbPath);
3438
+ try {
3439
+ const limit = parseInt(options.limit, 10);
3440
+ const incidents = db.listIncidents({
3441
+ projectId: options.project,
3442
+ status: options.status,
3443
+ limit
3444
+ });
3445
+ if (incidents.length === 0) {
3446
+ console.log("[BrowserTrack] No incidents found matching the filter.");
3447
+ } else {
3448
+ console.log(`
3007
3449
  Incidents (${incidents.length}):`);
3008
- for (const inc of incidents) {
3009
- const statusBadge = inc.status === "OPEN" ? "\u{1F534} OPEN" : inc.status === "VERIFIED" ? "\u{1F7E2} VERIFIED" : inc.status === "FAILED" ? "\u274C FAILED" : `\u26AA ${inc.status}`;
3010
- console.log(` ${inc.id.padEnd(12)} [${statusBadge}] (${inc.occurrences}x) ${inc.type}: ${inc.message}`);
3011
- console.log(` Source: ${inc.source.file}:${inc.source.line} | Route: ${inc.route}`);
3450
+ for (const inc of incidents) {
3451
+ const statusBadge = inc.status === "OPEN" ? "\u{1F534} OPEN" : inc.status === "VERIFIED" ? "\u{1F7E2} VERIFIED" : inc.status === "FAILED" ? "\u274C FAILED" : `\u26AA ${inc.status}`;
3452
+ console.log(` ${inc.id.padEnd(12)} [${statusBadge}] (${inc.occurrences}x) ${inc.type}: ${inc.message}`);
3453
+ console.log(` Source: ${inc.source.file}:${inc.source.line} | Route: ${inc.route}`);
3454
+ }
3455
+ console.log("");
3456
+ }
3457
+ } finally {
3458
+ db.close();
3012
3459
  }
3013
- console.log("");
3460
+ } catch (err) {
3461
+ console.error("[BrowserTrack] Failed to list incidents:", err?.message || err);
3014
3462
  }
3015
- db.close();
3016
3463
  });
3017
3464
  var noteCmd = program.command("note").description("Inspect or manage visual development notes");
3018
3465
  noteCmd.command("show <noteId>").description("Show full context for a specific visual note").action((noteId) => {
3019
- const config = getDaemonConfig();
3020
- const db = new StorageDB(config.dbPath);
3021
- const note = db.getNote(noteId);
3022
- if (!note) {
3023
- console.log(`[BrowserTrack] Note '${noteId}' not found.`);
3024
- } else {
3025
- 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");
3026
- console.log(` \u{1F4DD} Visual Note: ${note.id} [${note.status}]`);
3027
- console.log(` \u{1F4CD} Route: ${note.route} (${note.url})`);
3028
- console.log(` \u{1F4D0} Viewport: ${note.viewport.width} \xD7 ${note.viewport.height} (dpr: ${note.viewport.devicePixelRatio})`);
3029
- if (note.target) {
3030
- console.log(` \u{1F3AF} Target: ${note.target.selector}`);
3031
- console.log(` Bounds: x:${note.target.boundingRect.x}, y:${note.target.boundingRect.y}, ${note.target.boundingRect.width}\xD7${note.target.boundingRect.height}`);
3032
- }
3033
- console.log(` \u{1F4AC} Note: "${note.message}"`);
3034
- if (note.screenshots?.original) {
3035
- console.log(` \u{1F5BC}\uFE0F Screenshot: ${note.screenshots.original}`);
3036
- }
3037
- console.log(` \u{1F552} Created: ${note.createdAt}`);
3038
- 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");
3466
+ try {
3467
+ const config = getDaemonConfig();
3468
+ const db = new StorageDB(config.dbPath);
3469
+ try {
3470
+ const note = db.getNote(noteId);
3471
+ if (!note) {
3472
+ console.log(`[BrowserTrack] Note '${noteId}' not found.`);
3473
+ } else {
3474
+ 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");
3475
+ console.log(` \u{1F4DD} Visual Note: ${note.id} [${note.status}]`);
3476
+ console.log(` \u{1F4CD} Route: ${note.route} (${note.url})`);
3477
+ console.log(` \u{1F4D0} Viewport: ${note.viewport.width} \xD7 ${note.viewport.height} (dpr: ${note.viewport.devicePixelRatio})`);
3478
+ if (note.target) {
3479
+ console.log(` \u{1F3AF} Target: ${note.target.selector}`);
3480
+ console.log(` Bounds: x:${note.target.boundingRect.x}, y:${note.target.boundingRect.y}, ${note.target.boundingRect.width}\xD7${note.target.boundingRect.height}`);
3481
+ }
3482
+ console.log(` \u{1F4AC} Note: "${note.message}"`);
3483
+ if (note.screenshots?.original) {
3484
+ console.log(` \u{1F5BC}\uFE0F Screenshot: ${note.screenshots.original}`);
3485
+ }
3486
+ console.log(` \u{1F552} Created: ${note.createdAt}`);
3487
+ 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");
3488
+ }
3489
+ } finally {
3490
+ db.close();
3491
+ }
3492
+ } catch (err) {
3493
+ console.error("[BrowserTrack] Failed to retrieve note:", err?.message || err);
3039
3494
  }
3040
- db.close();
3041
3495
  });
3042
3496
  noteCmd.command("resolve <noteId>").description("Mark a visual note as resolved").action((noteId) => {
3043
- const config = getDaemonConfig();
3044
- const db = new StorageDB(config.dbPath);
3045
- const note = db.getNote(noteId);
3046
- if (!note) {
3047
- console.log(`[BrowserTrack] Note '${noteId}' not found.`);
3048
- } else {
3049
- db.updateNoteStatus(noteId, "RESOLVED");
3050
- console.log(`[BrowserTrack] Marked note '${noteId}' as RESOLVED.`);
3051
- }
3052
- db.close();
3497
+ try {
3498
+ const config = getDaemonConfig();
3499
+ const db = new StorageDB(config.dbPath);
3500
+ try {
3501
+ const note = db.getNote(noteId);
3502
+ if (!note) {
3503
+ console.log(`[BrowserTrack] Note '${noteId}' not found.`);
3504
+ } else {
3505
+ db.updateNoteStatus(noteId, "RESOLVED");
3506
+ console.log(`[BrowserTrack] Marked note '${noteId}' as RESOLVED.`);
3507
+ }
3508
+ } finally {
3509
+ db.close();
3510
+ }
3511
+ } catch (err) {
3512
+ console.error("[BrowserTrack] Failed to resolve note:", err?.message || err);
3513
+ }
3053
3514
  });
3054
3515
  noteCmd.command("reopen <noteId>").description("Reopen a visual note").action((noteId) => {
3055
- const config = getDaemonConfig();
3056
- const db = new StorageDB(config.dbPath);
3057
- const note = db.getNote(noteId);
3058
- if (!note) {
3059
- console.log(`[BrowserTrack] Note '${noteId}' not found.`);
3060
- } else {
3061
- db.updateNoteStatus(noteId, "OPEN");
3062
- console.log(`[BrowserTrack] Reopened note '${noteId}' (status: OPEN).`);
3063
- }
3064
- db.close();
3516
+ try {
3517
+ const config = getDaemonConfig();
3518
+ const db = new StorageDB(config.dbPath);
3519
+ try {
3520
+ const note = db.getNote(noteId);
3521
+ if (!note) {
3522
+ console.log(`[BrowserTrack] Note '${noteId}' not found.`);
3523
+ } else {
3524
+ db.updateNoteStatus(noteId, "OPEN");
3525
+ console.log(`[BrowserTrack] Reopened note '${noteId}' (status: OPEN).`);
3526
+ }
3527
+ } finally {
3528
+ db.close();
3529
+ }
3530
+ } catch (err) {
3531
+ console.error("[BrowserTrack] Failed to reopen note:", err?.message || err);
3532
+ }
3065
3533
  });
3066
3534
  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) => {
3067
- const config = getDaemonConfig();
3068
- const db = new StorageDB(config.dbPath);
3069
- const limit = parseInt(options.limit, 10);
3070
- const notes = db.listNotes({
3071
- projectId: options.project,
3072
- status: options.status,
3073
- limit
3074
- });
3075
- if (notes.length === 0) {
3076
- console.log("[BrowserTrack] No visual notes found.");
3077
- } else {
3078
- console.log(`
3535
+ try {
3536
+ const config = getDaemonConfig();
3537
+ const db = new StorageDB(config.dbPath);
3538
+ try {
3539
+ const limit = parseInt(options.limit, 10);
3540
+ const notes = db.listNotes({
3541
+ projectId: options.project,
3542
+ status: options.status,
3543
+ limit
3544
+ });
3545
+ if (notes.length === 0) {
3546
+ console.log("[BrowserTrack] No visual notes found.");
3547
+ } else {
3548
+ console.log(`
3079
3549
  Visual Notes (${notes.length}):`);
3080
- for (const n of notes) {
3081
- const badge = n.status === "OPEN" ? "\u{1F7E1} OPEN" : n.status === "RESOLVED" ? "\u{1F7E2} RESOLVED" : `\u26AA ${n.status}`;
3082
- console.log(` ${n.id.padEnd(12)} [${badge}] Route: ${n.route.padEnd(16)} | Target: ${n.target?.selector || n.type}`);
3083
- console.log(` Note: "${n.message}" (${n.viewport.width}\xD7${n.viewport.height})`);
3550
+ for (const n of notes) {
3551
+ const badge = n.status === "OPEN" ? "\u{1F7E1} OPEN" : n.status === "RESOLVED" ? "\u{1F7E2} RESOLVED" : `\u26AA ${n.status}`;
3552
+ console.log(` ${n.id.padEnd(12)} [${badge}] Route: ${n.route.padEnd(16)} | Target: ${n.target?.selector || n.type}`);
3553
+ console.log(` Note: "${n.message}" (${n.viewport.width}\xD7${n.viewport.height})`);
3554
+ }
3555
+ console.log("");
3556
+ }
3557
+ } finally {
3558
+ db.close();
3084
3559
  }
3085
- console.log("");
3560
+ } catch (err) {
3561
+ console.error("[BrowserTrack] Failed to list visual notes:", err?.message || err);
3086
3562
  }
3087
- db.close();
3088
3563
  });
3089
3564
  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) => {
3090
- const config = getDaemonConfig();
3091
- const db = new StorageDB(config.dbPath);
3092
- const incidents = db.listIncidents({ projectId: options.project, status: "OPEN", limit: 20 });
3093
- const notes = db.listNotes({ projectId: options.project, status: "OPEN", limit: 20 });
3094
- 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");
3095
- console.log(` \u{1F4E5} Browser Development Inbox ${options.project ? `(${options.project})` : ""}`);
3096
- 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");
3097
- if (incidents.length === 0 && notes.length === 0) {
3098
- console.log(" \u2728 All clear! No open errors or visual notes.");
3099
- } else {
3100
- if (incidents.length > 0) {
3101
- console.log(`
3565
+ try {
3566
+ const config = getDaemonConfig();
3567
+ const db = new StorageDB(config.dbPath);
3568
+ try {
3569
+ const incidents = db.listIncidents({ projectId: options.project, status: "OPEN", limit: 20 });
3570
+ const notes = db.listNotes({ projectId: options.project, status: "OPEN", limit: 20 });
3571
+ 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");
3572
+ console.log(` \u{1F4E5} Browser Development Inbox ${options.project ? `(${options.project})` : ""}`);
3573
+ 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");
3574
+ if (incidents.length === 0 && notes.length === 0) {
3575
+ console.log(" \u2728 All clear! No open errors or visual notes.");
3576
+ } else {
3577
+ if (incidents.length > 0) {
3578
+ console.log(`
3102
3579
  \u{1F6A8} Runtime Errors (${incidents.length}):`);
3103
- for (const inc of incidents) {
3104
- console.log(` \u2022 ${inc.id} (${inc.occurrences}x) ${inc.type}: ${inc.message}`);
3105
- console.log(` Route: ${inc.route} | Source: ${inc.source.file}:${inc.source.line}`);
3106
- }
3107
- }
3108
- if (notes.length > 0) {
3109
- console.log(`
3580
+ for (const inc of incidents) {
3581
+ console.log(` \u2022 ${inc.id} (${inc.occurrences}x) ${inc.type}: ${inc.message}`);
3582
+ console.log(` Route: ${inc.route} | Source: ${inc.source.file}:${inc.source.line}`);
3583
+ }
3584
+ }
3585
+ if (notes.length > 0) {
3586
+ console.log(`
3110
3587
  \u{1F4DD} Visual Notes (${notes.length}):`);
3111
- for (const n of notes) {
3112
- console.log(` \u2022 ${n.id} on ${n.route} (${n.viewport.width}\xD7${n.viewport.height})`);
3113
- console.log(` Target: ${n.target?.selector || n.type} | Note: "${n.message}"`);
3588
+ for (const n of notes) {
3589
+ console.log(` \u2022 ${n.id} on ${n.route} (${n.viewport.width}\xD7${n.viewport.height})`);
3590
+ console.log(` Target: ${n.target?.selector || n.type} | Note: "${n.message}"`);
3591
+ }
3592
+ }
3114
3593
  }
3594
+ 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");
3595
+ } finally {
3596
+ db.close();
3115
3597
  }
3598
+ } catch (err) {
3599
+ console.error("[BrowserTrack] Failed to display inbox:", err?.message || err);
3116
3600
  }
3117
- 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");
3118
- db.close();
3119
3601
  });
3120
3602
  program.command("clear").description("Clear all stored incidents, events, and sessions from the database").action(() => {
3121
- const config = getDaemonConfig();
3122
- const db = new StorageDB(config.dbPath);
3123
- db.clearAll();
3124
- console.log("[BrowserTrack] Database cleared.");
3125
- db.close();
3603
+ try {
3604
+ const config = getDaemonConfig();
3605
+ const db = new StorageDB(config.dbPath);
3606
+ try {
3607
+ db.clearAll();
3608
+ console.log("[BrowserTrack] Database cleared.");
3609
+ } finally {
3610
+ db.close();
3611
+ }
3612
+ } catch (err) {
3613
+ console.error("[BrowserTrack] Failed to clear database:", err?.message || err);
3614
+ }
3126
3615
  });
3127
- program.command("mcp").description("Launch the Model Context Protocol (MCP) server over stdio").action(async () => {
3616
+ program.command("mcp").description("Launch the Model Context Protocol (MCP) server over stdio").option("--no-daemon", "Do not auto-start background daemon if offline").action(async (options) => {
3128
3617
  try {
3129
- const server = createMcpServer();
3618
+ console.log = (...args) => {
3619
+ console.error(...args);
3620
+ };
3621
+ const server = createMcpServer({
3622
+ autoStartDaemon: options.daemon !== false
3623
+ });
3130
3624
  await server.startStdio();
3131
3625
  } catch (err) {
3132
3626
  console.error("[BrowserTrack] MCP Server error:", err?.message);