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,11 +1,23 @@
1
1
  import crypto from 'node:crypto';
2
2
  import type { WebSocket, WebSocketServer } from 'ws';
3
3
  import type { ClientEventMessage, CommandResponse, HelloMessage } from '../../../core/src/index.js';
4
+ import { safeJsonStringify } from '../../../core/src/index.js';
4
5
  import type { IncidentEngine } from '../incidents/engine.js';
5
6
  import type { NotesEngine } from '../notes/engine.js';
6
7
  import type { SessionManager } from '../session/manager.js';
7
8
  import type { StorageDB } from '../storage/db.js';
8
9
 
10
+ function safeWsSend(ws: WebSocket, payload: any): boolean {
11
+ if (ws.readyState !== 1 /* WebSocket.OPEN */) return false;
12
+ try {
13
+ const raw = typeof payload === 'string' ? payload : safeJsonStringify(payload);
14
+ ws.send(raw);
15
+ return true;
16
+ } catch {
17
+ return false;
18
+ }
19
+ }
20
+
9
21
  export function setupWebSocketServer(
10
22
  wss: WebSocketServer,
11
23
  db: StorageDB,
@@ -65,23 +77,19 @@ export function setupWebSocketServer(
65
77
 
66
78
  sessionManager.registerSocket(sessionId, ws, hello.origin || '', project.id);
67
79
 
68
- ws.send(
69
- JSON.stringify({
70
- type: 'hello_ack',
71
- sessionId,
72
- projectId: project.id,
73
- projectName: project.name,
74
- })
75
- );
80
+ safeWsSend(ws, {
81
+ type: 'hello_ack',
82
+ sessionId,
83
+ projectId: project.id,
84
+ projectName: project.name,
85
+ });
76
86
 
77
87
  // Sync existing notes for this project on load
78
88
  const existingNotes = db.listNotes({ projectId: project.id, limit: 100 });
79
- ws.send(
80
- JSON.stringify({
81
- type: 'notes_sync',
82
- notes: existingNotes,
83
- })
84
- );
89
+ safeWsSend(ws, {
90
+ type: 'notes_sync',
91
+ notes: existingNotes,
92
+ });
85
93
 
86
94
  if (verbose) {
87
95
  console.log(`[BrowserTrack] New session connected: ${sessionId} (${project.name} @ ${hello.origin})`);
@@ -147,15 +155,13 @@ export function setupWebSocketServer(
147
155
  );
148
156
  }
149
157
 
150
- ws.send(
151
- JSON.stringify({
152
- type: 'note_created_ack',
153
- noteId: note.id,
154
- status: note.status,
155
- scenarioId: note.scenarioId,
156
- stepNumber: note.stepNumber,
157
- })
158
- );
158
+ safeWsSend(ws, {
159
+ type: 'note_created_ack',
160
+ noteId: note.id,
161
+ status: note.status,
162
+ scenarioId: note.scenarioId,
163
+ stepNumber: note.stepNumber,
164
+ });
159
165
 
160
166
  // Broadcast updated notes to all active browser tabs in this project
161
167
  const allNotes = db.listNotes({ projectId: note.projectId, limit: 100 });
@@ -224,12 +230,10 @@ export function setupWebSocketServer(
224
230
  const projectId = data.projectId || session?.projectId;
225
231
  if (projectId) {
226
232
  const allNotes = db.listNotes({ projectId, limit: 100 });
227
- ws.send(
228
- JSON.stringify({
229
- type: 'notes_sync',
230
- notes: allNotes,
231
- })
232
- );
233
+ safeWsSend(ws, {
234
+ type: 'notes_sync',
235
+ notes: allNotes,
236
+ });
233
237
  }
234
238
  return;
235
239
  }
@@ -18,6 +18,7 @@ import type {
18
18
  ScenarioOverview,
19
19
  ScenarioDetail,
20
20
  } from '../../../core/src/index.js';
21
+ import { safeJsonParse, safeJsonStringify } from '../../../core/src/index.js';
21
22
 
22
23
  export class StorageDB {
23
24
  private db: Database.Database;
@@ -29,6 +30,7 @@ export class StorageDB {
29
30
  this.db = new Database(dbPath);
30
31
  this.db.pragma('journal_mode = WAL');
31
32
  this.db.pragma('synchronous = NORMAL');
33
+ this.db.pragma('busy_timeout = 5000');
32
34
  this.initTables();
33
35
  }
34
36
 
@@ -161,18 +163,38 @@ export class StorageDB {
161
163
  CREATE INDEX IF NOT EXISTS idx_incidents_project ON incidents(project_id, status);
162
164
  CREATE INDEX IF NOT EXISTS idx_incidents_fp ON incidents(fingerprint);
163
165
  CREATE INDEX IF NOT EXISTS idx_notes_project ON notes(project_id, status);
164
- CREATE INDEX IF NOT EXISTS idx_notes_scenario ON notes(scenario_id, step_number);
165
166
  `);
166
167
 
167
- // Schema migrations for scenario fields
168
+ // Schema migrations for scenario fields on existing databases
168
169
  try {
169
- this.db.exec('ALTER TABLE notes ADD COLUMN scenario_id TEXT;');
170
- } catch {}
171
- try {
172
- this.db.exec('ALTER TABLE notes ADD COLUMN step_number INTEGER;');
173
- } catch {}
170
+ const columns = (this.db.prepare('PRAGMA table_info(notes)').all() as Array<{ name: string }>).map(
171
+ (c) => c.name
172
+ );
173
+ if (!columns.includes('scenario_id')) {
174
+ this.db.exec('ALTER TABLE notes ADD COLUMN scenario_id TEXT;');
175
+ }
176
+ if (!columns.includes('step_number')) {
177
+ this.db.exec('ALTER TABLE notes ADD COLUMN step_number INTEGER;');
178
+ }
179
+ if (!columns.includes('scenario_title')) {
180
+ this.db.exec('ALTER TABLE notes ADD COLUMN scenario_title TEXT;');
181
+ }
182
+ } catch {
183
+ // Fallback try-catch
184
+ try {
185
+ this.db.exec('ALTER TABLE notes ADD COLUMN scenario_id TEXT;');
186
+ } catch {}
187
+ try {
188
+ this.db.exec('ALTER TABLE notes ADD COLUMN step_number INTEGER;');
189
+ } catch {}
190
+ try {
191
+ this.db.exec('ALTER TABLE notes ADD COLUMN scenario_title TEXT;');
192
+ } catch {}
193
+ }
194
+
195
+ // Create scenario index after columns are guaranteed to exist
174
196
  try {
175
- this.db.exec('ALTER TABLE notes ADD COLUMN scenario_title TEXT;');
197
+ this.db.exec('CREATE INDEX IF NOT EXISTS idx_notes_scenario ON notes(scenario_id, step_number);');
176
198
  } catch {}
177
199
  }
178
200
 
@@ -375,7 +397,7 @@ export class StorageDB {
375
397
  id: r.id,
376
398
  sessionId: r.session_id,
377
399
  eventType: r.event_type,
378
- payload: JSON.parse(r.payload),
400
+ payload: safeJsonParse(r.payload, {}),
379
401
  timestamp: r.timestamp,
380
402
  route: r.route,
381
403
  url: r.url,
@@ -445,9 +467,9 @@ export class StorageDB {
445
467
  incident.occurrences,
446
468
  incident.status,
447
469
  incident.stack || null,
448
- JSON.stringify(incident.breadcrumbs || []),
449
- JSON.stringify(incident.networkFailures || []),
450
- incident.lastElement ? JSON.stringify(incident.lastElement) : null,
470
+ safeJsonStringify(incident.breadcrumbs || []),
471
+ safeJsonStringify(incident.networkFailures || []),
472
+ incident.lastElement ? safeJsonStringify(incident.lastElement) : null,
451
473
  incident.screenshots?.error || null
452
474
  );
453
475
  }
@@ -475,8 +497,8 @@ export class StorageDB {
475
497
  update.lastSeen,
476
498
  update.occurrences,
477
499
  update.route,
478
- JSON.stringify(update.breadcrumbs || []),
479
- update.lastElement ? JSON.stringify(update.lastElement) : null,
500
+ safeJsonStringify(update.breadcrumbs || []),
501
+ update.lastElement ? safeJsonStringify(update.lastElement) : null,
480
502
  update.stack || null,
481
503
  incidentId
482
504
  );
@@ -500,8 +522,8 @@ export class StorageDB {
500
522
  occurrence.route,
501
523
  occurrence.url,
502
524
  occurrence.stack || null,
503
- JSON.stringify(occurrence.breadcrumbs || []),
504
- occurrence.lastElement ? JSON.stringify(occurrence.lastElement) : null
525
+ safeJsonStringify(occurrence.breadcrumbs || []),
526
+ occurrence.lastElement ? safeJsonStringify(occurrence.lastElement) : null
505
527
  );
506
528
  }
507
529
 
@@ -525,7 +547,7 @@ export class StorageDB {
525
547
  v.id,
526
548
  v.incidentId,
527
549
  v.status,
528
- JSON.stringify(v.checks),
550
+ safeJsonStringify(v.checks),
529
551
  v.beforeScreenshot || null,
530
552
  v.afterScreenshot || null,
531
553
  v.message || null,
@@ -539,7 +561,7 @@ export class StorageDB {
539
561
  return {
540
562
  incidentId: row.incident_id,
541
563
  status: row.status as IncidentStatus,
542
- checks: JSON.parse(row.checks || '[]'),
564
+ checks: safeJsonParse(row.checks, []),
543
565
  screenshots: {
544
566
  before: row.before_screenshot || undefined,
545
567
  after: row.after_screenshot || undefined,
@@ -567,11 +589,11 @@ export class StorageDB {
567
589
  note.message,
568
590
  note.route,
569
591
  note.url,
570
- JSON.stringify(note.viewport),
571
- JSON.stringify(note.scroll),
572
- note.target ? JSON.stringify(note.target) : null,
573
- note.elementContext ? JSON.stringify(note.elementContext) : null,
574
- note.region ? JSON.stringify(note.region) : null,
592
+ safeJsonStringify(note.viewport),
593
+ safeJsonStringify(note.scroll),
594
+ note.target ? safeJsonStringify(note.target) : null,
595
+ note.elementContext ? safeJsonStringify(note.elementContext) : null,
596
+ note.region ? safeJsonStringify(note.region) : null,
575
597
  note.screenshots?.original || null,
576
598
  note.incidentId || null,
577
599
  note.scenarioId || null,
@@ -739,8 +761,8 @@ export class StorageDB {
739
761
  `nver_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
740
762
  v.noteId,
741
763
  v.status,
742
- JSON.stringify(v.checks),
743
- v.geometryDiff ? JSON.stringify(v.geometryDiff) : null,
764
+ safeJsonStringify(v.checks),
765
+ v.geometryDiff ? safeJsonStringify(v.geometryDiff) : null,
744
766
  v.screenshots?.before || null,
745
767
  v.screenshots?.after || null,
746
768
  v.message || null,
@@ -754,8 +776,8 @@ export class StorageDB {
754
776
  return {
755
777
  noteId: row.note_id,
756
778
  status: row.status as NoteStatus,
757
- checks: JSON.parse(row.checks || '[]'),
758
- geometryDiff: row.geometry_diff ? JSON.parse(row.geometry_diff) : undefined,
779
+ checks: safeJsonParse(row.checks, []),
780
+ geometryDiff: safeJsonParse(row.geometry_diff, undefined),
759
781
  screenshots: {
760
782
  before: row.before_screenshot || undefined,
761
783
  after: row.after_screenshot || undefined,
@@ -798,9 +820,9 @@ export class StorageDB {
798
820
  occurrences: row.occurrences,
799
821
  status: row.status as IncidentStatus,
800
822
  stack: row.stack || undefined,
801
- breadcrumbs: JSON.parse(row.breadcrumbs || '[]'),
802
- networkFailures: JSON.parse(row.network_failures || '[]'),
803
- lastElement: row.last_element ? JSON.parse(row.last_element) : undefined,
823
+ breadcrumbs: safeJsonParse(row.breadcrumbs, []),
824
+ networkFailures: safeJsonParse(row.network_failures, []),
825
+ lastElement: safeJsonParse(row.last_element, undefined),
804
826
  screenshots: row.screenshot_path
805
827
  ? {
806
828
  error: row.screenshot_path,
@@ -818,11 +840,11 @@ export class StorageDB {
818
840
  message: row.message,
819
841
  route: row.route,
820
842
  url: row.url,
821
- viewport: JSON.parse(row.viewport_json || '{}'),
822
- scroll: JSON.parse(row.scroll_json || '{}'),
823
- target: row.target_json ? JSON.parse(row.target_json) : undefined,
824
- elementContext: row.element_context_json ? JSON.parse(row.element_context_json) : undefined,
825
- region: row.region_json ? JSON.parse(row.region_json) : undefined,
843
+ viewport: safeJsonParse(row.viewport_json, { width: 0, height: 0, devicePixelRatio: 1 }),
844
+ scroll: safeJsonParse(row.scroll_json, { scrollX: 0, scrollY: 0 }),
845
+ target: safeJsonParse(row.target_json, undefined),
846
+ elementContext: safeJsonParse(row.element_context_json, undefined),
847
+ region: safeJsonParse(row.region_json, undefined),
826
848
  status: row.status as NoteStatus,
827
849
  incidentId: row.incident_id || undefined,
828
850
  scenarioId: row.scenario_id || undefined,
@@ -11,6 +11,113 @@ export interface McpContext {
11
11
  daemonUrl?: string;
12
12
  }
13
13
 
14
+ async function sendSessionCommand(ctx: McpContext, sessionId: string | undefined, command: any, timeoutMs = 5000): Promise<any> {
15
+ // 1. In-process session manager (if active sockets exist)
16
+ if (ctx.sessionManager && ctx.sessionManager.getActiveCount() > 0) {
17
+ const targetSession = sessionId ? ctx.db.getSession(sessionId) : ctx.sessionManager.getAnyActiveSession();
18
+ if (targetSession) {
19
+ const res = await ctx.sessionManager.sendCommand(targetSession.id, command, timeoutMs);
20
+ if (!res.ok) {
21
+ throw new Error(res.error || res.reason || `Command ${command.type} failed`);
22
+ }
23
+ return res;
24
+ }
25
+ }
26
+
27
+ // 2. Multi-process proxy to running daemon HTTP server
28
+ if (ctx.daemonUrl) {
29
+ try {
30
+ const resp = await fetch(`${ctx.daemonUrl}/api/command`, {
31
+ method: 'POST',
32
+ headers: { 'Content-Type': 'application/json' },
33
+ body: JSON.stringify({ sessionId, command, timeoutMs }),
34
+ signal: AbortSignal.timeout(timeoutMs + 2000),
35
+ });
36
+ if (resp.ok) {
37
+ const res = await resp.json();
38
+ if (!res.ok) {
39
+ throw new Error(res.error || res.reason || `Command ${command.type} failed`);
40
+ }
41
+ return res;
42
+ }
43
+ const errJson = await resp.json().catch(() => ({}));
44
+ if (errJson.error) {
45
+ throw new Error(errJson.error);
46
+ }
47
+ } catch (err: any) {
48
+ if (err.message && !err.message.includes('fetch failed') && !err.message.includes('ECONNREFUSED')) {
49
+ throw err;
50
+ }
51
+ }
52
+ }
53
+
54
+ throw new Error(
55
+ 'No active browser session connected. Please ensure the BrowserTrack daemon is running ("browsertrack start") and your application tab is open in the browser.'
56
+ );
57
+ }
58
+
59
+ async function runVerifyIncident(ctx: McpContext, incidentId: string, options: any): Promise<any> {
60
+ if (ctx.verificationEngine && ctx.sessionManager && ctx.sessionManager.getActiveCount() > 0) {
61
+ return await ctx.verificationEngine.verifyIncident(incidentId, options);
62
+ }
63
+
64
+ if (ctx.daemonUrl) {
65
+ try {
66
+ const timeoutMs = (options?.observationWindowMs || 3000) + 7000;
67
+ const resp = await fetch(`${ctx.daemonUrl}/api/verify/incident`, {
68
+ method: 'POST',
69
+ headers: { 'Content-Type': 'application/json' },
70
+ body: JSON.stringify({ incidentId, options }),
71
+ signal: AbortSignal.timeout(timeoutMs),
72
+ });
73
+ if (resp.ok) {
74
+ const data = await resp.json();
75
+ if (data.ok) return data.result;
76
+ throw new Error(data.error);
77
+ }
78
+ } catch (err: any) {
79
+ if (err.message && !err.message.includes('fetch failed') && !err.message.includes('ECONNREFUSED')) {
80
+ throw err;
81
+ }
82
+ }
83
+ }
84
+
85
+ throw new Error(
86
+ 'Verification failed: No active browser session connected. Please ensure the BrowserTrack daemon is running ("browsertrack start") and your application tab is open in the browser.'
87
+ );
88
+ }
89
+
90
+ async function runVerifyNote(ctx: McpContext, noteId: string, options: any): Promise<any> {
91
+ if (ctx.noteVerificationEngine && ctx.sessionManager && ctx.sessionManager.getActiveCount() > 0) {
92
+ return await ctx.noteVerificationEngine.verifyNote(noteId, options);
93
+ }
94
+
95
+ if (ctx.daemonUrl) {
96
+ try {
97
+ const timeoutMs = (options?.observationWindowMs || 3000) + 7000;
98
+ const resp = await fetch(`${ctx.daemonUrl}/api/verify/note`, {
99
+ method: 'POST',
100
+ headers: { 'Content-Type': 'application/json' },
101
+ body: JSON.stringify({ noteId, options }),
102
+ signal: AbortSignal.timeout(timeoutMs),
103
+ });
104
+ if (resp.ok) {
105
+ const data = await resp.json();
106
+ if (data.ok) return data.result;
107
+ throw new Error(data.error);
108
+ }
109
+ } catch (err: any) {
110
+ if (err.message && !err.message.includes('fetch failed') && !err.message.includes('ECONNREFUSED')) {
111
+ throw err;
112
+ }
113
+ }
114
+ }
115
+
116
+ throw new Error(
117
+ 'Verification failed: No active browser session connected. Please ensure the BrowserTrack daemon is running ("browsertrack start") and your application tab is open in the browser.'
118
+ );
119
+ }
120
+
14
121
  export async function handleToolCall(name: string, args: any, ctx: McpContext): Promise<any> {
15
122
  const { db, sessionManager, verificationEngine, noteVerificationEngine } = ctx;
16
123
 
@@ -104,6 +211,7 @@ export async function handleToolCall(name: string, args: any, ctx: McpContext):
104
211
  visible: incident.lastElement.visible,
105
212
  innerText: incident.lastElement.innerText,
106
213
  outerHTML: incident.lastElement.outerHTML,
214
+ componentSource: incident.lastElement.componentSource,
107
215
  }
108
216
  : undefined,
109
217
  recentBreadcrumbs: breadcrumbsTimeline,
@@ -180,43 +288,20 @@ export async function handleToolCall(name: string, args: any, ctx: McpContext):
180
288
  }
181
289
 
182
290
  case 'get_page_state': {
183
- if (!sessionManager) {
184
- throw new Error('Live browser connection not available: Daemon session manager not attached.');
185
- }
186
- let session = args.sessionId ? db.getSession(args.sessionId) : sessionManager.getAnyActiveSession();
187
- if (!session) {
188
- throw new Error('No active browser session connected.');
189
- }
190
-
191
- const cmdRes = await sessionManager.sendCommand(session.id, {
291
+ const cmdRes = await sendSessionCommand(ctx, args.sessionId, {
192
292
  id: `cmd_mcp_${Date.now()}`,
193
293
  type: 'get_page_state',
194
294
  });
195
-
196
- if (!cmdRes.ok) {
197
- throw new Error(cmdRes.error || 'Failed to retrieve page state from browser.');
198
- }
199
295
  return cmdRes.result;
200
296
  }
201
297
 
202
298
  case 'capture_element': {
203
- if (!sessionManager) {
204
- throw new Error('Live browser connection not available: Daemon session manager not attached.');
205
- }
206
- let session = args.sessionId ? db.getSession(args.sessionId) : sessionManager.getAnyActiveSession();
207
- if (!session) {
208
- throw new Error('No active browser session connected.');
209
- }
210
-
211
- const cmdRes = await sessionManager.sendCommand(session.id, {
299
+ const cmdRes = await sendSessionCommand(ctx, args.sessionId, {
212
300
  id: `cmd_mcp_${Date.now()}`,
213
301
  type: 'capture_element',
214
302
  params: { selector: args.selector },
215
303
  });
216
304
 
217
- if (!cmdRes.ok) {
218
- throw new Error(cmdRes.error || cmdRes.reason || 'Failed to capture element screenshot.');
219
- }
220
305
  return {
221
306
  ok: true,
222
307
  format: cmdRes.result?.format || 'webp',
@@ -227,11 +312,7 @@ export async function handleToolCall(name: string, args: any, ctx: McpContext):
227
312
  }
228
313
 
229
314
  case 'verify_incident': {
230
- if (!verificationEngine) {
231
- throw new Error('Verification engine not available: Daemon session manager not attached.');
232
- }
233
-
234
- const res = await verificationEngine.verifyIncident(args.incidentId, {
315
+ const res = await runVerifyIncident(ctx, args.incidentId, {
235
316
  route: args.route,
236
317
  targetSelector: args.targetSelector,
237
318
  expect: args.expect,
@@ -398,10 +479,7 @@ export async function handleToolCall(name: string, args: any, ctx: McpContext):
398
479
  }
399
480
 
400
481
  case 'verify_note': {
401
- if (!noteVerificationEngine) {
402
- throw new Error('Note verification engine not available: Daemon session manager not attached.');
403
- }
404
- const res = await noteVerificationEngine.verifyNote(args.noteId, {
482
+ const res = await runVerifyNote(ctx, args.noteId, {
405
483
  observationWindowMs: args.observationWindowMs,
406
484
  });
407
485
  return res;
@@ -416,27 +494,19 @@ export async function handleToolCall(name: string, args: any, ctx: McpContext):
416
494
  }
417
495
 
418
496
  case 'capture_note_context': {
419
- if (!sessionManager) {
420
- throw new Error('Live browser connection not available: Daemon session manager not attached.');
421
- }
422
- let session = args.sessionId ? db.getSession(args.sessionId) : sessionManager.getAnyActiveSession();
423
- if (!session) {
424
- throw new Error('No active browser session connected.');
425
- }
426
-
427
- const queryCmd = await sessionManager.sendCommand(session.id, {
497
+ const queryCmd = await sendSessionCommand(ctx, args.sessionId, {
428
498
  id: `cmd_ctx_${Date.now()}`,
429
499
  type: 'query_element',
430
500
  params: { selector: args.selector },
431
501
  });
432
502
 
433
- const overflowCmd = await sessionManager.sendCommand(session.id, {
503
+ const overflowCmd = await sendSessionCommand(ctx, args.sessionId, {
434
504
  id: `cmd_ovf_${Date.now()}`,
435
505
  type: 'check_overflow',
436
506
  params: { selector: args.selector },
437
507
  });
438
508
 
439
- const styleCmd = await sessionManager.sendCommand(session.id, {
509
+ const styleCmd = await sendSessionCommand(ctx, args.sessionId, {
440
510
  id: `cmd_sty_${Date.now()}`,
441
511
  type: 'get_element_style',
442
512
  params: { selector: args.selector },