browsertrack 0.1.2 → 0.2.1
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 +6 -2
- package/dist/{chunk-ILRYKMME.js → chunk-3HOXPTM2.js} +97 -3
- package/dist/chunk-3HOXPTM2.js.map +1 -0
- package/dist/{chunk-G2Y3CXCY.js → chunk-464D4U2U.js} +86 -3
- package/dist/chunk-464D4U2U.js.map +1 -0
- package/dist/{chunk-SPCIROIU.js → chunk-7OCOQGDN.js} +24 -5
- package/dist/chunk-7OCOQGDN.js.map +1 -0
- package/dist/{chunk-SKCMT2DE.js → chunk-INXDWPJW.js} +806 -160
- package/dist/chunk-INXDWPJW.js.map +1 -0
- package/dist/cli/index.js +202 -6
- package/dist/cli/index.js.map +1 -1
- package/dist/client/index.cjs +809 -161
- package/dist/client/index.d.ts +62 -11
- package/dist/client/index.js +7 -3
- package/dist/client.iife.js +206 -27
- package/dist/{notes-CBvN91Wf.d.ts → commands-fjuqKzkm.d.ts} +125 -91
- package/dist/core/index.d.ts +2 -2
- package/dist/daemon/index.d.ts +3 -3
- package/dist/daemon/index.js +2 -2
- package/dist/{engine-CeT9URuN.d.ts → engine-CmchnMDq.d.ts} +13 -2
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -4
- package/dist/mcp/index.d.ts +4 -4
- package/dist/mcp/index.js +2 -2
- package/dist/{projects-CY8ungMt.d.ts → projects-DB7S312i.d.ts} +1 -1
- package/dist/{server-Dd8NX2Mk.d.ts → server-DiVmTrIR.d.ts} +1 -1
- package/docs/component-resolver.md +108 -0
- package/docs/index.md +3 -1
- package/docs/mcp-reference.md +15 -2
- package/docs/scenarios-flows.md +86 -0
- package/docs/visual-notes.md +36 -0
- package/package.json +1 -1
- 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/notes/inspector.ts +640 -166
- package/packages/client/src/source/resolver.ts +272 -0
- package/packages/core/src/types/events.ts +3 -0
- package/packages/core/src/types/notes.ts +36 -0
- package/packages/daemon/src/notes/engine.ts +6 -0
- package/packages/daemon/src/server/ws.ts +23 -1
- package/packages/daemon/src/storage/db.ts +106 -3
- package/packages/mcp/src/handlers.ts +59 -0
- package/packages/mcp/src/tools.ts +28 -0
- package/test/client/component-resolver.test.ts +141 -0
- package/test/client/interceptors.test.ts +120 -0
- package/test/daemon/scenario-storage.test.ts +158 -0
- package/dist/chunk-G2Y3CXCY.js.map +0 -1
- package/dist/chunk-ILRYKMME.js.map +0 -1
- package/dist/chunk-SKCMT2DE.js.map +0 -1
- package/dist/chunk-SPCIROIU.js.map +0 -1
package/dist/cli/index.js
CHANGED
|
@@ -142,6 +142,9 @@ var StorageDB = class {
|
|
|
142
142
|
region_json TEXT,
|
|
143
143
|
screenshot_path TEXT,
|
|
144
144
|
incident_id TEXT,
|
|
145
|
+
scenario_id TEXT,
|
|
146
|
+
step_number INTEGER,
|
|
147
|
+
scenario_title TEXT,
|
|
145
148
|
status TEXT DEFAULT 'OPEN',
|
|
146
149
|
created_at TEXT,
|
|
147
150
|
updated_at TEXT,
|
|
@@ -164,7 +167,20 @@ var StorageDB = class {
|
|
|
164
167
|
CREATE INDEX IF NOT EXISTS idx_incidents_project ON incidents(project_id, status);
|
|
165
168
|
CREATE INDEX IF NOT EXISTS idx_incidents_fp ON incidents(fingerprint);
|
|
166
169
|
CREATE INDEX IF NOT EXISTS idx_notes_project ON notes(project_id, status);
|
|
170
|
+
CREATE INDEX IF NOT EXISTS idx_notes_scenario ON notes(scenario_id, step_number);
|
|
167
171
|
`);
|
|
172
|
+
try {
|
|
173
|
+
this.db.exec("ALTER TABLE notes ADD COLUMN scenario_id TEXT;");
|
|
174
|
+
} catch {
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
this.db.exec("ALTER TABLE notes ADD COLUMN step_number INTEGER;");
|
|
178
|
+
} catch {
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
this.db.exec("ALTER TABLE notes ADD COLUMN scenario_title TEXT;");
|
|
182
|
+
} catch {
|
|
183
|
+
}
|
|
168
184
|
}
|
|
169
185
|
// --- PROJECTS ---
|
|
170
186
|
upsertProject(project) {
|
|
@@ -466,8 +482,8 @@ var StorageDB = class {
|
|
|
466
482
|
`INSERT INTO notes (
|
|
467
483
|
id, project_id, session_id, type, message, route, url,
|
|
468
484
|
viewport_json, scroll_json, target_json, element_context_json, region_json,
|
|
469
|
-
screenshot_path, incident_id, status, created_at, updated_at, resolved_at
|
|
470
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
485
|
+
screenshot_path, incident_id, scenario_id, step_number, scenario_title, status, created_at, updated_at, resolved_at
|
|
486
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
471
487
|
).run(
|
|
472
488
|
note.id,
|
|
473
489
|
note.projectId,
|
|
@@ -483,6 +499,9 @@ var StorageDB = class {
|
|
|
483
499
|
note.region ? JSON.stringify(note.region) : null,
|
|
484
500
|
note.screenshots?.original || null,
|
|
485
501
|
note.incidentId || null,
|
|
502
|
+
note.scenarioId || null,
|
|
503
|
+
note.stepNumber ?? null,
|
|
504
|
+
note.scenarioTitle || null,
|
|
486
505
|
note.status,
|
|
487
506
|
note.createdAt,
|
|
488
507
|
note.updatedAt,
|
|
@@ -509,6 +528,10 @@ var StorageDB = class {
|
|
|
509
528
|
sql += " AND route = ?";
|
|
510
529
|
params.push(options.route);
|
|
511
530
|
}
|
|
531
|
+
if (options.scenarioId) {
|
|
532
|
+
sql += " AND scenario_id = ?";
|
|
533
|
+
params.push(options.scenarioId);
|
|
534
|
+
}
|
|
512
535
|
sql += " ORDER BY created_at DESC LIMIT ?";
|
|
513
536
|
params.push(options.limit || 50);
|
|
514
537
|
const rows = this.db.prepare(sql).all(...params);
|
|
@@ -518,6 +541,71 @@ var StorageDB = class {
|
|
|
518
541
|
this.db.prepare("DELETE FROM note_verifications WHERE note_id = ?").run(id);
|
|
519
542
|
this.db.prepare("DELETE FROM notes WHERE id = ?").run(id);
|
|
520
543
|
}
|
|
544
|
+
// --- SCENARIOS (MULTI-STEP FLOWS) ---
|
|
545
|
+
listScenarios(options = {}) {
|
|
546
|
+
let sql = `
|
|
547
|
+
SELECT
|
|
548
|
+
scenario_id,
|
|
549
|
+
project_id,
|
|
550
|
+
COALESCE(MAX(scenario_title), 'Scenario') as title,
|
|
551
|
+
COUNT(id) as steps_count,
|
|
552
|
+
CASE WHEN SUM(CASE WHEN status != 'RESOLVED' THEN 1 ELSE 0 END) = 0 THEN 'RESOLVED' ELSE 'OPEN' END as status,
|
|
553
|
+
MIN(route) as route,
|
|
554
|
+
MIN(created_at) as first_step_at,
|
|
555
|
+
MAX(created_at) as last_step_at
|
|
556
|
+
FROM notes
|
|
557
|
+
WHERE scenario_id IS NOT NULL
|
|
558
|
+
`;
|
|
559
|
+
const params = [];
|
|
560
|
+
if (options.projectId) {
|
|
561
|
+
sql += " AND (project_id = ? OR project_id IS NULL)";
|
|
562
|
+
params.push(options.projectId);
|
|
563
|
+
}
|
|
564
|
+
sql += " GROUP BY scenario_id";
|
|
565
|
+
if (options.status) {
|
|
566
|
+
if (options.status === "RESOLVED") {
|
|
567
|
+
sql += " HAVING SUM(CASE WHEN status != 'RESOLVED' THEN 1 ELSE 0 END) = 0";
|
|
568
|
+
} else {
|
|
569
|
+
sql += " HAVING SUM(CASE WHEN status != 'RESOLVED' THEN 1 ELSE 0 END) > 0";
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
sql += " ORDER BY last_step_at DESC LIMIT ?";
|
|
573
|
+
params.push(options.limit || 50);
|
|
574
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
575
|
+
return rows.map((r) => ({
|
|
576
|
+
id: r.scenario_id,
|
|
577
|
+
projectId: r.project_id,
|
|
578
|
+
title: r.title,
|
|
579
|
+
stepsCount: Number(r.steps_count),
|
|
580
|
+
status: r.status,
|
|
581
|
+
route: r.route || "/",
|
|
582
|
+
firstStepAt: r.first_step_at,
|
|
583
|
+
lastStepAt: r.last_step_at
|
|
584
|
+
}));
|
|
585
|
+
}
|
|
586
|
+
getScenario(scenarioId) {
|
|
587
|
+
const rows = this.db.prepare("SELECT * FROM notes WHERE scenario_id = ? ORDER BY COALESCE(step_number, 999999) ASC, created_at ASC").all(scenarioId);
|
|
588
|
+
if (!rows || rows.length === 0) return null;
|
|
589
|
+
const steps = rows.map((r) => this.mapNoteRow(r));
|
|
590
|
+
const title = steps.find((s) => s.scenarioTitle)?.scenarioTitle || `Scenario ${scenarioId}`;
|
|
591
|
+
const allResolved = steps.every((s) => s.status === "RESOLVED");
|
|
592
|
+
return {
|
|
593
|
+
id: scenarioId,
|
|
594
|
+
projectId: steps[0].projectId,
|
|
595
|
+
title,
|
|
596
|
+
stepsCount: steps.length,
|
|
597
|
+
status: allResolved ? "RESOLVED" : "OPEN",
|
|
598
|
+
steps,
|
|
599
|
+
createdAt: steps[0].createdAt,
|
|
600
|
+
updatedAt: steps[steps.length - 1].updatedAt
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
deleteScenario(scenarioId) {
|
|
604
|
+
const notes = this.db.prepare("SELECT id FROM notes WHERE scenario_id = ?").all(scenarioId);
|
|
605
|
+
for (const n of notes) {
|
|
606
|
+
this.deleteNote(n.id);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
521
609
|
updateNoteStatus(id, status) {
|
|
522
610
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
523
611
|
const resolvedAt = status === "RESOLVED" ? now : null;
|
|
@@ -629,6 +717,9 @@ var StorageDB = class {
|
|
|
629
717
|
region: row.region_json ? JSON.parse(row.region_json) : void 0,
|
|
630
718
|
status: row.status,
|
|
631
719
|
incidentId: row.incident_id || void 0,
|
|
720
|
+
scenarioId: row.scenario_id || void 0,
|
|
721
|
+
stepNumber: row.step_number != null ? Number(row.step_number) : void 0,
|
|
722
|
+
scenarioTitle: row.scenario_title || void 0,
|
|
632
723
|
screenshots: row.screenshot_path ? {
|
|
633
724
|
original: row.screenshot_path
|
|
634
725
|
} : void 0,
|
|
@@ -1174,6 +1265,9 @@ var NotesEngine = class {
|
|
|
1174
1265
|
region: payload.region,
|
|
1175
1266
|
status: "OPEN",
|
|
1176
1267
|
incidentId: payload.incidentId,
|
|
1268
|
+
scenarioId: payload.scenarioId,
|
|
1269
|
+
stepNumber: payload.stepNumber,
|
|
1270
|
+
scenarioTitle: payload.scenarioTitle,
|
|
1177
1271
|
screenshots: screenshotPath ? { original: screenshotPath } : void 0,
|
|
1178
1272
|
createdAt: now,
|
|
1179
1273
|
updatedAt: now
|
|
@@ -1796,16 +1890,23 @@ function setupWebSocketServer(wss, db, sessionManager, incidentEngine, notesEngi
|
|
|
1796
1890
|
elementContext: data.elementContext,
|
|
1797
1891
|
region: data.region,
|
|
1798
1892
|
screenshot: data.screenshot,
|
|
1799
|
-
incidentId: data.incidentId
|
|
1893
|
+
incidentId: data.incidentId,
|
|
1894
|
+
scenarioId: data.scenarioId,
|
|
1895
|
+
stepNumber: data.stepNumber,
|
|
1896
|
+
scenarioTitle: data.scenarioTitle
|
|
1800
1897
|
});
|
|
1801
1898
|
if (verbose) {
|
|
1802
|
-
console.log(
|
|
1899
|
+
console.log(
|
|
1900
|
+
`[BrowserTrack] Visual note created: ${note.id} on ${note.route} ("${note.message}")${note.scenarioId ? ` [Scenario: ${note.scenarioTitle || note.scenarioId} Step ${note.stepNumber}]` : ""}`
|
|
1901
|
+
);
|
|
1803
1902
|
}
|
|
1804
1903
|
ws.send(
|
|
1805
1904
|
JSON.stringify({
|
|
1806
1905
|
type: "note_created_ack",
|
|
1807
1906
|
noteId: note.id,
|
|
1808
|
-
status: note.status
|
|
1907
|
+
status: note.status,
|
|
1908
|
+
scenarioId: note.scenarioId,
|
|
1909
|
+
stepNumber: note.stepNumber
|
|
1809
1910
|
})
|
|
1810
1911
|
);
|
|
1811
1912
|
const allNotes = db.listNotes({ projectId: note.projectId, limit: 100 });
|
|
@@ -1851,6 +1952,18 @@ function setupWebSocketServer(wss, db, sessionManager, incidentEngine, notesEngi
|
|
|
1851
1952
|
}
|
|
1852
1953
|
return;
|
|
1853
1954
|
}
|
|
1955
|
+
if (data.type === "delete_scenario" && data.scenarioId) {
|
|
1956
|
+
const scenario = db.getScenario(data.scenarioId);
|
|
1957
|
+
if (scenario) {
|
|
1958
|
+
db.deleteScenario(data.scenarioId);
|
|
1959
|
+
const allNotes = db.listNotes({ projectId: scenario.projectId, limit: 100 });
|
|
1960
|
+
sessionManager.broadcastToProject(scenario.projectId, {
|
|
1961
|
+
type: "notes_sync",
|
|
1962
|
+
notes: allNotes
|
|
1963
|
+
});
|
|
1964
|
+
}
|
|
1965
|
+
return;
|
|
1966
|
+
}
|
|
1854
1967
|
if (data.type === "get_notes") {
|
|
1855
1968
|
const session = currentSessionId ? db.getSession(currentSessionId) : null;
|
|
1856
1969
|
const projectId = data.projectId || session?.projectId;
|
|
@@ -2082,7 +2195,8 @@ async function handleToolCall(name, args, ctx) {
|
|
|
2082
2195
|
tag: incident.lastElement.tag,
|
|
2083
2196
|
visible: incident.lastElement.visible,
|
|
2084
2197
|
innerText: incident.lastElement.innerText,
|
|
2085
|
-
outerHTML: incident.lastElement.outerHTML
|
|
2198
|
+
outerHTML: incident.lastElement.outerHTML,
|
|
2199
|
+
componentSource: incident.lastElement.componentSource
|
|
2086
2200
|
} : void 0,
|
|
2087
2201
|
recentBreadcrumbs: breadcrumbsTimeline,
|
|
2088
2202
|
networkFailures: incident.networkFailures,
|
|
@@ -2208,6 +2322,7 @@ async function handleToolCall(name, args, ctx) {
|
|
|
2208
2322
|
case "list_notes": {
|
|
2209
2323
|
const notes = db.listNotes({
|
|
2210
2324
|
projectId: args.projectId,
|
|
2325
|
+
scenarioId: args.scenarioId,
|
|
2211
2326
|
status: args.status,
|
|
2212
2327
|
limit: args.limit || 20
|
|
2213
2328
|
});
|
|
@@ -2218,6 +2333,9 @@ async function handleToolCall(name, args, ctx) {
|
|
|
2218
2333
|
type: n.type,
|
|
2219
2334
|
status: n.status,
|
|
2220
2335
|
route: n.route,
|
|
2336
|
+
scenarioId: n.scenarioId,
|
|
2337
|
+
stepNumber: n.stepNumber,
|
|
2338
|
+
scenarioTitle: n.scenarioTitle,
|
|
2221
2339
|
viewport: `${n.viewport.width} \xD7 ${n.viewport.height}`,
|
|
2222
2340
|
target: n.target?.selector || n.type,
|
|
2223
2341
|
message: n.message,
|
|
@@ -2226,6 +2344,53 @@ async function handleToolCall(name, args, ctx) {
|
|
|
2226
2344
|
}))
|
|
2227
2345
|
};
|
|
2228
2346
|
}
|
|
2347
|
+
case "list_scenarios": {
|
|
2348
|
+
const scenarios = db.listScenarios({
|
|
2349
|
+
projectId: args.projectId,
|
|
2350
|
+
status: args.status,
|
|
2351
|
+
limit: args.limit || 20
|
|
2352
|
+
});
|
|
2353
|
+
return {
|
|
2354
|
+
total: scenarios.length,
|
|
2355
|
+
scenarios: scenarios.map((s) => ({
|
|
2356
|
+
id: s.id,
|
|
2357
|
+
title: s.title,
|
|
2358
|
+
stepsCount: s.stepsCount,
|
|
2359
|
+
status: s.status,
|
|
2360
|
+
route: s.route,
|
|
2361
|
+
firstStepAt: s.firstStepAt,
|
|
2362
|
+
lastStepAt: s.lastStepAt
|
|
2363
|
+
}))
|
|
2364
|
+
};
|
|
2365
|
+
}
|
|
2366
|
+
case "get_scenario": {
|
|
2367
|
+
const scenario = db.getScenario(args.scenarioId);
|
|
2368
|
+
if (!scenario) {
|
|
2369
|
+
throw new Error(`Scenario '${args.scenarioId}' not found.`);
|
|
2370
|
+
}
|
|
2371
|
+
return {
|
|
2372
|
+
id: scenario.id,
|
|
2373
|
+
title: scenario.title,
|
|
2374
|
+
stepsCount: scenario.stepsCount,
|
|
2375
|
+
status: scenario.status,
|
|
2376
|
+
steps: scenario.steps.map((step) => ({
|
|
2377
|
+
id: step.id,
|
|
2378
|
+
stepNumber: step.stepNumber,
|
|
2379
|
+
type: step.type,
|
|
2380
|
+
message: step.message,
|
|
2381
|
+
route: step.route,
|
|
2382
|
+
url: step.url,
|
|
2383
|
+
targetSelector: step.target?.selector,
|
|
2384
|
+
boundingRect: step.target?.boundingRect || step.region,
|
|
2385
|
+
elementContext: step.elementContext,
|
|
2386
|
+
screenshot: step.screenshots?.original,
|
|
2387
|
+
status: step.status,
|
|
2388
|
+
createdAt: step.createdAt
|
|
2389
|
+
})),
|
|
2390
|
+
createdAt: scenario.createdAt,
|
|
2391
|
+
updatedAt: scenario.updatedAt
|
|
2392
|
+
};
|
|
2393
|
+
}
|
|
2229
2394
|
case "get_note": {
|
|
2230
2395
|
const note = db.getNote(args.noteId);
|
|
2231
2396
|
if (!note) {
|
|
@@ -2237,6 +2402,9 @@ async function handleToolCall(name, args, ctx) {
|
|
|
2237
2402
|
type: note.type,
|
|
2238
2403
|
status: note.status,
|
|
2239
2404
|
message: note.message,
|
|
2405
|
+
scenarioId: note.scenarioId,
|
|
2406
|
+
stepNumber: note.stepNumber,
|
|
2407
|
+
scenarioTitle: note.scenarioTitle,
|
|
2240
2408
|
route: note.route,
|
|
2241
2409
|
url: note.url,
|
|
2242
2410
|
viewport: note.viewport,
|
|
@@ -2494,6 +2662,7 @@ var TOOLS = [
|
|
|
2494
2662
|
type: "object",
|
|
2495
2663
|
properties: {
|
|
2496
2664
|
projectId: { type: "string", description: "Filter notes by project ID or name" },
|
|
2665
|
+
scenarioId: { type: "string", description: "Filter notes by scenario / flow ID" },
|
|
2497
2666
|
status: {
|
|
2498
2667
|
type: "string",
|
|
2499
2668
|
enum: ["OPEN", "IN_PROGRESS", "VERIFYING", "RESOLVED", "FAILED", "INCONCLUSIVE"],
|
|
@@ -2503,6 +2672,33 @@ var TOOLS = [
|
|
|
2503
2672
|
}
|
|
2504
2673
|
}
|
|
2505
2674
|
},
|
|
2675
|
+
{
|
|
2676
|
+
name: "list_scenarios",
|
|
2677
|
+
description: "List recorded reproduction scenarios and multi-step user interaction flows",
|
|
2678
|
+
inputSchema: {
|
|
2679
|
+
type: "object",
|
|
2680
|
+
properties: {
|
|
2681
|
+
projectId: { type: "string", description: "Filter scenarios by project ID or name" },
|
|
2682
|
+
status: {
|
|
2683
|
+
type: "string",
|
|
2684
|
+
enum: ["OPEN", "RESOLVED"],
|
|
2685
|
+
description: "Filter by scenario status"
|
|
2686
|
+
},
|
|
2687
|
+
limit: { type: "number", description: "Maximum number of scenarios to return (default: 20)" }
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
},
|
|
2691
|
+
{
|
|
2692
|
+
name: "get_scenario",
|
|
2693
|
+
description: "Retrieve full chronological step-by-step reproduction flow with selectors, route, screenshots, and action details",
|
|
2694
|
+
inputSchema: {
|
|
2695
|
+
type: "object",
|
|
2696
|
+
properties: {
|
|
2697
|
+
scenarioId: { type: "string", description: "The unique ID of the scenario / flow (e.g. scen_123)" }
|
|
2698
|
+
},
|
|
2699
|
+
required: ["scenarioId"]
|
|
2700
|
+
}
|
|
2701
|
+
},
|
|
2506
2702
|
{
|
|
2507
2703
|
name: "get_note",
|
|
2508
2704
|
description: "Retrieve full debugging context for a visual note (message, route, viewport dimensions, target element selector, DOM context, screenshot file path, project path)",
|