browsertrack 0.2.1 → 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 (52) hide show
  1. package/AGENTS.md +9 -6
  2. package/README.md +2 -0
  3. package/dist/{chunk-INXDWPJW.js → chunk-4HRLW6YF.js} +161 -106
  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-464D4U2U.js → chunk-TWEYRBDU.js} +279 -43
  10. package/dist/chunk-TWEYRBDU.js.map +1 -0
  11. package/dist/cli/index.js +1038 -545
  12. package/dist/cli/index.js.map +1 -1
  13. package/dist/client/index.cjs +184 -104
  14. package/dist/client/index.js +2 -2
  15. package/dist/client.iife.js +9 -9
  16. package/dist/core/index.d.ts +24 -1
  17. package/dist/core/index.js +9 -1
  18. package/dist/daemon/index.d.ts +2 -2
  19. package/dist/daemon/index.js +6 -8
  20. package/dist/index.d.ts +2 -2
  21. package/dist/index.js +14 -7
  22. package/dist/mcp/index.d.ts +1 -1
  23. package/dist/mcp/index.js +7 -4
  24. package/dist/{server-DiVmTrIR.d.ts → server-DjV7RWQM.d.ts} +9 -1
  25. package/docs/cli.md +4 -1
  26. package/docs/getting-started.md +60 -6
  27. package/docs/mcp-reference.md +42 -0
  28. package/package.json +1 -1
  29. package/packages/cli/src/index.ts +247 -151
  30. package/packages/client/src/interceptors/navigation.ts +38 -26
  31. package/packages/client/src/interceptors/network.ts +22 -17
  32. package/packages/client/src/notes/inspector.ts +81 -48
  33. package/packages/client/src/source/resolver.ts +9 -3
  34. package/packages/client/src/transport/websocket.ts +23 -18
  35. package/packages/core/src/index.ts +1 -0
  36. package/packages/core/src/safety.ts +86 -0
  37. package/packages/daemon/src/server/daemon.ts +7 -1
  38. package/packages/daemon/src/server/http.ts +125 -5
  39. package/packages/daemon/src/server/ws.ts +33 -29
  40. package/packages/daemon/src/storage/db.ts +57 -35
  41. package/packages/mcp/src/handlers.ts +114 -45
  42. package/packages/mcp/src/server.ts +202 -2
  43. package/test/core/safety.test.ts +106 -0
  44. package/test/daemon/storage.test.ts +36 -0
  45. package/test/e2e/daemon-mcp-e2e.test.ts +10 -0
  46. package/test/mcp/auto-start.test.ts +87 -0
  47. package/dist/chunk-3HOXPTM2.js.map +0 -1
  48. package/dist/chunk-464D4U2U.js.map +0 -1
  49. package/dist/chunk-6VA7GBAO.js.map +0 -1
  50. package/dist/chunk-7OCOQGDN.js +0 -635
  51. package/dist/chunk-7OCOQGDN.js.map +0 -1
  52. package/dist/chunk-INXDWPJW.js.map +0 -1
@@ -4,7 +4,16 @@ import path from 'node:path';
4
4
  import { getDaemonConfig } from '../../daemon/src/config.js';
5
5
  import { createDaemon } from '../../daemon/src/index.js';
6
6
  import { StorageDB } from '../../daemon/src/storage/db.js';
7
- import { createMcpServer } from '../../mcp/src/server.js';
7
+ import { createMcpServer, isDaemonRunning } from '../../mcp/src/server.js';
8
+
9
+ // Global process safety handlers to prevent fatal crashes
10
+ process.on('uncaughtException', (err: any) => {
11
+ console.error('[BrowserTrack] Uncaught Exception:', err?.message || err);
12
+ });
13
+
14
+ process.on('unhandledRejection', (reason: any) => {
15
+ console.error('[BrowserTrack] Unhandled Rejection:', reason?.message || reason);
16
+ });
8
17
 
9
18
  const program = new Command();
10
19
  program.name('browsertrack').description('Local browser diagnostics + MCP bridge for coding agents').version('0.1.0');
@@ -65,6 +74,11 @@ program
65
74
  }
66
75
 
67
76
  const port = parseInt(options.port, 10);
