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
@@ -1,3 +1,12 @@
1
+ import {
2
+ computeFingerprint,
3
+ extractSourceFromStack,
4
+ normalizeSourceFile,
5
+ redactSensitiveData,
6
+ safeJsonParse,
7
+ safeJsonStringify
8
+ } from "./chunk-QRZ57ME3.js";
9
+
1
10
  // packages/daemon/src/config.ts
2
11
  import os from "os";
3
12
  import path from "path";
@@ -29,6 +38,7 @@ var StorageDB = class {
29
38
  this.db = new Database(dbPath);
30
39
  this.db.pragma("journal_mode = WAL");
31
40
  this.db.pragma("synchronous = NORMAL");
41
+ this.db.pragma("busy_timeout = 5000");
32
42
  this.initTables();
33
43
  }
34
44
  initTables() {
@@ -160,18 +170,36 @@ var StorageDB = class {
160
170
  CREATE INDEX IF NOT EXISTS idx_incidents_project ON incidents(project_id, status);
161
171
  CREATE INDEX IF NOT EXISTS idx_incidents_fp ON incidents(fingerprint);
162
172
  CREATE INDEX IF NOT EXISTS idx_notes_project ON notes(project_id, status);
163
- CREATE INDEX IF NOT EXISTS idx_notes_scenario ON notes(scenario_id, step_number);
164
173
  `);
165
174
  try {
166
- this.db.exec("ALTER TABLE notes ADD COLUMN scenario_id TEXT;");
167
- } catch {
168
- }
169
- try {
170
- this.db.exec("ALTER TABLE notes ADD COLUMN step_number INTEGER;");
175
+ const columns = this.db.prepare("PRAGMA table_info(notes)").all().map(
176
+ (c) => c.name
177
+ );
178
+ if (!columns.includes("scenario_id")) {
179
+ this.db.exec("ALTER TABLE notes ADD COLUMN scenario_id TEXT;");
180
+ }
181
+ if (!columns.includes("step_number")) {
182
+ this.db.exec("ALTER TABLE notes ADD COLUMN step_number INTEGER;");
183
+ }
184
+ if (!columns.includes("scenario_title")) {
185
+ this.db.exec("ALTER TABLE notes ADD COLUMN scenario_title TEXT;");
186
+ }
171
187
  } catch {
188
+ try {
189
+ this.db.exec("ALTER TABLE notes ADD COLUMN scenario_id TEXT;");
190
+ } catch {
191
+ }
192
+ try {
193
+ this.db.exec("ALTER TABLE notes ADD COLUMN step_number INTEGER;");
194
+ } catch {
195
+ }
196
+ try {
197
+ this.db.exec("ALTER TABLE notes ADD COLUMN scenario_title TEXT;");
198
+ } catch {
199
+ }
172
200
  }
173
201
  try {
174
- this.db.exec("ALTER TABLE notes ADD COLUMN scenario_title TEXT;");
202
+ this.db.exec("CREATE INDEX IF NOT EXISTS idx_notes_scenario ON notes(scenario_id, step_number);");
175
203
  } catch {
176
204
  }
177
205
  }
@@ -336,7 +364,7 @@ var StorageDB = class {
336
364
  id: r.id,
337
365
  sessionId: r.session_id,
338
366
  eventType: r.event_type,
339
- payload: JSON.parse(r.payload),
367
+ payload: safeJsonParse(r.payload, {}),
340
368
  timestamp: r.timestamp,
341
369
  route: r.route,
342
370
  url: r.url
@@ -397,9 +425,9 @@ var StorageDB = class {
397
425
  incident.occurrences,
398
426
  incident.status,
399
427
  incident.stack || null,
400
- JSON.stringify(incident.breadcrumbs || []),
401
- JSON.stringify(incident.networkFailures || []),
402
- incident.lastElement ? JSON.stringify(incident.lastElement) : null,
428
+ safeJsonStringify(incident.breadcrumbs || []),
429
+ safeJsonStringify(incident.networkFailures || []),
430
+ incident.lastElement ? safeJsonStringify(incident.lastElement) : null,
403
431
  incident.screenshots?.error || null
404
432
  );
405
433
  }
@@ -413,8 +441,8 @@ var StorageDB = class {
413
441
  update.lastSeen,
414
442
  update.occurrences,
415
443
  update.route,
416
- JSON.stringify(update.breadcrumbs || []),
417
- update.lastElement ? JSON.stringify(update.lastElement) : null,
444
+ safeJsonStringify(update.breadcrumbs || []),
445
+ update.lastElement ? safeJsonStringify(update.lastElement) : null,
418
446
  update.stack || null,
419
447
  incidentId
420
448
  );
@@ -434,8 +462,8 @@ var StorageDB = class {
434
462
  occurrence.route,
435
463
  occurrence.url,
436
464
  occurrence.stack || null,
437
- JSON.stringify(occurrence.breadcrumbs || []),
438
- occurrence.lastElement ? JSON.stringify(occurrence.lastElement) : null
465
+ safeJsonStringify(occurrence.breadcrumbs || []),
466
+ occurrence.lastElement ? safeJsonStringify(occurrence.lastElement) : null
439
467
  );
440
468
  }
441
469
  // --- VERIFICATIONS ---
@@ -447,7 +475,7 @@ var StorageDB = class {
447
475
  v.id,
448
476
  v.incidentId,
449
477
  v.status,
450
- JSON.stringify(v.checks),
478
+ safeJsonStringify(v.checks),
451
479
  v.beforeScreenshot || null,
452
480
  v.afterScreenshot || null,
453
481
  v.message || null,
@@ -460,7 +488,7 @@ var StorageDB = class {
460
488
  return {
461
489
  incidentId: row.incident_id,
462
490
  status: row.status,
463
- checks: JSON.parse(row.checks || "[]"),
491
+ checks: safeJsonParse(row.checks, []),
464
492
  screenshots: {
465
493
  before: row.before_screenshot || void 0,
466
494
  after: row.after_screenshot || void 0
@@ -485,11 +513,11 @@ var StorageDB = class {
485
513
  note.message,
486
514
  note.route,
487
515
  note.url,
488
- JSON.stringify(note.viewport),
489
- JSON.stringify(note.scroll),
490
- note.target ? JSON.stringify(note.target) : null,
491
- note.elementContext ? JSON.stringify(note.elementContext) : null,
492
- note.region ? JSON.stringify(note.region) : null,
516
+ safeJsonStringify(note.viewport),
517
+ safeJsonStringify(note.scroll),
518
+ note.target ? safeJsonStringify(note.target) : null,
519
+ note.elementContext ? safeJsonStringify(note.elementContext) : null,
520
+ note.region ? safeJsonStringify(note.region) : null,
493
521
  note.screenshots?.original || null,
494
522
  note.incidentId || null,
495
523
  note.scenarioId || null,
@@ -630,8 +658,8 @@ var StorageDB = class {
630
658
  `nver_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
631
659
  v.noteId,
632
660
  v.status,
633
- JSON.stringify(v.checks),
634
- v.geometryDiff ? JSON.stringify(v.geometryDiff) : null,
661
+ safeJsonStringify(v.checks),
662
+ v.geometryDiff ? safeJsonStringify(v.geometryDiff) : null,
635
663
  v.screenshots?.before || null,
636
664
  v.screenshots?.after || null,
637
665
  v.message || null,
@@ -644,8 +672,8 @@ var StorageDB = class {
644
672
  return {
645
673
  noteId: row.note_id,
646
674
  status: row.status,
647
- checks: JSON.parse(row.checks || "[]"),
648
- geometryDiff: row.geometry_diff ? JSON.parse(row.geometry_diff) : void 0,
675
+ checks: safeJsonParse(row.checks, []),
676
+ geometryDiff: safeJsonParse(row.geometry_diff, void 0),
649
677
  screenshots: {
650
678
  before: row.before_screenshot || void 0,
651
679
  after: row.after_screenshot || void 0
@@ -686,9 +714,9 @@ var StorageDB = class {
686
714
  occurrences: row.occurrences,
687
715
  status: row.status,
688
716
  stack: row.stack || void 0,
689
- breadcrumbs: JSON.parse(row.breadcrumbs || "[]"),
690
- networkFailures: JSON.parse(row.network_failures || "[]"),
691
- lastElement: row.last_element ? JSON.parse(row.last_element) : void 0,
717
+ breadcrumbs: safeJsonParse(row.breadcrumbs, []),
718
+ networkFailures: safeJsonParse(row.network_failures, []),
719
+ lastElement: safeJsonParse(row.last_element, void 0),
692
720
  screenshots: row.screenshot_path ? {
693
721
  error: row.screenshot_path
694
722
  } : void 0
@@ -703,11 +731,11 @@ var StorageDB = class {
703
731
  message: row.message,
704
732
  route: row.route,
705
733
  url: row.url,
706
- viewport: JSON.parse(row.viewport_json || "{}"),
707
- scroll: JSON.parse(row.scroll_json || "{}"),
708
- target: row.target_json ? JSON.parse(row.target_json) : void 0,
709
- elementContext: row.element_context_json ? JSON.parse(row.element_context_json) : void 0,
710
- region: row.region_json ? JSON.parse(row.region_json) : void 0,
734
+ viewport: safeJsonParse(row.viewport_json, { width: 0, height: 0, devicePixelRatio: 1 }),
735
+ scroll: safeJsonParse(row.scroll_json, { scrollX: 0, scrollY: 0 }),
736
+ target: safeJsonParse(row.target_json, void 0),
737
+ elementContext: safeJsonParse(row.element_context_json, void 0),
738
+ region: safeJsonParse(row.region_json, void 0),
711
739
  status: row.status,
712
740
  incidentId: row.incident_id || void 0,
713
741
  scenarioId: row.scenario_id || void 0,
@@ -884,8 +912,167 @@ var SessionManager = class {
884
912
  }
885
913
  };
886
914
 
887
- // packages/daemon/src/notes/engine.ts
915
+ // packages/daemon/src/incidents/engine.ts
888
916
  import crypto from "crypto";
917
+ var IncidentEngine = class {
918
+ db;
919
+ screenshotStore;
920
+ constructor(db, screenshotStore) {
921
+ this.db = db;
922
+ this.screenshotStore = screenshotStore;
923
+ }
924
+ processClientEvent(message) {
925
+ const session = this.db.getSession(message.sessionId);
926
+ const projectId = session?.projectId || "default";
927
+ const sanitizedBreadcrumbs = (message.breadcrumbs || []).map((b) => redactSensitiveData(b));
928
+ const sanitizedLastElement = message.lastElement ? redactSensitiveData(message.lastElement) : void 0;
929
+ const networkFailures = sanitizedBreadcrumbs.filter((b) => (b.type === "fetch" || b.type === "xhr") && b.level === "error" && b.data).map((b) => ({
930
+ url: b.message.split(" ")[1] || "",
931
+ method: b.message.split(" ")[0] || "GET",
932
+ status: b.data?.status,
933
+ durationMs: b.data?.durationMs || 0,
934
+ error: b.data?.error,
935
+ aborted: b.data?.aborted,
936
+ timestamp: b.timestamp
937
+ }));
938
+ if (message.eventType === "runtime_error" || message.eventType === "unhandled_rejection") {
939
+ const payload = message.payload;
940
+ return this.handleErrorEvent({
941
+ projectId,
942
+ sessionId: message.sessionId,
943
+ type: payload.errorType || "runtime_exception",
944
+ severity: "error",
945
+ message: payload.message || "Unknown runtime error",
946
+ stack: payload.stack,
947
+ sourceFile: payload.filename,
948
+ line: payload.lineno,
949
+ column: payload.colno,
950
+ route: message.route || "/",
951
+ url: message.url,
952
+ breadcrumbs: sanitizedBreadcrumbs,
953
+ networkFailures,
954
+ lastElement: sanitizedLastElement,
955
+ screenshotDataUrl: message.screenshot
956
+ });
957
+ }
958
+ if (message.eventType === "console") {
959
+ const payload = message.payload;
960
+ if (payload.level === "error") {
961
+ return this.handleErrorEvent({
962
+ projectId,
963
+ sessionId: message.sessionId,
964
+ type: "console_error",
965
+ severity: "error",
966
+ message: payload.message,
967
+ stack: payload.stack,
968
+ route: message.route || "/",
969
+ url: message.url,
970
+ breadcrumbs: sanitizedBreadcrumbs,
971
+ networkFailures,
972
+ lastElement: sanitizedLastElement,
973
+ screenshotDataUrl: message.screenshot
974
+ });
975
+ }
976
+ }
977
+ return null;
978
+ }
979
+ handleErrorEvent(input) {
980
+ const extracted = (!input.sourceFile || !input.line) && input.stack ? extractSourceFromStack(input.stack) : null;
981
+ const rawSource = input.sourceFile || extracted?.file || "unknown_source";
982
+ const sourceFile = normalizeSourceFile(rawSource);
983
+ const line = input.line || extracted?.line || 0;
984
+ const column = input.column || extracted?.column;
985
+ const fingerprint = computeFingerprint({
986
+ type: input.type,
987
+ message: input.message,
988
+ sourceFile,
989
+ line,
990
+ column,
991
+ stack: input.stack
992
+ });
993
+ const now = (/* @__PURE__ */ new Date()).toISOString();
994
+ const existing = this.db.findIncidentByFingerprint(fingerprint);
995
+ if (existing) {
996
+ const updatedOccurrences = existing.occurrences + 1;
997
+ this.db.updateIncidentOccurrence(existing.id, {
998
+ sessionId: input.sessionId,
999
+ lastSeen: now,
1000
+ occurrences: updatedOccurrences,
1001
+ route: input.route,
1002
+ breadcrumbs: input.breadcrumbs,
1003
+ lastElement: input.lastElement,
1004
+ stack: input.stack
1005
+ });
1006
+ const occurrenceId = `occ_${crypto.randomUUID().slice(0, 8)}`;
1007
+ this.db.insertIncidentOccurrence({
1008
+ id: occurrenceId,
1009
+ incidentId: existing.id,
1010
+ sessionId: input.sessionId,
1011
+ timestamp: now,
1012
+ route: input.route,
1013
+ url: input.url,
1014
+ stack: input.stack,
1015
+ breadcrumbs: input.breadcrumbs,
1016
+ lastElement: input.lastElement
1017
+ });
1018
+ return {
1019
+ ...existing,
1020
+ occurrences: updatedOccurrences,
1021
+ lastSeen: now,
1022
+ breadcrumbs: input.breadcrumbs,
1023
+ lastElement: input.lastElement || existing.lastElement
1024
+ };
1025
+ }
1026
+ const incidentId = `inc_${crypto.randomUUID().slice(0, 8)}`;
1027
+ let screenshotPath;
1028
+ if (input.screenshotDataUrl) {
1029
+ const saved = this.screenshotStore.saveScreenshot(input.projectId, incidentId, "error", input.screenshotDataUrl);
1030
+ if (saved) {
1031
+ screenshotPath = saved.filePath;
1032
+ }
1033
+ }
1034
+ const newIncident = {
1035
+ id: incidentId,
1036
+ projectId: input.projectId,
1037
+ sessionId: input.sessionId,
1038
+ type: input.type,
1039
+ severity: input.severity,
1040
+ message: input.message,
1041
+ source: {
1042
+ file: sourceFile,
1043
+ line,
1044
+ column
1045
+ },
1046
+ fingerprint,
1047
+ route: input.route,
1048
+ firstSeen: now,
1049
+ lastSeen: now,
1050
+ occurrences: 1,
1051
+ status: "OPEN",
1052
+ stack: input.stack,
1053
+ breadcrumbs: input.breadcrumbs,
1054
+ networkFailures: input.networkFailures,
1055
+ lastElement: input.lastElement,
1056
+ screenshots: screenshotPath ? { error: screenshotPath } : void 0
1057
+ };
1058
+ this.db.insertIncident(newIncident);
1059
+ this.db.insertIncidentOccurrence({
1060
+ id: `occ_${crypto.randomUUID().slice(0, 8)}`,
1061
+ incidentId,
1062
+ sessionId: input.sessionId,
1063
+ timestamp: now,
1064
+ route: input.route,
1065
+ url: input.url,
1066
+ stack: input.stack,
1067
+ breadcrumbs: input.breadcrumbs,
1068
+ lastElement: input.lastElement
1069
+ });
1070
+ return newIncident;
1071
+ }
1072
+ };
1073
+
1074
+ // packages/daemon/src/notes/engine.ts
1075
+ import crypto2 from "crypto";
889
1076
  import path4 from "path";
890
1077
  import fs3 from "fs";
891
1078
  var NotesEngine = class {
@@ -898,7 +1085,7 @@ var NotesEngine = class {
898
1085
  createNoteFromClient(payload) {
899
1086
  const session = this.db.getSession(payload.sessionId);
900
1087
  const projectId = session?.projectId || "default";
901
- const noteId = `note_${crypto.randomUUID().slice(0, 8)}`;
1088
+ const noteId = `note_${crypto2.randomUUID().slice(0, 8)}`;
902
1089
  const now = (/* @__PURE__ */ new Date()).toISOString();
903
1090
  let screenshotPath;
904
1091
  if (payload.screenshot) {
@@ -954,7 +1141,7 @@ var NotesEngine = class {
954
1141
  };
955
1142
 
956
1143
  // packages/daemon/src/notes/verification.ts
957
- import crypto2 from "crypto";
1144
+ import crypto3 from "crypto";
958
1145
  var NoteVerificationEngine = class {
959
1146
  db;
960
1147
  sessionManager;
@@ -992,14 +1179,14 @@ var NoteVerificationEngine = class {
992
1179
  }
993
1180
  this.db.updateNoteStatus(noteId, "VERIFYING");
994
1181
  const pageStateCmd = await this.sessionManager.sendCommand(session.id, {
995
- id: `cmd_${crypto2.randomUUID().slice(0, 8)}`,
1182
+ id: `cmd_${crypto3.randomUUID().slice(0, 8)}`,
996
1183
  type: "get_page_state"
997
1184
  });
998
1185
  const currentRoute = pageStateCmd.result?.route || "/";
999
1186
  const isRouteMatch = currentRoute === note.route || currentRoute.startsWith(note.route);
1000
1187
  if (!isRouteMatch && note.route) {
1001
1188
  await this.sessionManager.sendCommand(session.id, {
1002
- id: `cmd_${crypto2.randomUUID().slice(0, 8)}`,
1189
+ id: `cmd_${crypto3.randomUUID().slice(0, 8)}`,
1003
1190
  type: "navigate",
1004
1191
  params: { url: note.route }
1005
1192
  });
@@ -1018,7 +1205,7 @@ var NoteVerificationEngine = class {
1018
1205
  let overflowResult;
1019
1206
  if (note.type === "element" && targetSelector) {
1020
1207
  const queryCmd = await this.sessionManager.sendCommand(session.id, {
1021
- id: `cmd_${crypto2.randomUUID().slice(0, 8)}`,
1208
+ id: `cmd_${crypto3.randomUUID().slice(0, 8)}`,
1022
1209
  type: "query_element",
1023
1210
  params: { selector: targetSelector }
1024
1211
  });
@@ -1036,7 +1223,7 @@ var NoteVerificationEngine = class {
1036
1223
  details: elementVisible ? `Element ${targetSelector} is visible` : `Element ${targetSelector} is hidden`
1037
1224
  });
1038
1225
  const overflowCmd = await this.sessionManager.sendCommand(session.id, {
1039
- id: `cmd_${crypto2.randomUUID().slice(0, 8)}`,
1226
+ id: `cmd_${crypto3.randomUUID().slice(0, 8)}`,
1040
1227
  type: "check_overflow",
1041
1228
  params: { selector: targetSelector }
1042
1229
  });
@@ -1053,7 +1240,7 @@ var NoteVerificationEngine = class {
1053
1240
  }
1054
1241
  let afterScreenshotPath;
1055
1242
  const captureCmd = await this.sessionManager.sendCommand(session.id, {
1056
- id: `cmd_${crypto2.randomUUID().slice(0, 8)}`,
1243
+ id: `cmd_${crypto3.randomUUID().slice(0, 8)}`,
1057
1244
  type: "capture_element",
1058
1245
  params: { selector: targetSelector }
1059
1246
  });
@@ -1101,7 +1288,7 @@ var NoteVerificationEngine = class {
1101
1288
  };
1102
1289
 
1103
1290
  // packages/daemon/src/verification/engine.ts
1104
- import crypto3 from "crypto";
1291
+ import crypto4 from "crypto";
1105
1292
  var VerificationEngine = class {
1106
1293
  db;
1107
1294
  sessionManager;
@@ -1143,13 +1330,13 @@ var VerificationEngine = class {
1143
1330
  const observationMs = recipe?.observationWindowMs || 2e3;
1144
1331
  if (recipe?.route && recipe.route !== incident.route) {
1145
1332
  await this.sessionManager.sendCommand(session.id, {
1146
- id: `cmd_${crypto3.randomUUID().slice(0, 8)}`,
1333
+ id: `cmd_${crypto4.randomUUID().slice(0, 8)}`,
1147
1334
  type: "navigate",
1148
1335
  params: { url: recipe.route }
1149
1336
  });
1150
1337
  } else {
1151
1338
  await this.sessionManager.sendCommand(session.id, {
1152
- id: `cmd_${crypto3.randomUUID().slice(0, 8)}`,
1339
+ id: `cmd_${crypto4.randomUUID().slice(0, 8)}`,
1153
1340
  type: "reload",
1154
1341
  params: { force: true }
1155
1342
  });
@@ -1172,7 +1359,7 @@ var VerificationEngine = class {
1172
1359
  const targetSelector = recipe?.targetSelector || incident.lastElement?.selector;
1173
1360
  if (targetSelector) {
1174
1361
  const captureCmd = await this.sessionManager.sendCommand(session.id, {
1175
- id: `cmd_${crypto3.randomUUID().slice(0, 8)}`,
1362
+ id: `cmd_${crypto4.randomUUID().slice(0, 8)}`,
1176
1363
  type: "capture_element",
1177
1364
  params: { selector: targetSelector }
1178
1365
  });
@@ -1220,7 +1407,7 @@ var VerificationEngine = class {
1220
1407
  return { type: probe.type, passed: false, details: "Missing selector in probe" };
1221
1408
  }
1222
1409
  const res = await this.sessionManager.sendCommand(sessionId, {
1223
- id: `cmd_${crypto3.randomUUID().slice(0, 8)}`,
1410
+ id: `cmd_${crypto4.randomUUID().slice(0, 8)}`,
1224
1411
  type: "query_element",
1225
1412
  params: { selector: probe.selector }
1226
1413
  });
@@ -1236,7 +1423,7 @@ var VerificationEngine = class {
1236
1423
  return { type: probe.type, passed: false, details: "Missing selector in probe" };
1237
1424
  }
1238
1425
  const res = await this.sessionManager.sendCommand(sessionId, {
1239
- id: `cmd_${crypto3.randomUUID().slice(0, 8)}`,
1426
+ id: `cmd_${crypto4.randomUUID().slice(0, 8)}`,
1240
1427
  type: "query_element",
1241
1428
  params: { selector: probe.selector }
1242
1429
  });
@@ -1252,7 +1439,7 @@ var VerificationEngine = class {
1252
1439
  return { type: probe.type, passed: false, details: "Missing selector or text in probe" };
1253
1440
  }
1254
1441
  const res = await this.sessionManager.sendCommand(sessionId, {
1255
- id: `cmd_${crypto3.randomUUID().slice(0, 8)}`,
1442
+ id: `cmd_${crypto4.randomUUID().slice(0, 8)}`,
1256
1443
  type: "query_element",
1257
1444
  params: { selector: probe.selector }
1258
1445
  });
@@ -1269,7 +1456,7 @@ var VerificationEngine = class {
1269
1456
  return { type: probe.type, passed: false, details: "Missing route in probe" };
1270
1457
  }
1271
1458
  const res = await this.sessionManager.sendCommand(sessionId, {
1272
- id: `cmd_${crypto3.randomUUID().slice(0, 8)}`,
1459
+ id: `cmd_${crypto4.randomUUID().slice(0, 8)}`,
1273
1460
  type: "get_page_state"
1274
1461
  });
1275
1462
  const currentRoute = res.result?.route || "";
@@ -1298,7 +1485,7 @@ var VerificationEngine = class {
1298
1485
  recordVerification(result, incident) {
1299
1486
  this.db.updateIncidentStatus(incident.id, result.status);
1300
1487
  this.db.insertVerification({
1301
- id: `ver_${crypto3.randomUUID().slice(0, 8)}`,
1488
+ id: `ver_${crypto4.randomUUID().slice(0, 8)}`,
1302
1489
  incidentId: incident.id,
1303
1490
  status: result.status,
1304
1491
  checks: result.checks,
@@ -1310,13 +1497,581 @@ var VerificationEngine = class {
1310
1497
  }
1311
1498
  };
1312
1499
 
1500
+ // packages/daemon/src/server/http.ts
1501
+ import fs4 from "fs";
1502
+ import path5 from "path";
1503
+ import { fileURLToPath } from "url";
1504
+ function readJsonBody(req, res, maxSizeBytes = 10 * 1024 * 1024) {
1505
+ return new Promise((resolve, reject) => {
1506
+ let body = "";
1507
+ let isTooLarge = false;
1508
+ req.on("data", (chunk) => {
1509
+ if (isTooLarge) return;
1510
+ body += chunk;
1511
+ if (body.length > maxSizeBytes) {
1512
+ isTooLarge = true;
1513
+ if (!res.headersSent) {
1514
+ res.writeHead(413, { "Content-Type": "application/json" });
1515
+ res.end(JSON.stringify({ ok: false, error: "Payload too large" }));
1516
+ }
1517
+ req.destroy();
1518
+ reject(new Error("Payload too large"));
1519
+ }
1520
+ });
1521
+ req.on("end", () => {
1522
+ if (isTooLarge) return;
1523
+ try {
1524
+ const parsed = body ? JSON.parse(body) : {};
1525
+ resolve(parsed);
1526
+ } catch (err) {
1527
+ if (!res.headersSent) {
1528
+ res.writeHead(400, { "Content-Type": "application/json" });
1529
+ res.end(JSON.stringify({ ok: false, error: "Malformed JSON payload" }));
1530
+ }
1531
+ reject(err);
1532
+ }
1533
+ });
1534
+ req.on("error", (err) => {
1535
+ reject(err);
1536
+ });
1537
+ });
1538
+ }
1539
+ function createHttpHandler(db, sessionManager, baseScreenshotsDir, verificationEngine, noteVerificationEngine) {
1540
+ return async (req, res) => {
1541
+ try {
1542
+ const parsedUrl = new URL(req.url || "/", `http://${req.headers.host || "127.0.0.1"}`);
1543
+ const pathname = parsedUrl.pathname;
1544
+ res.setHeader("Access-Control-Allow-Origin", "*");
1545
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
1546
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
1547
+ if (req.method === "OPTIONS") {
1548
+ res.writeHead(204);
1549
+ res.end();
1550
+ return;
1551
+ }
1552
+ if (pathname === "/health" || pathname === "/") {
1553
+ res.writeHead(200, { "Content-Type": "application/json" });
1554
+ res.end(
1555
+ JSON.stringify({
1556
+ status: "ok",
1557
+ name: "browsertrack",
1558
+ version: "0.1.0",
1559
+ activeSessions: sessionManager.getActiveCount(),
1560
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1561
+ })
1562
+ );
1563
+ return;
1564
+ }
1565
+ if (pathname === "/client.js" || pathname === "/browserdiag.js") {
1566
+ let scriptContent = "";
1567
+ try {
1568
+ const __filename = fileURLToPath(import.meta.url);
1569
+ const __dirname = path5.dirname(__filename);
1570
+ const candidatePaths = [
1571
+ path5.resolve(__dirname, "../client.iife.js"),
1572
+ path5.resolve(__dirname, "../../dist/client.iife.js"),
1573
+ path5.resolve(__dirname, "../../../dist/client.iife.js")
1574
+ ];
1575
+ for (const p of candidatePaths) {
1576
+ if (fs4.existsSync(p)) {
1577
+ scriptContent = fs4.readFileSync(p, "utf-8");
1578
+ break;
1579
+ }
1580
+ }
1581
+ } catch {
1582
+ }
1583
+ if (!scriptContent) {
1584
+ scriptContent = `console.warn("[BrowserTrack] Standalone client bundle not built yet. Run 'npm run build'.");`;
1585
+ }
1586
+ res.writeHead(200, {
1587
+ "Content-Type": "application/javascript; charset=utf-8",
1588
+ "Cache-Control": "no-cache"
1589
+ });
1590
+ res.end(scriptContent);
1591
+ return;
1592
+ }
1593
+ if (pathname === "/api/projects") {
1594
+ const projects = db.listProjects();
1595
+ res.writeHead(200, { "Content-Type": "application/json" });
1596
+ res.end(JSON.stringify({ ok: true, projects }));
1597
+ return;
1598
+ }
1599
+ if (pathname === "/api/sessions") {
1600
+ const projectId = parsedUrl.searchParams.get("project") || void 0;
1601
+ const activeOnly = parsedUrl.searchParams.get("active") === "true";
1602
+ const sessions = db.listSessions(projectId, activeOnly);
1603
+ res.writeHead(200, { "Content-Type": "application/json" });
1604
+ res.end(JSON.stringify({ ok: true, sessions }));
1605
+ return;
1606
+ }
1607
+ if (pathname === "/api/incidents") {
1608
+ const projectId = parsedUrl.searchParams.get("project") || void 0;
1609
+ const status = parsedUrl.searchParams.get("status") || void 0;
1610
+ const limit = parseInt(parsedUrl.searchParams.get("limit") || "50", 10);
1611
+ const incidents = db.listIncidents({ projectId, status, limit });
1612
+ res.writeHead(200, { "Content-Type": "application/json" });
1613
+ res.end(JSON.stringify({ ok: true, incidents }));
1614
+ return;
1615
+ }
1616
+ if (pathname.startsWith("/api/incidents/")) {
1617
+ const incidentId = pathname.replace("/api/incidents/", "");
1618
+ const incident = db.getIncident(incidentId);
1619
+ if (!incident) {
1620
+ res.writeHead(404, { "Content-Type": "application/json" });
1621
+ res.end(JSON.stringify({ ok: false, error: "Incident not found" }));
1622
+ return;
1623
+ }
1624
+ res.writeHead(200, { "Content-Type": "application/json" });
1625
+ res.end(JSON.stringify({ ok: true, incident }));
1626
+ return;
1627
+ }
1628
+ if (pathname === "/api/notes") {
1629
+ const projectId = parsedUrl.searchParams.get("project") || void 0;
1630
+ const status = parsedUrl.searchParams.get("status") || void 0;
1631
+ const limit = parseInt(parsedUrl.searchParams.get("limit") || "50", 10);
1632
+ const notes = db.listNotes({ projectId, status, limit });
1633
+ res.writeHead(200, { "Content-Type": "application/json" });
1634
+ res.end(JSON.stringify({ ok: true, notes }));
1635
+ return;
1636
+ }
1637
+ if (pathname.startsWith("/api/notes/")) {
1638
+ const noteId = pathname.replace("/api/notes/", "");
1639
+ const note = db.getNote(noteId);
1640
+ if (!note) {
1641
+ res.writeHead(404, { "Content-Type": "application/json" });
1642
+ res.end(JSON.stringify({ ok: false, error: "Note not found" }));
1643
+ return;
1644
+ }
1645
+ res.writeHead(200, { "Content-Type": "application/json" });
1646
+ res.end(JSON.stringify({ ok: true, note }));
1647
+ return;
1648
+ }
1649
+ if (pathname.startsWith("/screenshots/")) {
1650
+ const relativePath = pathname.replace("/screenshots/", "");
1651
+ const filePath = path5.join(baseScreenshotsDir, relativePath);
1652
+ if (fs4.existsSync(filePath) && fs4.statSync(filePath).isFile()) {
1653
+ const ext = path5.extname(filePath).toLowerCase();
1654
+ const mimeTypes = {
1655
+ ".webp": "image/webp",
1656
+ ".png": "image/png",
1657
+ ".jpg": "image/jpeg",
1658
+ ".jpeg": "image/jpeg"
1659
+ };
1660
+ res.writeHead(200, { "Content-Type": mimeTypes[ext] || "application/octet-stream" });
1661
+ fs4.createReadStream(filePath).pipe(res);
1662
+ return;
1663
+ }
1664
+ res.writeHead(404, { "Content-Type": "application/json" });
1665
+ res.end(JSON.stringify({ ok: false, error: "Screenshot not found" }));
1666
+ return;
1667
+ }
1668
+ if (pathname === "/api/command" && req.method === "POST") {
1669
+ try {
1670
+ const data = await readJsonBody(req, res);
1671
+ const sessionId = data.sessionId || sessionManager.getAnyActiveSession()?.id;
1672
+ if (!sessionId) {
1673
+ res.writeHead(400, { "Content-Type": "application/json" });
1674
+ res.end(JSON.stringify({ ok: false, error: "No active browser session connected" }));
1675
+ return;
1676
+ }
1677
+ const cmdRes = await sessionManager.sendCommand(sessionId, data.command, data.timeoutMs || 5e3);
1678
+ res.writeHead(200, { "Content-Type": "application/json" });
1679
+ res.end(JSON.stringify(cmdRes));
1680
+ } catch (err) {
1681
+ if (!res.headersSent) {
1682
+ res.writeHead(500, { "Content-Type": "application/json" });
1683
+ res.end(JSON.stringify({ ok: false, error: err?.message || String(err) }));
1684
+ }
1685
+ }
1686
+ return;
1687
+ }
1688
+ if (pathname === "/api/verify/incident" && req.method === "POST") {
1689
+ try {
1690
+ if (!verificationEngine) {
1691
+ res.writeHead(500, { "Content-Type": "application/json" });
1692
+ res.end(JSON.stringify({ ok: false, error: "Verification engine not attached to daemon" }));
1693
+ return;
1694
+ }
1695
+ const data = await readJsonBody(req, res);
1696
+ const result = await verificationEngine.verifyIncident(data.incidentId, data.options);
1697
+ res.writeHead(200, { "Content-Type": "application/json" });
1698
+ res.end(JSON.stringify({ ok: true, result }));
1699
+ } catch (err) {
1700
+ if (!res.headersSent) {
1701
+ res.writeHead(500, { "Content-Type": "application/json" });
1702
+ res.end(JSON.stringify({ ok: false, error: err?.message || String(err) }));
1703
+ }
1704
+ }
1705
+ return;
1706
+ }
1707
+ if (pathname === "/api/verify/note" && req.method === "POST") {
1708
+ try {
1709
+ if (!noteVerificationEngine) {
1710
+ res.writeHead(500, { "Content-Type": "application/json" });
1711
+ res.end(JSON.stringify({ ok: false, error: "Note verification engine not attached to daemon" }));
1712
+ return;
1713
+ }
1714
+ const data = await readJsonBody(req, res);
1715
+ const result = await noteVerificationEngine.verifyNote(data.noteId, data.options);
1716
+ res.writeHead(200, { "Content-Type": "application/json" });
1717
+ res.end(JSON.stringify({ ok: true, result }));
1718
+ } catch (err) {
1719
+ if (!res.headersSent) {
1720
+ res.writeHead(500, { "Content-Type": "application/json" });
1721
+ res.end(JSON.stringify({ ok: false, error: err?.message || String(err) }));
1722
+ }
1723
+ }
1724
+ return;
1725
+ }
1726
+ res.writeHead(404, { "Content-Type": "application/json" });
1727
+ res.end(JSON.stringify({ ok: false, error: "Not found" }));
1728
+ } catch (err) {
1729
+ if (!res.headersSent) {
1730
+ try {
1731
+ res.writeHead(500, { "Content-Type": "application/json" });
1732
+ res.end(JSON.stringify({ ok: false, error: err?.message || "Internal Server Error" }));
1733
+ } catch {
1734
+ }
1735
+ }
1736
+ }
1737
+ };
1738
+ }
1739
+
1740
+ // packages/daemon/src/server/ws.ts
1741
+ import crypto5 from "crypto";
1742
+ function safeWsSend(ws, payload) {
1743
+ if (ws.readyState !== 1) return false;
1744
+ try {
1745
+ const raw = typeof payload === "string" ? payload : safeJsonStringify(payload);
1746
+ ws.send(raw);
1747
+ return true;
1748
+ } catch {
1749
+ return false;
1750
+ }
1751
+ }
1752
+ function setupWebSocketServer(wss, db, sessionManager, incidentEngine, notesEngine, maxEventsPerSession = 1e3, verbose = false) {
1753
+ wss.on("connection", (ws) => {
1754
+ let currentSessionId = null;
1755
+ ws.on("message", (raw) => {
1756
+ try {
1757
+ const text = typeof raw === "string" ? raw : raw.toString("utf-8");
1758
+ const data = JSON.parse(text);
1759
+ if (data.type === "hello") {
1760
+ const hello = data;
1761
+ const sessionId = `sess_${crypto5.randomUUID().slice(0, 8)}`;
1762
+ currentSessionId = sessionId;
1763
+ let project = hello.projectId ? db.getProject(hello.projectId) : null;
1764
+ if (!project && hello.origin) {
1765
+ project = db.getProjectByOrigin(hello.origin);
1766
+ }
1767
+ if (!project) {
1768
+ let projName = "default";
1769
+ try {
1770
+ const url = new URL(hello.origin || "http://localhost");
1771
+ projName = url.port ? `app-${url.port}` : url.hostname;
1772
+ } catch {
1773
+ projName = "app";
1774
+ }
1775
+ project = db.upsertProject({
1776
+ id: `proj_${crypto5.randomUUID().slice(0, 8)}`,
1777
+ name: projName,
1778
+ origin: hello.origin || "http://localhost"
1779
+ });
1780
+ }
1781
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1782
+ db.upsertSession({
1783
+ id: sessionId,
1784
+ projectId: project.id,
1785
+ origin: hello.origin || "",
1786
+ url: hello.url || "",
1787
+ title: hello.title || "",
1788
+ userAgent: hello.userAgent || "",
1789
+ connectedAt: now,
1790
+ lastSeenAt: now,
1791
+ active: true
1792
+ });
1793
+ sessionManager.registerSocket(sessionId, ws, hello.origin || "", project.id);
1794
+ safeWsSend(ws, {
1795
+ type: "hello_ack",
1796
+ sessionId,
1797
+ projectId: project.id,
1798
+ projectName: project.name
1799
+ });
1800
+ const existingNotes = db.listNotes({ projectId: project.id, limit: 100 });
1801
+ safeWsSend(ws, {
1802
+ type: "notes_sync",
1803
+ notes: existingNotes
1804
+ });
1805
+ if (verbose) {
1806
+ console.log(`[BrowserTrack] New session connected: ${sessionId} (${project.name} @ ${hello.origin})`);
1807
+ }
1808
+ return;
1809
+ }
1810
+ if (data.type === "event") {
1811
+ const eventMsg = data;
1812
+ const sessionId = eventMsg.sessionId || currentSessionId;
1813
+ if (!sessionId) return;
1814
+ const eventId = `evt_${crypto5.randomUUID().slice(0, 8)}`;
1815
+ db.insertEvent({
1816
+ id: eventId,
1817
+ sessionId,
1818
+ eventType: eventMsg.eventType,
1819
+ payload: eventMsg.payload,
1820
+ timestamp: eventMsg.timestamp || Date.now(),
1821
+ route: eventMsg.route,
1822
+ url: eventMsg.url
1823
+ });
1824
+ db.pruneSessionEvents(sessionId, maxEventsPerSession);
1825
+ const incident = incidentEngine.processClientEvent(eventMsg);
1826
+ if (incident && verbose) {
1827
+ console.log(
1828
+ `[BrowserTrack] Incident recorded: ${incident.id} (${incident.type}: ${incident.message}) [${incident.occurrences}x]`
1829
+ );
1830
+ }
1831
+ return;
1832
+ }
1833
+ if (data.type === "create_note" && notesEngine) {
1834
+ const note = notesEngine.createNoteFromClient({
1835
+ sessionId: data.sessionId || currentSessionId || "",
1836
+ noteType: data.noteType,
1837
+ message: data.message,
1838
+ route: data.route,
1839
+ url: data.url,
1840
+ viewport: data.viewport,
1841
+ scroll: data.scroll,
1842
+ target: data.target,
1843
+ elementContext: data.elementContext,
1844
+ region: data.region,
1845
+ screenshot: data.screenshot,
1846
+ incidentId: data.incidentId,
1847
+ scenarioId: data.scenarioId,
1848
+ stepNumber: data.stepNumber,
1849
+ scenarioTitle: data.scenarioTitle
1850
+ });
1851
+ if (verbose) {
1852
+ console.log(
1853
+ `[BrowserTrack] Visual note created: ${note.id} on ${note.route} ("${note.message}")${note.scenarioId ? ` [Scenario: ${note.scenarioTitle || note.scenarioId} Step ${note.stepNumber}]` : ""}`
1854
+ );
1855
+ }
1856
+ safeWsSend(ws, {
1857
+ type: "note_created_ack",
1858
+ noteId: note.id,
1859
+ status: note.status,
1860
+ scenarioId: note.scenarioId,
1861
+ stepNumber: note.stepNumber
1862
+ });
1863
+ const allNotes = db.listNotes({ projectId: note.projectId, limit: 100 });
1864
+ sessionManager.broadcastToProject(note.projectId, {
1865
+ type: "notes_sync",
1866
+ notes: allNotes
1867
+ });
1868
+ return;
1869
+ }
1870
+ if (data.type === "resolve_note" && data.noteId) {
1871
+ const note = db.getNote(data.noteId);
1872
+ if (note) {
1873
+ db.updateNoteStatus(data.noteId, "RESOLVED");
1874
+ const allNotes = db.listNotes({ projectId: note.projectId, limit: 100 });
1875
+ sessionManager.broadcastToProject(note.projectId, {
1876
+ type: "notes_sync",
1877
+ notes: allNotes
1878
+ });
1879
+ }
1880
+ return;
1881
+ }
1882
+ if (data.type === "reopen_note" && data.noteId) {
1883
+ const note = db.getNote(data.noteId);
1884
+ if (note) {
1885
+ db.updateNoteStatus(data.noteId, "OPEN");
1886
+ const allNotes = db.listNotes({ projectId: note.projectId, limit: 100 });
1887
+ sessionManager.broadcastToProject(note.projectId, {
1888
+ type: "notes_sync",
1889
+ notes: allNotes
1890
+ });
1891
+ }
1892
+ return;
1893
+ }
1894
+ if (data.type === "delete_note" && data.noteId) {
1895
+ const note = db.getNote(data.noteId);
1896
+ if (note) {
1897
+ db.deleteNote(data.noteId);
1898
+ const allNotes = db.listNotes({ projectId: note.projectId, limit: 100 });
1899
+ sessionManager.broadcastToProject(note.projectId, {
1900
+ type: "notes_sync",
1901
+ notes: allNotes
1902
+ });
1903
+ }
1904
+ return;
1905
+ }
1906
+ if (data.type === "delete_scenario" && data.scenarioId) {
1907
+ const scenario = db.getScenario(data.scenarioId);
1908
+ if (scenario) {
1909
+ db.deleteScenario(data.scenarioId);
1910
+ const allNotes = db.listNotes({ projectId: scenario.projectId, limit: 100 });
1911
+ sessionManager.broadcastToProject(scenario.projectId, {
1912
+ type: "notes_sync",
1913
+ notes: allNotes
1914
+ });
1915
+ }
1916
+ return;
1917
+ }
1918
+ if (data.type === "get_notes") {
1919
+ const session = currentSessionId ? db.getSession(currentSessionId) : null;
1920
+ const projectId = data.projectId || session?.projectId;
1921
+ if (projectId) {
1922
+ const allNotes = db.listNotes({ projectId, limit: 100 });
1923
+ safeWsSend(ws, {
1924
+ type: "notes_sync",
1925
+ notes: allNotes
1926
+ });
1927
+ }
1928
+ return;
1929
+ }
1930
+ if (data.type === "command_response") {
1931
+ const res = data.response;
1932
+ const sessionId = data.sessionId || currentSessionId;
1933
+ if (sessionId && res) {
1934
+ sessionManager.handleCommandResponse(sessionId, res);
1935
+ }
1936
+ return;
1937
+ }
1938
+ } catch (err) {
1939
+ if (verbose) {
1940
+ console.error("[BrowserTrack] WebSocket message error:", err?.message);
1941
+ }
1942
+ }
1943
+ });
1944
+ ws.on("close", () => {
1945
+ if (currentSessionId) {
1946
+ sessionManager.unregisterSocket(currentSessionId);
1947
+ if (verbose) {
1948
+ console.log(`[BrowserTrack] Session disconnected: ${currentSessionId}`);
1949
+ }
1950
+ }
1951
+ });
1952
+ ws.on("error", () => {
1953
+ if (currentSessionId) {
1954
+ sessionManager.unregisterSocket(currentSessionId);
1955
+ }
1956
+ });
1957
+ });
1958
+ }
1959
+
1960
+ // packages/daemon/src/server/daemon.ts
1961
+ import http from "http";
1962
+ import { WebSocketServer } from "ws";
1963
+ var BrowserTrackDaemon = class {
1964
+ config;
1965
+ db;
1966
+ screenshotStore;
1967
+ sessionManager;
1968
+ incidentEngine;
1969
+ notesEngine;
1970
+ verificationEngine;
1971
+ noteVerificationEngine;
1972
+ httpServer = null;
1973
+ wss = null;
1974
+ isRunning = false;
1975
+ constructor(config = {}) {
1976
+ this.config = getDaemonConfig(config);
1977
+ this.db = new StorageDB(this.config.dbPath);
1978
+ this.screenshotStore = new ScreenshotStore(this.config.screenshotsDir);
1979
+ this.sessionManager = new SessionManager(this.db);
1980
+ this.incidentEngine = new IncidentEngine(this.db, this.screenshotStore);
1981
+ this.notesEngine = new NotesEngine(this.db, this.screenshotStore);
1982
+ this.verificationEngine = new VerificationEngine(this.db, this.sessionManager, this.screenshotStore);
1983
+ this.noteVerificationEngine = new NoteVerificationEngine(this.db, this.sessionManager, this.notesEngine);
1984
+ }
1985
+ async start() {
1986
+ if (this.isRunning) return;
1987
+ const httpHandler = createHttpHandler(
1988
+ this.db,
1989
+ this.sessionManager,
1990
+ this.config.screenshotsDir,
1991
+ this.verificationEngine,
1992
+ this.noteVerificationEngine
1993
+ );
1994
+ this.httpServer = http.createServer(httpHandler);
1995
+ this.wss = new WebSocketServer({ server: this.httpServer });
1996
+ setupWebSocketServer(
1997
+ this.wss,
1998
+ this.db,
1999
+ this.sessionManager,
2000
+ this.incidentEngine,
2001
+ this.notesEngine,
2002
+ this.config.maxEventsPerSession,
2003
+ this.config.verbose
2004
+ );
2005
+ await new Promise((resolve, reject) => {
2006
+ this.httpServer.listen(this.config.port, this.config.host, () => {
2007
+ this.isRunning = true;
2008
+ if (this.config.verbose) {
2009
+ console.log(`[BrowserTrack] Daemon running at http://${this.config.host}:${this.config.port}`);
2010
+ }
2011
+ resolve();
2012
+ });
2013
+ this.httpServer.once("error", (err) => {
2014
+ reject(err);
2015
+ });
2016
+ });
2017
+ }
2018
+ async stop() {
2019
+ if (!this.isRunning) return;
2020
+ if (this.wss) {
2021
+ for (const client of this.wss.clients) {
2022
+ try {
2023
+ client.terminate();
2024
+ } catch {
2025
+ }
2026
+ }
2027
+ await new Promise((resolve) => {
2028
+ this.wss.close(() => resolve());
2029
+ });
2030
+ this.wss = null;
2031
+ }
2032
+ if (this.httpServer) {
2033
+ if (typeof this.httpServer.closeAllConnections === "function") {
2034
+ this.httpServer.closeAllConnections();
2035
+ }
2036
+ await new Promise((resolve) => {
2037
+ this.httpServer.close(() => resolve());
2038
+ });
2039
+ this.httpServer = null;
2040
+ }
2041
+ try {
2042
+ this.db.close();
2043
+ } catch {
2044
+ }
2045
+ this.isRunning = false;
2046
+ }
2047
+ getStatus() {
2048
+ return {
2049
+ isRunning: this.isRunning,
2050
+ host: this.config.host,
2051
+ port: this.config.port,
2052
+ activeSessions: this.sessionManager.getActiveCount(),
2053
+ dbPath: this.config.dbPath
2054
+ };
2055
+ }
2056
+ };
2057
+
2058
+ // packages/daemon/src/index.ts
2059
+ function createDaemon(config = {}) {
2060
+ return new BrowserTrackDaemon(config);
2061
+ }
2062
+
1313
2063
  export {
1314
2064
  getDaemonConfig,
1315
2065
  StorageDB,
1316
2066
  ScreenshotStore,
1317
2067
  SessionManager,
2068
+ IncidentEngine,
1318
2069
  NotesEngine,
1319
2070
  NoteVerificationEngine,
1320
- VerificationEngine
2071
+ VerificationEngine,
2072
+ createHttpHandler,
2073
+ setupWebSocketServer,
2074
+ BrowserTrackDaemon,
2075
+ createDaemon
1321
2076
  };
1322
- //# sourceMappingURL=chunk-3HOXPTM2.js.map
2077
+ //# sourceMappingURL=chunk-AYSVE6NG.js.map