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.
- package/AGENTS.md +11 -6
- package/README.md +2 -0
- package/dist/{chunk-WB7ZKWK7.js → chunk-4HRLW6YF.js} +509 -109
- package/dist/chunk-4HRLW6YF.js.map +1 -0
- package/dist/{chunk-3HOXPTM2.js → chunk-AYSVE6NG.js} +808 -53
- package/dist/chunk-AYSVE6NG.js.map +1 -0
- package/dist/{chunk-6VA7GBAO.js → chunk-QRZ57ME3.js} +70 -2
- package/dist/chunk-QRZ57ME3.js.map +1 -0
- package/dist/{chunk-UP5JKCFY.js → chunk-TWEYRBDU.js} +281 -44
- package/dist/chunk-TWEYRBDU.js.map +1 -0
- package/dist/cli/index.js +1040 -546
- package/dist/cli/index.js.map +1 -1
- package/dist/client/index.cjs +536 -109
- package/dist/client/index.d.ts +36 -4
- package/dist/client/index.js +8 -4
- package/dist/client.iife.js +42 -19
- package/dist/{notes-BMnonq46.d.ts → commands-fjuqKzkm.d.ts} +125 -114
- package/dist/core/index.d.ts +26 -3
- package/dist/core/index.js +9 -1
- package/dist/daemon/index.d.ts +4 -4
- package/dist/daemon/index.js +6 -8
- package/dist/{engine-B43IohQY.d.ts → engine-CmchnMDq.d.ts} +2 -2
- package/dist/index.d.ts +5 -5
- package/dist/index.js +14 -7
- package/dist/mcp/index.d.ts +4 -4
- package/dist/mcp/index.js +7 -4
- package/dist/{projects-D5J-egVN.d.ts → projects-DB7S312i.d.ts} +1 -1
- package/dist/{server-BztYp1Zc.d.ts → server-DjV7RWQM.d.ts} +10 -2
- package/docs/cli.md +4 -1
- package/docs/component-resolver.md +108 -0
- package/docs/getting-started.md +60 -6
- package/docs/index.md +1 -0
- package/docs/mcp-reference.md +44 -2
- package/docs/visual-notes.md +36 -0
- package/package.json +1 -1
- package/packages/cli/src/index.ts +247 -151
- package/packages/client/src/client.ts +18 -1
- package/packages/client/src/config.ts +84 -1
- package/packages/client/src/index.ts +4 -1
- package/packages/client/src/interceptors/interaction.ts +3 -0
- package/packages/client/src/interceptors/navigation.ts +38 -26
- package/packages/client/src/interceptors/network.ts +22 -17
- package/packages/client/src/notes/inspector.ts +186 -51
- package/packages/client/src/source/resolver.ts +278 -0
- package/packages/client/src/transport/websocket.ts +23 -18
- package/packages/core/src/index.ts +1 -0
- package/packages/core/src/safety.ts +86 -0
- package/packages/core/src/types/events.ts +3 -0
- package/packages/core/src/types/notes.ts +11 -0
- package/packages/daemon/src/server/daemon.ts +7 -1
- package/packages/daemon/src/server/http.ts +125 -5
- package/packages/daemon/src/server/ws.ts +33 -29
- package/packages/daemon/src/storage/db.ts +57 -35
- package/packages/mcp/src/handlers.ts +115 -45
- package/packages/mcp/src/server.ts +202 -2
- package/test/client/component-resolver.test.ts +141 -0
- package/test/client/interceptors.test.ts +56 -0
- package/test/core/safety.test.ts +106 -0
- package/test/daemon/storage.test.ts +36 -0
- package/test/e2e/daemon-mcp-e2e.test.ts +10 -0
- package/test/mcp/auto-start.test.ts +87 -0
- package/dist/chunk-3HOXPTM2.js.map +0 -1
- package/dist/chunk-6VA7GBAO.js.map +0 -1
- package/dist/chunk-7OCOQGDN.js +0 -635
- package/dist/chunk-7OCOQGDN.js.map +0 -1
- package/dist/chunk-UP5JKCFY.js.map +0 -1
- 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
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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.
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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('
|
|
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:
|
|
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
|
-
|
|
449
|
-
|
|
450
|
-
incident.lastElement ?
|
|
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
|
-
|
|
479
|
-
update.lastElement ?
|
|
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
|
-
|
|
504
|
-
occurrence.lastElement ?
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
571
|
-
|
|
572
|
-
note.target ?
|
|
573
|
-
note.elementContext ?
|
|
574
|
-
note.region ?
|
|
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
|
-
|
|
743
|
-
v.geometryDiff ?
|
|
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:
|
|
758
|
-
geometryDiff:
|
|
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:
|
|
802
|
-
networkFailures:
|
|
803
|
-
lastElement:
|
|
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:
|
|
822
|
-
scroll:
|
|
823
|
-
target:
|
|
824
|
-
elementContext:
|
|
825
|
-
region:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 },
|