77
+ const isRunningHttp = await isDaemonRunning(options.host, port);
78
+ if (isRunningHttp) {
79
+ console.log(`[BrowserTrack] Daemon is already running on http://${options.host}:${port} (active via MCP Server).`);
80
+ return;
81
+ }
68
82
  const daemon = createDaemon({
69
83
  port,
70
84
  host: options.host,
@@ -120,6 +134,13 @@ program
120
134
  .action(async () => {
121
135
  const pid = readPid();
122
136
  if (!pid || !isProcessRunning(pid)) {
137
+ const config = getDaemonConfig();
138
+ const isRunningHttp = await isDaemonRunning(config.host, config.port);
139
+ if (isRunningHttp) {
140
+ console.log('[BrowserTrack] Daemon is actively maintained by an MCP Server process in your IDE and will shut down when your editor session ends.');
141
+ removePid();
142
+ return;
143
+ }
123
144
  console.log('[BrowserTrack] Daemon is not currently running.');
124
145
  removePid();
125
146
  return;
@@ -140,11 +161,20 @@ program
140
161
  .description('Check if the BrowserTrack daemon is running and view active sessions')
141
162
  .action(async () => {
142
163
  const pid = readPid();
143
- const isRunning = pid ? isProcessRunning(pid) : false;
144
164
  const config = getDaemonConfig();
165
+ let isRunning = pid ? isProcessRunning(pid) : false;
166
+ let detail = isRunning ? ` (PID: ${pid})` : '';
167
+
168
+ if (!isRunning) {
169
+ const isRunningHttp = await isDaemonRunning(config.host, config.port);
170
+ if (isRunningHttp) {
171
+ isRunning = true;
172
+ detail = ' (Active via MCP Server)';
173
+ }
174
+ }
145
175
 
146
176
  console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
147
- console.log(` Status: ${isRunning ? '🟢 RUNNING' : '⚪ STOPPED'}${isRunning ? ` (PID: ${pid})` : ''}`);
177
+ console.log(` Status: ${isRunning ? '🟢 RUNNING' : '⚪ STOPPED'}${detail}`);
148
178
  console.log(` Endpoint: http://${config.host}:${config.port}`);
149
179
  console.log(` Database: ${config.dbPath}`);
150
180
 
@@ -174,42 +204,54 @@ projectCommand
174
204
  .requiredOption('-o, --origin <origin>', 'Project origin (e.g. http://localhost:5173)')
175
205
  .option('-p, --path <path>', 'Filesystem path (e.g. /path/to/project)')
176
206
  .action((name, options) => {
177
- const config = getDaemonConfig();
178
- const db = new StorageDB(config.dbPath);
179
- const resolvedPath = options.path ? path.resolve(options.path) : undefined;
180
-
181
- const proj = db.upsertProject({
182
- id: `proj_${name}`,
183
- name,
184
- origin: options.origin,
185
- path: resolvedPath,
186
- });
187
-
188
- console.log(`[BrowserTrack] Registered project '${proj.name}':`);
189
- console.log(` ID: ${proj.id}`);
190
- console.log(` Origin: ${proj.origin}`);
191
- console.log(` Path: ${proj.path || '(none)'}`);
192
- db.close();
207
+ try {
208
+ const config = getDaemonConfig();
209
+ const db = new StorageDB(config.dbPath);
210
+ try {
211
+ const resolvedPath = options.path ? path.resolve(options.path) : undefined;
212
+ const proj = db.upsertProject({
213
+ id: `proj_${name}`,
214
+ name,
215
+ origin: options.origin,
216
+ path: resolvedPath,
217
+ });
218
+
219
+ console.log(`[BrowserTrack] Registered project '${proj.name}':`);
220
+ console.log(` ID: ${proj.id}`);
221
+ console.log(` Origin: ${proj.origin}`);
222
+ console.log(` Path: ${proj.path || '(none)'}`);
223
+ } finally {
224
+ db.close();
225
+ }
226
+ } catch (err: any) {
227
+ console.error('[BrowserTrack] Failed to register project:', err?.message || err);
228
+ }
193
229
  });
194
230
 
195
231
  program
196
232
  .command('projects')
197
233
  .description('List all tracked projects')
198
234
  .action(() => {
199
- const config = getDaemonConfig();
200
- const db = new StorageDB(config.dbPath);
201
- const projects = db.listProjects();
202
-
203
- if (projects.length === 0) {
204
- console.log('[BrowserTrack] No projects registered yet. Projects will be auto-detected upon browser connection.');
205
- } else {
206
- console.log('\nTracked Projects:');
207
- for (const p of projects) {
208
- console.log(` • ${p.name.padEnd(16)} | ${p.origin.padEnd(26)} | ${p.path || '(auto-detected)'}`);
235
+ try {
236
+ const config = getDaemonConfig();
237
+ const db = new StorageDB(config.dbPath);
238
+ try {
239
+ const projects = db.listProjects();
240
+ if (projects.length === 0) {
241
+ console.log('[BrowserTrack] No projects registered yet. Projects will be auto-detected upon browser connection.');
242
+ } else {
243
+ console.log('\nTracked Projects:');
244
+ for (const p of projects) {
245
+ console.log(` • ${p.name.padEnd(16)} | ${p.origin.padEnd(26)} | ${p.path || '(auto-detected)'}`);
246
+ }
247
+ console.log('');
248
+ }
249
+ } finally {
250
+ db.close();
209
251
  }
210
- console.log('');
252
+ } catch (err: any) {
253
+ console.error('[BrowserTrack] Failed to list projects:', err?.message || err);
211
254
  }
212
- db.close();
213
255
  });
214
256
 
215
257
  // 5. ERRORS / INCIDENTS
@@ -220,34 +262,41 @@ program
220
262
  .option('-s, --status <status>', 'Filter by status (OPEN, VERIFIED, FAILED, etc.)')
221
263
  .option('-l, --limit <number>', 'Limit result count', '20')
222
264
  .action((options) => {
223
- const config = getDaemonConfig();
224
- const db = new StorageDB(config.dbPath);
225
- const limit = parseInt(options.limit, 10);
226
- const incidents = db.listIncidents({
227
- projectId: options.project,
228
- status: options.status,
229
- limit,
230
- });
231
-
232
- if (incidents.length === 0) {
233
- console.log('[BrowserTrack] No incidents found matching the filter.');
234
- } else {
235
- console.log(`\nIncidents (${incidents.length}):`);
236
- for (const inc of incidents) {
237
- const statusBadge =
238
- inc.status === 'OPEN'
239
- ? '🔴 OPEN'
240
- : inc.status === 'VERIFIED'
241
- ? '🟢 VERIFIED'
242
- : inc.status === 'FAILED'
243
- ? ' FAILED'
244
- : `⚪ ${inc.status}`;
245
- console.log(` ${inc.id.padEnd(12)} [${statusBadge}] (${inc.occurrences}x) ${inc.type}: ${inc.message}`);
246
- console.log(` Source: ${inc.source.file}:${inc.source.line} | Route: ${inc.route}`);
265
+ try {
266
+ const config = getDaemonConfig();
267
+ const db = new StorageDB(config.dbPath);
268
+ try {
269
+ const limit = parseInt(options.limit, 10);
270
+ const incidents = db.listIncidents({
271
+ projectId: options.project,
272
+ status: options.status,
273
+ limit,
274
+ });
275
+
276
+ if (incidents.length === 0) {
277
+ console.log('[BrowserTrack] No incidents found matching the filter.');
278
+ } else {
279
+ console.log(`\nIncidents (${incidents.length}):`);
280
+ for (const inc of incidents) {
281
+ const statusBadge =
282
+ inc.status === 'OPEN'
283
+ ? '🔴 OPEN'
284
+ : inc.status === 'VERIFIED'
285
+ ? '🟢 VERIFIED'
286
+ : inc.status === 'FAILED'
287
+ ? '❌ FAILED'
288
+ : `⚪ ${inc.status}`;
289
+ console.log(` ${inc.id.padEnd(12)} [${statusBadge}] (${inc.occurrences}x) ${inc.type}: ${inc.message}`);
290
+ console.log(` Source: ${inc.source.file}:${inc.source.line} | Route: ${inc.route}`);
291
+ }
292
+ console.log('');
293
+ }
294
+ } finally {
295
+ db.close();
247
296
  }
248
- console.log('');
297
+ } catch (err: any) {
298
+ console.error('[BrowserTrack] Failed to list incidents:', err?.message || err);
249
299
  }
250
- db.close();
251
300
  });
252
301
 
253
302
  // 6. VISUAL NOTES
@@ -257,63 +306,81 @@ noteCmd
257
306
  .command('show <noteId>')
258
307
  .description('Show full context for a specific visual note')
259
308
  .action((noteId) => {
260
- const config = getDaemonConfig();
261
- const db = new StorageDB(config.dbPath);
262
- const note = db.getNote(noteId);
263
-
264
- if (!note) {
265
- console.log(`[BrowserTrack] Note '${noteId}' not found.`);
266
- } else {
267
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
268
- console.log(` 📝 Visual Note: ${note.id} [${note.status}]`);
269
- console.log(` 📍 Route: ${note.route} (${note.url})`);
270
- console.log(` 📐 Viewport: ${note.viewport.width} × ${note.viewport.height} (dpr: ${note.viewport.devicePixelRatio})`);
271
- if (note.target) {
272
- console.log(` 🎯 Target: ${note.target.selector}`);
273
- console.log(` Bounds: x:${note.target.boundingRect.x}, y:${note.target.boundingRect.y}, ${note.target.boundingRect.width}×${note.target.boundingRect.height}`);
274
- }
275
- console.log(` 💬 Note: "${note.message}"`);
276
- if (note.screenshots?.original) {
277
- console.log(` 🖼️ Screenshot: ${note.screenshots.original}`);
309
+ try {
310
+ const config = getDaemonConfig();
311
+ const db = new StorageDB(config.dbPath);
312
+ try {
313
+ const note = db.getNote(noteId);
314
+ if (!note) {
315
+ console.log(`[BrowserTrack] Note '${noteId}' not found.`);
316
+ } else {
317
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
318
+ console.log(` 📝 Visual Note: ${note.id} [${note.status}]`);
319
+ console.log(` 📍 Route: ${note.route} (${note.url})`);
320
+ console.log(` 📐 Viewport: ${note.viewport.width} × ${note.viewport.height} (dpr: ${note.viewport.devicePixelRatio})`);
321
+ if (note.target) {
322
+ console.log(` 🎯 Target: ${note.target.selector}`);
323
+ console.log(` Bounds: x:${note.target.boundingRect.x}, y:${note.target.boundingRect.y}, ${note.target.boundingRect.width}×${note.target.boundingRect.height}`);
324
+ }
325
+ console.log(` 💬 Note: "${note.message}"`);
326
+ if (note.screenshots?.original) {
327
+ console.log(` 🖼️ Screenshot: ${note.screenshots.original}`);
328
+ }
329
+ console.log(` 🕒 Created: ${note.createdAt}`);
330
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
331
+ }
332
+ } finally {
333
+ db.close();
278
334
  }
279
- console.log(` 🕒 Created: ${note.createdAt}`);
280
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
335
+ } catch (err: any) {
336
+ console.error('[BrowserTrack] Failed to retrieve note:', err?.message || err);
281
337
  }
282
- db.close();
283
338
  });
284
339
 
285
340
  noteCmd
286
341
  .command('resolve <noteId>')
287
342
  .description('Mark a visual note as resolved')
288
343
  .action((noteId) => {
289
- const config = getDaemonConfig();
290
- const db = new StorageDB(config.dbPath);
291
- const note = db.getNote(noteId);
292
-
293
- if (!note) {
294
- console.log(`[BrowserTrack] Note '${noteId}' not found.`);
295
- } else {
296
- db.updateNoteStatus(noteId, 'RESOLVED');
297
- console.log(`[BrowserTrack] Marked note '${noteId}' as RESOLVED.`);
344
+ try {
345
+ const config = getDaemonConfig();
346
+ const db = new StorageDB(config.dbPath);
347
+ try {
348
+ const note = db.getNote(noteId);
349
+ if (!note) {
350
+ console.log(`[BrowserTrack] Note '${noteId}' not found.`);
351
+ } else {
352
+ db.updateNoteStatus(noteId, 'RESOLVED');
353
+ console.log(`[BrowserTrack] Marked note '${noteId}' as RESOLVED.`);
354
+ }
355
+ } finally {
356
+ db.close();
357
+ }
358
+ } catch (err: any) {
359
+ console.error('[BrowserTrack] Failed to resolve note:', err?.message || err);
298
360
  }
299
- db.close();
300
361
  });
301
362
 
302
363
  noteCmd
303
364
  .command('reopen <noteId>')
304
365
  .description('Reopen a visual note')
305
366
  .action((noteId) => {
306
- const config = getDaemonConfig();
307
- const db = new StorageDB(config.dbPath);
308
- const note = db.getNote(noteId);
309
-
310
- if (!note) {
311
- console.log(`[BrowserTrack] Note '${noteId}' not found.`);
312
- } else {
313
- db.updateNoteStatus(noteId, 'OPEN');
314
- console.log(`[BrowserTrack] Reopened note '${noteId}' (status: OPEN).`);
367
+ try {
368
+ const config = getDaemonConfig();
369
+ const db = new StorageDB(config.dbPath);
370
+ try {
371
+ const note = db.getNote(noteId);
372
+ if (!note) {
373
+ console.log(`[BrowserTrack] Note '${noteId}' not found.`);
374
+ } else {
375
+ db.updateNoteStatus(noteId, 'OPEN');
376
+ console.log(`[BrowserTrack] Reopened note '${noteId}' (status: OPEN).`);
377
+ }
378
+ } finally {
379
+ db.close();
380
+ }
381
+ } catch (err: any) {
382
+ console.error('[BrowserTrack] Failed to reopen note:', err?.message || err);
315
383
  }
316
- db.close();
317
384
  });
318
385
 
319
386
  program
@@ -323,27 +390,34 @@ program
323
390
  .option('-s, --status <status>', 'Filter by status (OPEN, RESOLVED, etc.)')
324
391
  .option('-l, --limit <number>', 'Limit result count', '20')
325
392
  .action((options) => {
326
- const config = getDaemonConfig();
327
- const db = new StorageDB(config.dbPath);
328
- const limit = parseInt(options.limit, 10);
329
- const notes = db.listNotes({
330
- projectId: options.project,
331
- status: options.status,
332
- limit,
333
- });
334
-
335
- if (notes.length === 0) {
336
- console.log('[BrowserTrack] No visual notes found.');
337
- } else {
338
- console.log(`\nVisual Notes (${notes.length}):`);
339
- for (const n of notes) {
340
- const badge = n.status === 'OPEN' ? '🟡 OPEN' : n.status === 'RESOLVED' ? '🟢 RESOLVED' : `⚪ ${n.status}`;
341
- console.log(` ${n.id.padEnd(12)} [${badge}] Route: ${n.route.padEnd(16)} | Target: ${n.target?.selector || n.type}`);
342
- console.log(` Note: "${n.message}" (${n.viewport.width}×${n.viewport.height})`);
393
+ try {
394
+ const config = getDaemonConfig();
395
+ const db = new StorageDB(config.dbPath);
396
+ try {
397
+ const limit = parseInt(options.limit, 10);
398
+ const notes = db.listNotes({
399
+ projectId: options.project,
400
+ status: options.status,
401
+ limit,
402
+ });
403
+
404
+ if (notes.length === 0) {
405
+ console.log('[BrowserTrack] No visual notes found.');
406
+ } else {
407
+ console.log(`\nVisual Notes (${notes.length}):`);
408
+ for (const n of notes) {
409
+ const badge = n.status === 'OPEN' ? '🟡 OPEN' : n.status === 'RESOLVED' ? '🟢 RESOLVED' : `⚪ ${n.status}`;
410
+ console.log(` ${n.id.padEnd(12)} [${badge}] Route: ${n.route.padEnd(16)} | Target: ${n.target?.selector || n.type}`);
411
+ console.log(` Note: "${n.message}" (${n.viewport.width}×${n.viewport.height})`);
412
+ }
413
+ console.log('');
414
+ }
415
+ } finally {
416
+ db.close();
343
417
  }
344
- console.log('');
418
+ } catch (err: any) {
419
+ console.error('[BrowserTrack] Failed to list visual notes:', err?.message || err);
345
420
  }
346
- db.close();
347
421
  });
348
422
 
349
423
  // 7. INBOX (Errors + Visual Notes)
@@ -352,36 +426,43 @@ program
352
426
  .description('View combined developer inbox with active runtime errors and visual notes')
353
427
  .option('-p, --project <project>', 'Filter by project name or ID')
354
428
  .action((options) => {
355
- const config = getDaemonConfig();
356
- const db = new StorageDB(config.dbPath);
357
- const incidents = db.listIncidents({ projectId: options.project, status: 'OPEN', limit: 20 });
358
- const notes = db.listNotes({ projectId: options.project, status: 'OPEN', limit: 20 });
359
-
360
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
361
- console.log(` 📥 Browser Development Inbox ${options.project ? `(${options.project})` : ''}`);
362
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
363
-
364
- if (incidents.length === 0 && notes.length === 0) {
365
- console.log(' ✨ All clear! No open errors or visual notes.');
366
- } else {
367
- if (incidents.length > 0) {
368
- console.log(`\n 🚨 Runtime Errors (${incidents.length}):`);
369
- for (const inc of incidents) {
370
- console.log(` • ${inc.id} (${inc.occurrences}x) ${inc.type}: ${inc.message}`);
371
- console.log(` Route: ${inc.route} | Source: ${inc.source.file}:${inc.source.line}`);
372
- }
373
- }
374
-
375
- if (notes.length > 0) {
376
- console.log(`\n 📝 Visual Notes (${notes.length}):`);
377
- for (const n of notes) {
378
- console.log(` • ${n.id} on ${n.route} (${n.viewport.width}×${n.viewport.height})`);
379
- console.log(` Target: ${n.target?.selector || n.type} | Note: "${n.message}"`);
429
+ try {
430
+ const config = getDaemonConfig();
431
+ const db = new StorageDB(config.dbPath);
432
+ try {
433
+ const incidents = db.listIncidents({ projectId: options.project, status: 'OPEN', limit: 20 });
434
+ const notes = db.listNotes({ projectId: options.project, status: 'OPEN', limit: 20 });
435
+
436
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
437
+ console.log(` 📥 Browser Development Inbox ${options.project ? `(${options.project})` : ''}`);
438
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
439
+
440
+ if (incidents.length === 0 && notes.length === 0) {
441
+ console.log(' ✨ All clear! No open errors or visual notes.');
442
+ } else {
443
+ if (incidents.length > 0) {
444
+ console.log(`\n 🚨 Runtime Errors (${incidents.length}):`);
445
+ for (const inc of incidents) {
446
+ console.log(` • ${inc.id} (${inc.occurrences}x) ${inc.type}: ${inc.message}`);
447
+ console.log(` Route: ${inc.route} | Source: ${inc.source.file}:${inc.source.line}`);
448
+ }
449
+ }
450
+
451
+ if (notes.length > 0) {
452
+ console.log(`\n 📝 Visual Notes (${notes.length}):`);
453
+ for (const n of notes) {
454
+ console.log(` • ${n.id} on ${n.route} (${n.viewport.width}×${n.viewport.height})`);
455
+ console.log(` Target: ${n.target?.selector || n.type} | Note: "${n.message}"`);
456
+ }
457
+ }
380
458
  }
459
+ console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
460
+ } finally {
461
+ db.close();
381
462
  }
463
+ } catch (err: any) {
464
+ console.error('[BrowserTrack] Failed to display inbox:', err?.message || err);
382
465
  }
383
- console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
384
- db.close();
385
466
  });
386
467
 
387
468
  // 6. CLEAR
@@ -389,20 +470,35 @@ program
389
470
  .command('clear')
390
471
  .description('Clear all stored incidents, events, and sessions from the database')
391
472
  .action(() => {
392
- const config = getDaemonConfig();
393
- const db = new StorageDB(config.dbPath);
394
- db.clearAll();
395
- console.log('[BrowserTrack] Database cleared.');
396
- db.close();
473
+ try {
474
+ const config = getDaemonConfig();
475
+ const db = new StorageDB(config.dbPath);
476
+ try {
477
+ db.clearAll();
478
+ console.log('[BrowserTrack] Database cleared.');
479
+ } finally {
480
+ db.close();
481
+ }
482
+ } catch (err: any) {
483
+ console.error('[BrowserTrack] Failed to clear database:', err?.message || err);
484
+ }
397
485
  });
398
486
 
399
487
  // 7. MCP SERVER
400
488
  program
401
489
  .command('mcp')
402
490
  .description('Launch the Model Context Protocol (MCP) server over stdio')
403
- .action(async () => {
491
+ .option('--no-daemon', 'Do not auto-start background daemon if offline')
492
+ .action(async (options) => {
404
493
  try {
405
- const server = createMcpServer();
494
+ // In MCP stdio mode, redirect console.log to console.error to preserve JSON-RPC protocol on stdout
495
+ console.log = (...args: any[]) => {
496
+ console.error(...args);
497
+ };
498
+
499
+ const server = createMcpServer({
500
+ autoStartDaemon: options.daemon !== false,
501
+ });
406
502
  await server.startStdio();
407
503
  } catch (err: any) {
408
504
  console.error('[BrowserTrack] MCP Server error:', err?.message);
@@ -9,14 +9,18 @@ export type NavigationCallback = (event: NavigationEvent) => void;
9
9
  export function setupNavigationInterceptors(onNavigation: NavigationCallback): () => void {
10
10
  if (typeof window === 'undefined' || typeof history === 'undefined') return () => {};
11
11
 
12
- let currentUrl = redactUrl(window.location.href);
13
-
14
- // Initial navigation
15
- onNavigation({
16
- to: currentUrl,
17
- type: 'initial',
18
- timestamp: Date.now(),
19
- });
12
+ let currentUrl = '';
13
+ try {
14
+ currentUrl = redactUrl(window.location.href);
15
+ // Initial navigation
16
+ onNavigation({
17
+ to: currentUrl,
18
+ type: 'initial',
19
+ timestamp: Date.now(),
20
+ });
21
+ } catch {
22
+ // Defensive
23
+ }
20
24
 
21
25
  const originalPushState = history.pushState;
22
26
  const originalReplaceState = history.replaceState;
@@ -58,27 +62,35 @@ export function setupNavigationInterceptors(onNavigation: NavigationCallback): (
58
62
  };
59
63
 
60
64
  const onPopState = () => {
61
- const from = currentUrl;
62
- const to = redactUrl(window.location.href);
63
- currentUrl = to;
64
- onNavigation({
65
- from,
66
- to,
67
- type: 'popstate',
68
- timestamp: Date.now(),
69
- });
65
+ try {
66
+ const from = currentUrl;
67
+ const to = redactUrl(window.location.href);
68
+ currentUrl = to;
69
+ onNavigation({
70
+ from,
71
+ to,
72
+ type: 'popstate',
73
+ timestamp: Date.now(),
74
+ });
75
+ } catch {
76
+ // Defensive
77
+ }
70
78
  };
71
79
 
72
80
  const onHashChange = () => {
73
- const from = currentUrl;
74
- const to = redactUrl(window.location.href);
75
- currentUrl = to;
76
- onNavigation({
77
- from,
78
- to,
79
- type: 'hashchange',
80
- timestamp: Date.now(),
81
- });
81
+ try {
82
+ const from = currentUrl;
83
+ const to = redactUrl(window.location.href);
84
+ currentUrl = to;
85
+ onNavigation({
86
+ from,
87
+ to,
88
+ type: 'hashchange',
89
+ timestamp: Date.now(),
90
+ });
91
+ } catch {
92
+ // Defensive
93
+ }
82
94
  };
83
95
 
84
96
  window.addEventListener('popstate', onPopState);
@@ -37,35 +37,40 @@ export function setupNetworkInterceptors(onNetwork: NetworkCallback): () => void
37
37
 
38
38
  const safeUrl = redactUrl(urlStr);
39
39
 
40
+ let response: Response;
40
41
  try {
41
- const response = await originalFetch.apply(window, [input as any, init]);
42
- const durationMs = Date.now() - startTime;
43
-
44
- onNetwork({
45
- url: safeUrl,
46
- method,
47
- status: response.status,
48
- statusText: response.statusText,
49
- durationMs,
50
- timestamp: startTime,
51
- });
52
-
53
- return response;
42
+ response = await originalFetch.apply(window, [input as any, init]);
54
43
  } catch (err: any) {
55
44
  const durationMs = Date.now() - startTime;
56
45
  const isAbort = err?.name === 'AbortError';
57
46
 
47
+ try {
48
+ onNetwork({
49
+ url: safeUrl,
50
+ method,
51
+ durationMs,
52
+ error: err?.message || 'Network request failed',
53
+ aborted: isAbort,
54
+ timestamp: startTime,
55
+ });
56
+ } catch {}
57
+
58
+ throw err;
59
+ }
60
+
61
+ const durationMs = Date.now() - startTime;
62
+ try {
58
63
  onNetwork({
59
64
  url: safeUrl,
60
65
  method,
66
+ status: response.status,
67
+ statusText: response.statusText,
61
68
  durationMs,
62
- error: err?.message || 'Network request failed',
63
- aborted: isAbort,
64
69
  timestamp: startTime,
65
70
  });
71
+ } catch {}
66
72
 
67
- throw err;
68
- }
73
+ return response;
69
74
  };
70
75
 
71
76
  cleanups.push(() => {