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,635 +0,0 @@
1
- import {
2
- computeFingerprint,
3
- extractSourceFromStack,
4
- normalizeSourceFile,
5
- redactSensitiveData
6
- } from "./chunk-6VA7GBAO.js";
7
- import {
8
- NoteVerificationEngine,
9
- NotesEngine,
10
- ScreenshotStore,
11
- SessionManager,
12
- StorageDB,
13
- VerificationEngine,
14
- getDaemonConfig
15
- } from "./chunk-3HOXPTM2.js";
16
-
17
- // packages/daemon/src/incidents/engine.ts
18
- import crypto from "crypto";
19
- var IncidentEngine = class {
20
- db;
21
- screenshotStore;
22
- constructor(db, screenshotStore) {
23
- this.db = db;
24
- this.screenshotStore = screenshotStore;
25
- }
26
- processClientEvent(message) {
27
- const session = this.db.getSession(message.sessionId);
28
- const projectId = session?.projectId || "default";
29
- const sanitizedBreadcrumbs = (message.breadcrumbs || []).map((b) => redactSensitiveData(b));
30
- const sanitizedLastElement = message.lastElement ? redactSensitiveData(message.lastElement) : void 0;
31
- const networkFailures = sanitizedBreadcrumbs.filter((b) => (b.type === "fetch" || b.type === "xhr") && b.level === "error" && b.data).map((b) => ({
32
- url: b.message.split(" ")[1] || "",
33
- method: b.message.split(" ")[0] || "GET",
34
- status: b.data?.status,
35
- durationMs: b.data?.durationMs || 0,
36
- error: b.data?.error,
37
- aborted: b.data?.aborted,
38
- timestamp: b.timestamp
39
- }));
40
- if (message.eventType === "runtime_error" || message.eventType === "unhandled_rejection") {
41
- const payload = message.payload;
42
- return this.handleErrorEvent({
43
- projectId,
44
- sessionId: message.sessionId,
45
- type: payload.errorType || "runtime_exception",
46
- severity: "error",
47
- message: payload.message || "Unknown runtime error",
48
- stack: payload.stack,
49
- sourceFile: payload.filename,
50
- line: payload.lineno,
51
- column: payload.colno,
52
- route: message.route || "/",
53
- url: message.url,
54
- breadcrumbs: sanitizedBreadcrumbs,
55
- networkFailures,
56
- lastElement: sanitizedLastElement,
57
- screenshotDataUrl: message.screenshot
58
- });
59
- }
60
- if (message.eventType === "console") {
61
- const payload = message.payload;
62
- if (payload.level === "error") {
63
- return this.handleErrorEvent({
64
- projectId,
65
- sessionId: message.sessionId,
66
- type: "console_error",
67
- severity: "error",
68
- message: payload.message,
69
- stack: payload.stack,
70
- route: message.route || "/",
71
- url: message.url,
72
- breadcrumbs: sanitizedBreadcrumbs,
73
- networkFailures,
74
- lastElement: sanitizedLastElement,
75
- screenshotDataUrl: message.screenshot
76
- });
77
- }
78
- }
79
- return null;
80
- }
81
- handleErrorEvent(input) {
82
- const extracted = (!input.sourceFile || !input.line) && input.stack ? extractSourceFromStack(input.stack) : null;
83
- const rawSource = input.sourceFile || extracted?.file || "unknown_source";
84
- const sourceFile = normalizeSourceFile(rawSource);
85
- const line = input.line || extracted?.line || 0;
86
- const column = input.column || extracted?.column;
87
- const fingerprint = computeFingerprint({
88
- type: input.type,
89
- message: input.message,
90
- sourceFile,
91
- line,
92
- column,
93
- stack: input.stack
94
- });
95
- const now = (/* @__PURE__ */ new Date()).toISOString();
96
- const existing = this.db.findIncidentByFingerprint(fingerprint);
97
- if (existing) {
98
- const updatedOccurrences = existing.occurrences + 1;
99
- this.db.updateIncidentOccurrence(existing.id, {
100
- sessionId: input.sessionId,
101
- lastSeen: now,
102
- occurrences: updatedOccurrences,
103
- route: input.route,
104
- breadcrumbs: input.breadcrumbs,
105
- lastElement: input.lastElement,
106
- stack: input.stack
107
- });
108
- const occurrenceId = `occ_${crypto.randomUUID().slice(0, 8)}`;
109
- this.db.insertIncidentOccurrence({
110
- id: occurrenceId,
111
- incidentId: existing.id,
112
- sessionId: input.sessionId,
113
- timestamp: now,
114
- route: input.route,
115
- url: input.url,
116
- stack: input.stack,
117
- breadcrumbs: input.breadcrumbs,
118
- lastElement: input.lastElement
119
- });
120
- return {
121
- ...existing,
122
- occurrences: updatedOccurrences,
123
- lastSeen: now,
124
- breadcrumbs: input.breadcrumbs,
125
- lastElement: input.lastElement || existing.lastElement
126
- };
127
- }
128
- const incidentId = `inc_${crypto.randomUUID().slice(0, 8)}`;
129
- let screenshotPath;
130
- if (input.screenshotDataUrl) {
131
- const saved = this.screenshotStore.saveScreenshot(input.projectId, incidentId, "error", input.screenshotDataUrl);
132
- if (saved) {
133
- screenshotPath = saved.filePath;
134
- }
135
- }
136
- const newIncident = {
137
- id: incidentId,
138
- projectId: input.projectId,
139
- sessionId: input.sessionId,
140
- type: input.type,
141
- severity: input.severity,
142
- message: input.message,
143
- source: {
144
- file: sourceFile,
145
- line,
146
- column
147
- },
148
- fingerprint,
149
- route: input.route,
150
- firstSeen: now,
151
- lastSeen: now,
152
- occurrences: 1,
153
- status: "OPEN",
154
- stack: input.stack,
155
- breadcrumbs: input.breadcrumbs,
156
- networkFailures: input.networkFailures,
157
- lastElement: input.lastElement,
158
- screenshots: screenshotPath ? { error: screenshotPath } : void 0
159
- };
160
- this.db.insertIncident(newIncident);
161
- this.db.insertIncidentOccurrence({
162
- id: `occ_${crypto.randomUUID().slice(0, 8)}`,
163
- incidentId,
164
- sessionId: input.sessionId,
165
- timestamp: now,
166
- route: input.route,
167
- url: input.url,
168
- stack: input.stack,
169
- breadcrumbs: input.breadcrumbs,
170
- lastElement: input.lastElement
171
- });
172
- return newIncident;
173
- }
174
- };
175
-
176
- // packages/daemon/src/server/http.ts
177
- import fs from "fs";
178
- import path from "path";
179
- import { fileURLToPath } from "url";
180
- function createHttpHandler(db, sessionManager, baseScreenshotsDir) {
181
- return (req, res) => {
182
- const parsedUrl = new URL(req.url || "/", `http://${req.headers.host || "127.0.0.1"}`);
183
- const pathname = parsedUrl.pathname;
184
- res.setHeader("Access-Control-Allow-Origin", "*");
185
- res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
186
- res.setHeader("Access-Control-Allow-Headers", "Content-Type");
187
- if (req.method === "OPTIONS") {
188
- res.writeHead(204);
189
- res.end();
190
- return;
191
- }
192
- if (pathname === "/health" || pathname === "/") {
193
- res.writeHead(200, { "Content-Type": "application/json" });
194
- res.end(
195
- JSON.stringify({
196
- status: "ok",
197
- name: "browsertrack",
198
- version: "0.1.0",
199
- activeSessions: sessionManager.getActiveCount(),
200
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
201
- })
202
- );
203
- return;
204
- }
205
- if (pathname === "/client.js" || pathname === "/browserdiag.js") {
206
- let scriptContent = "";
207
- try {
208
- const __filename = fileURLToPath(import.meta.url);
209
- const __dirname = path.dirname(__filename);
210
- const candidatePaths = [
211
- path.resolve(__dirname, "../client.iife.js"),
212
- path.resolve(__dirname, "../../dist/client.iife.js"),
213
- path.resolve(__dirname, "../../../dist/client.iife.js")
214
- ];
215
- for (const p of candidatePaths) {
216
- if (fs.existsSync(p)) {
217
- scriptContent = fs.readFileSync(p, "utf-8");
218
- break;
219
- }
220
- }
221
- } catch {
222
- }
223
- if (!scriptContent) {
224
- scriptContent = `console.warn("[BrowserTrack] Standalone client bundle not built yet. Run 'npm run build'.");`;
225
- }
226
- res.writeHead(200, {
227
- "Content-Type": "application/javascript; charset=utf-8",
228
- "Cache-Control": "no-cache"
229
- });
230
- res.end(scriptContent);
231
- return;
232
- }
233
- if (pathname === "/api/projects") {
234
- const projects = db.listProjects();
235
- res.writeHead(200, { "Content-Type": "application/json" });
236
- res.end(JSON.stringify({ ok: true, projects }));
237
- return;
238
- }
239
- if (pathname === "/api/sessions") {
240
- const projectId = parsedUrl.searchParams.get("project") || void 0;
241
- const activeOnly = parsedUrl.searchParams.get("active") === "true";
242
- const sessions = db.listSessions(projectId, activeOnly);
243
- res.writeHead(200, { "Content-Type": "application/json" });
244
- res.end(JSON.stringify({ ok: true, sessions }));
245
- return;
246
- }
247
- if (pathname === "/api/incidents") {
248
- const projectId = parsedUrl.searchParams.get("project") || void 0;
249
- const status = parsedUrl.searchParams.get("status") || void 0;
250
- const limit = parseInt(parsedUrl.searchParams.get("limit") || "50", 10);
251
- const incidents = db.listIncidents({ projectId, status, limit });
252
- res.writeHead(200, { "Content-Type": "application/json" });
253
- res.end(JSON.stringify({ ok: true, incidents }));
254
- return;
255
- }
256
- if (pathname.startsWith("/api/incidents/")) {
257
- const incidentId = pathname.replace("/api/incidents/", "");
258
- const incident = db.getIncident(incidentId);
259
- if (!incident) {
260
- res.writeHead(404, { "Content-Type": "application/json" });
261
- res.end(JSON.stringify({ ok: false, error: "Incident not found" }));
262
- return;
263
- }
264
- res.writeHead(200, { "Content-Type": "application/json" });
265
- res.end(JSON.stringify({ ok: true, incident }));
266
- return;
267
- }
268
- if (pathname === "/api/notes") {
269
- const projectId = parsedUrl.searchParams.get("project") || void 0;
270
- const status = parsedUrl.searchParams.get("status") || void 0;
271
- const limit = parseInt(parsedUrl.searchParams.get("limit") || "50", 10);
272
- const notes = db.listNotes({ projectId, status, limit });
273
- res.writeHead(200, { "Content-Type": "application/json" });
274
- res.end(JSON.stringify({ ok: true, notes }));
275
- return;
276
- }
277
- if (pathname.startsWith("/api/notes/")) {
278
- const noteId = pathname.replace("/api/notes/", "");
279
- const note = db.getNote(noteId);
280
- if (!note) {
281
- res.writeHead(404, { "Content-Type": "application/json" });
282
- res.end(JSON.stringify({ ok: false, error: "Note not found" }));
283
- return;
284
- }
285
- res.writeHead(200, { "Content-Type": "application/json" });
286
- res.end(JSON.stringify({ ok: true, note }));
287
- return;
288
- }
289
- if (pathname.startsWith("/screenshots/")) {
290
- const relativePath = pathname.replace("/screenshots/", "");
291
- const filePath = path.join(baseScreenshotsDir, relativePath);
292
- if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
293
- const ext = path.extname(filePath).toLowerCase();
294
- const mimeTypes = {
295
- ".webp": "image/webp",
296
- ".png": "image/png",
297
- ".jpg": "image/jpeg",
298
- ".jpeg": "image/jpeg"
299
- };
300
- res.writeHead(200, { "Content-Type": mimeTypes[ext] || "application/octet-stream" });
301
- fs.createReadStream(filePath).pipe(res);
302
- return;
303
- }
304
- res.writeHead(404, { "Content-Type": "application/json" });
305
- res.end(JSON.stringify({ ok: false, error: "Screenshot not found" }));
306
- return;
307
- }
308
- res.writeHead(404, { "Content-Type": "application/json" });
309
- res.end(JSON.stringify({ ok: false, error: "Not found" }));
310
- };
311
- }
312
-
313
- // packages/daemon/src/server/ws.ts
314
- import crypto2 from "crypto";
315
- function setupWebSocketServer(wss, db, sessionManager, incidentEngine, notesEngine, maxEventsPerSession = 1e3, verbose = false) {
316
- wss.on("connection", (ws) => {
317
- let currentSessionId = null;
318
- ws.on("message", (raw) => {
319
- try {
320
- const text = typeof raw === "string" ? raw : raw.toString("utf-8");
321
- const data = JSON.parse(text);
322
- if (data.type === "hello") {
323
- const hello = data;
324
- const sessionId = `sess_${crypto2.randomUUID().slice(0, 8)}`;
325
- currentSessionId = sessionId;
326
- let project = hello.projectId ? db.getProject(hello.projectId) : null;
327
- if (!project && hello.origin) {
328
- project = db.getProjectByOrigin(hello.origin);
329
- }
330
- if (!project) {
331
- let projName = "default";
332
- try {
333
- const url = new URL(hello.origin || "http://localhost");
334
- projName = url.port ? `app-${url.port}` : url.hostname;
335
- } catch {
336
- projName = "app";
337
- }
338
- project = db.upsertProject({
339
- id: `proj_${crypto2.randomUUID().slice(0, 8)}`,
340
- name: projName,
341
- origin: hello.origin || "http://localhost"
342
- });
343
- }
344
- const now = (/* @__PURE__ */ new Date()).toISOString();
345
- db.upsertSession({
346
- id: sessionId,
347
- projectId: project.id,
348
- origin: hello.origin || "",
349
- url: hello.url || "",
350
- title: hello.title || "",
351
- userAgent: hello.userAgent || "",
352
- connectedAt: now,
353
- lastSeenAt: now,
354
- active: true
355
- });
356
- sessionManager.registerSocket(sessionId, ws, hello.origin || "", project.id);
357
- ws.send(
358
- JSON.stringify({
359
- type: "hello_ack",
360
- sessionId,
361
- projectId: project.id,
362
- projectName: project.name
363
- })
364
- );
365
- const existingNotes = db.listNotes({ projectId: project.id, limit: 100 });
366
- ws.send(
367
- JSON.stringify({
368
- type: "notes_sync",
369
- notes: existingNotes
370
- })
371
- );
372
- if (verbose) {
373
- console.log(`[BrowserTrack] New session connected: ${sessionId} (${project.name} @ ${hello.origin})`);
374
- }
375
- return;
376
- }
377
- if (data.type === "event") {
378
- const eventMsg = data;
379
- const sessionId = eventMsg.sessionId || currentSessionId;
380
- if (!sessionId) return;
381
- const eventId = `evt_${crypto2.randomUUID().slice(0, 8)}`;
382
- db.insertEvent({
383
- id: eventId,
384
- sessionId,
385
- eventType: eventMsg.eventType,
386
- payload: eventMsg.payload,
387
- timestamp: eventMsg.timestamp || Date.now(),
388
- route: eventMsg.route,
389
- url: eventMsg.url
390
- });
391
- db.pruneSessionEvents(sessionId, maxEventsPerSession);
392
- const incident = incidentEngine.processClientEvent(eventMsg);
393
- if (incident && verbose) {
394
- console.log(
395
- `[BrowserTrack] Incident recorded: ${incident.id} (${incident.type}: ${incident.message}) [${incident.occurrences}x]`
396
- );
397
- }
398
- return;
399
- }
400
- if (data.type === "create_note" && notesEngine) {
401
- const note = notesEngine.createNoteFromClient({
402
- sessionId: data.sessionId || currentSessionId || "",
403
- noteType: data.noteType,
404
- message: data.message,
405
- route: data.route,
406
- url: data.url,
407
- viewport: data.viewport,
408
- scroll: data.scroll,
409
- target: data.target,
410
- elementContext: data.elementContext,
411
- region: data.region,
412
- screenshot: data.screenshot,
413
- incidentId: data.incidentId,
414
- scenarioId: data.scenarioId,
415
- stepNumber: data.stepNumber,
416
- scenarioTitle: data.scenarioTitle
417
- });
418
- if (verbose) {
419
- console.log(
420
- `[BrowserTrack] Visual note created: ${note.id} on ${note.route} ("${note.message}")${note.scenarioId ? ` [Scenario: ${note.scenarioTitle || note.scenarioId} Step ${note.stepNumber}]` : ""}`
421
- );
422
- }
423
- ws.send(
424
- JSON.stringify({
425
- type: "note_created_ack",
426
- noteId: note.id,
427
- status: note.status,
428
- scenarioId: note.scenarioId,
429
- stepNumber: note.stepNumber
430
- })
431
- );
432
- const allNotes = db.listNotes({ projectId: note.projectId, limit: 100 });
433
- sessionManager.broadcastToProject(note.projectId, {
434
- type: "notes_sync",
435
- notes: allNotes
436
- });
437
- return;
438
- }
439
- if (data.type === "resolve_note" && data.noteId) {
440
- const note = db.getNote(data.noteId);
441
- if (note) {
442
- db.updateNoteStatus(data.noteId, "RESOLVED");
443
- const allNotes = db.listNotes({ projectId: note.projectId, limit: 100 });
444
- sessionManager.broadcastToProject(note.projectId, {
445
- type: "notes_sync",
446
- notes: allNotes
447
- });
448
- }
449
- return;
450
- }
451
- if (data.type === "reopen_note" && data.noteId) {
452
- const note = db.getNote(data.noteId);
453
- if (note) {
454
- db.updateNoteStatus(data.noteId, "OPEN");
455
- const allNotes = db.listNotes({ projectId: note.projectId, limit: 100 });
456
- sessionManager.broadcastToProject(note.projectId, {
457
- type: "notes_sync",
458
- notes: allNotes
459
- });
460
- }
461
- return;
462
- }
463
- if (data.type === "delete_note" && data.noteId) {
464
- const note = db.getNote(data.noteId);
465
- if (note) {
466
- db.deleteNote(data.noteId);
467
- const allNotes = db.listNotes({ projectId: note.projectId, limit: 100 });
468
- sessionManager.broadcastToProject(note.projectId, {
469
- type: "notes_sync",
470
- notes: allNotes
471
- });
472
- }
473
- return;
474
- }
475
- if (data.type === "delete_scenario" && data.scenarioId) {
476
- const scenario = db.getScenario(data.scenarioId);
477
- if (scenario) {
478
- db.deleteScenario(data.scenarioId);
479
- const allNotes = db.listNotes({ projectId: scenario.projectId, limit: 100 });
480
- sessionManager.broadcastToProject(scenario.projectId, {
481
- type: "notes_sync",
482
- notes: allNotes
483
- });
484
- }
485
- return;
486
- }
487
- if (data.type === "get_notes") {
488
- const session = currentSessionId ? db.getSession(currentSessionId) : null;
489
- const projectId = data.projectId || session?.projectId;
490
- if (projectId) {
491
- const allNotes = db.listNotes({ projectId, limit: 100 });
492
- ws.send(
493
- JSON.stringify({
494
- type: "notes_sync",
495
- notes: allNotes
496
- })
497
- );
498
- }
499
- return;
500
- }
501
- if (data.type === "command_response") {
502
- const res = data.response;
503
- const sessionId = data.sessionId || currentSessionId;
504
- if (sessionId && res) {
505
- sessionManager.handleCommandResponse(sessionId, res);
506
- }
507
- return;
508
- }
509
- } catch (err) {
510
- if (verbose) {
511
- console.error("[BrowserTrack] WebSocket message error:", err?.message);
512
- }
513
- }
514
- });
515
- ws.on("close", () => {
516
- if (currentSessionId) {
517
- sessionManager.unregisterSocket(currentSessionId);
518
- if (verbose) {
519
- console.log(`[BrowserTrack] Session disconnected: ${currentSessionId}`);
520
- }
521
- }
522
- });
523
- ws.on("error", () => {
524
- if (currentSessionId) {
525
- sessionManager.unregisterSocket(currentSessionId);
526
- }
527
- });
528
- });
529
- }
530
-
531
- // packages/daemon/src/server/daemon.ts
532
- import http from "http";
533
- import { WebSocketServer } from "ws";
534
- var BrowserTrackDaemon = class {
535
- config;
536
- db;
537
- screenshotStore;
538
- sessionManager;
539
- incidentEngine;
540
- notesEngine;
541
- verificationEngine;
542
- noteVerificationEngine;
543
- httpServer = null;
544
- wss = null;
545
- isRunning = false;
546
- constructor(config = {}) {
547
- this.config = getDaemonConfig(config);
548
- this.db = new StorageDB(this.config.dbPath);
549
- this.screenshotStore = new ScreenshotStore(this.config.screenshotsDir);
550
- this.sessionManager = new SessionManager(this.db);
551
- this.incidentEngine = new IncidentEngine(this.db, this.screenshotStore);
552
- this.notesEngine = new NotesEngine(this.db, this.screenshotStore);
553
- this.verificationEngine = new VerificationEngine(this.db, this.sessionManager, this.screenshotStore);
554
- this.noteVerificationEngine = new NoteVerificationEngine(this.db, this.sessionManager, this.notesEngine);
555
- }
556
- async start() {
557
- if (this.isRunning) return;
558
- const httpHandler = createHttpHandler(this.db, this.sessionManager, this.config.screenshotsDir);
559
- this.httpServer = http.createServer(httpHandler);
560
- this.wss = new WebSocketServer({ server: this.httpServer });
561
- setupWebSocketServer(
562
- this.wss,
563
- this.db,
564
- this.sessionManager,
565
- this.incidentEngine,
566
- this.notesEngine,
567
- this.config.maxEventsPerSession,
568
- this.config.verbose
569
- );
570
- await new Promise((resolve, reject) => {
571
- this.httpServer.listen(this.config.port, this.config.host, () => {
572
- this.isRunning = true;
573
- if (this.config.verbose) {
574
- console.log(`[BrowserTrack] Daemon running at http://${this.config.host}:${this.config.port}`);
575
- }
576
- resolve();
577
- });
578
- this.httpServer.once("error", (err) => {
579
- reject(err);
580
- });
581
- });
582
- }
583
- async stop() {
584
- if (!this.isRunning) return;
585
- if (this.wss) {
586
- for (const client of this.wss.clients) {
587
- try {
588
- client.terminate();
589
- } catch {
590
- }
591
- }
592
- await new Promise((resolve) => {
593
- this.wss.close(() => resolve());
594
- });
595
- this.wss = null;
596
- }
597
- if (this.httpServer) {
598
- if (typeof this.httpServer.closeAllConnections === "function") {
599
- this.httpServer.closeAllConnections();
600
- }
601
- await new Promise((resolve) => {
602
- this.httpServer.close(() => resolve());
603
- });
604
- this.httpServer = null;
605
- }
606
- try {
607
- this.db.close();
608
- } catch {
609
- }
610
- this.isRunning = false;
611
- }
612
- getStatus() {
613
- return {
614
- isRunning: this.isRunning,
615
- host: this.config.host,
616
- port: this.config.port,
617
- activeSessions: this.sessionManager.getActiveCount(),
618
- dbPath: this.config.dbPath
619
- };
620
- }
621
- };
622
-
623
- // packages/daemon/src/index.ts
624
- function createDaemon(config = {}) {
625
- return new BrowserTrackDaemon(config);
626
- }
627
-
628
- export {
629
- IncidentEngine,
630
- createHttpHandler,
631
- setupWebSocketServer,
632
- BrowserTrackDaemon,
633
- createDaemon
634
- };
635
- //# sourceMappingURL=chunk-7OCOQGDN.js.map