browsertrack 0.1.2 → 0.2.0

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 (43) hide show
  1. package/AGENTS.md +4 -2
  2. package/dist/{chunk-ILRYKMME.js → chunk-3HOXPTM2.js} +97 -3
  3. package/dist/chunk-3HOXPTM2.js.map +1 -0
  4. package/dist/{chunk-SPCIROIU.js → chunk-7OCOQGDN.js} +24 -5
  5. package/dist/chunk-7OCOQGDN.js.map +1 -0
  6. package/dist/{chunk-G2Y3CXCY.js → chunk-UP5JKCFY.js} +84 -2
  7. package/dist/chunk-UP5JKCFY.js.map +1 -0
  8. package/dist/{chunk-SKCMT2DE.js → chunk-WB7ZKWK7.js} +453 -152
  9. package/dist/chunk-WB7ZKWK7.js.map +1 -0
  10. package/dist/cli/index.js +200 -5
  11. package/dist/cli/index.js.map +1 -1
  12. package/dist/client/index.cjs +452 -151
  13. package/dist/client/index.d.ts +28 -9
  14. package/dist/client/index.js +1 -1
  15. package/dist/client.iife.js +185 -29
  16. package/dist/core/index.d.ts +2 -2
  17. package/dist/daemon/index.d.ts +3 -3
  18. package/dist/daemon/index.js +2 -2
  19. package/dist/{engine-CeT9URuN.d.ts → engine-B43IohQY.d.ts} +13 -2
  20. package/dist/index.d.ts +4 -4
  21. package/dist/index.js +4 -4
  22. package/dist/mcp/index.d.ts +4 -4
  23. package/dist/mcp/index.js +2 -2
  24. package/dist/{notes-CBvN91Wf.d.ts → notes-BMnonq46.d.ts} +24 -1
  25. package/dist/{projects-CY8ungMt.d.ts → projects-D5J-egVN.d.ts} +1 -1
  26. package/dist/{server-Dd8NX2Mk.d.ts → server-BztYp1Zc.d.ts} +1 -1
  27. package/docs/index.md +2 -1
  28. package/docs/mcp-reference.md +14 -1
  29. package/docs/scenarios-flows.md +86 -0
  30. package/package.json +1 -1
  31. package/packages/client/src/notes/inspector.ts +533 -161
  32. package/packages/core/src/types/notes.ts +25 -0
  33. package/packages/daemon/src/notes/engine.ts +6 -0
  34. package/packages/daemon/src/server/ws.ts +23 -1
  35. package/packages/daemon/src/storage/db.ts +106 -3
  36. package/packages/mcp/src/handlers.ts +58 -0
  37. package/packages/mcp/src/tools.ts +28 -0
  38. package/test/client/interceptors.test.ts +64 -0
  39. package/test/daemon/scenario-storage.test.ts +158 -0
  40. package/dist/chunk-G2Y3CXCY.js.map +0 -1
  41. package/dist/chunk-ILRYKMME.js.map +0 -1
  42. package/dist/chunk-SKCMT2DE.js.map +0 -1
  43. 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(`[BrowserTrack] Visual note created: ${note.id} on ${note.route} ("${note.message}")`);
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;
@@ -2208,6 +2321,7 @@ async function handleToolCall(name, args, ctx) {
2208
2321
  case "list_notes": {
2209
2322
  const notes = db.listNotes({
2210
2323
  projectId: args.projectId,
2324
+ scenarioId: args.scenarioId,
2211
2325
  status: args.status,
2212
2326
  limit: args.limit || 20
2213
2327
  });
@@ -2218,6 +2332,9 @@ async function handleToolCall(name, args, ctx) {
2218
2332
  type: n.type,
2219
2333
  status: n.status,
2220
2334
  route: n.route,
2335
+ scenarioId: n.scenarioId,
2336
+ stepNumber: n.stepNumber,
2337
+ scenarioTitle: n.scenarioTitle,
2221
2338
  viewport: `${n.viewport.width} \xD7 ${n.viewport.height}`,
2222
2339
  target: n.target?.selector || n.type,
2223
2340
  message: n.message,
@@ -2226,6 +2343,53 @@ async function handleToolCall(name, args, ctx) {
2226
2343
  }))
2227
2344
  };
2228
2345
  }
2346
+ case "list_scenarios": {
2347
+ const scenarios = db.listScenarios({
2348
+ projectId: args.projectId,
2349
+ status: args.status,
2350
+ limit: args.limit || 20
2351
+ });
2352
+ return {
2353
+ total: scenarios.length,
2354
+ scenarios: scenarios.map((s) => ({
2355
+ id: s.id,
2356
+ title: s.title,
2357
+ stepsCount: s.stepsCount,
2358
+ status: s.status,
2359
+ route: s.route,
2360
+ firstStepAt: s.firstStepAt,
2361
+ lastStepAt: s.lastStepAt
2362
+ }))
2363
+ };
2364
+ }
2365
+ case "get_scenario": {
2366
+ const scenario = db.getScenario(args.scenarioId);
2367
+ if (!scenario) {
2368
+ throw new Error(`Scenario '${args.scenarioId}' not found.`);
2369
+ }
2370
+ return {
2371
+ id: scenario.id,
2372
+ title: scenario.title,
2373
+ stepsCount: scenario.stepsCount,
2374
+ status: scenario.status,
2375
+ steps: scenario.steps.map((step) => ({
2376
+ id: step.id,
2377
+ stepNumber: step.stepNumber,
2378
+ type: step.type,
2379
+ message: step.message,
2380
+ route: step.route,
2381
+ url: step.url,
2382
+ targetSelector: step.target?.selector,
2383
+ boundingRect: step.target?.boundingRect || step.region,
2384
+ elementContext: step.elementContext,
2385
+ screenshot: step.screenshots?.original,
2386
+ status: step.status,
2387
+ createdAt: step.createdAt
2388
+ })),
2389
+ createdAt: scenario.createdAt,
2390
+ updatedAt: scenario.updatedAt
2391
+ };
2392
+ }
2229
2393
  case "get_note": {
2230
2394
  const note = db.getNote(args.noteId);
2231
2395
  if (!note) {
@@ -2237,6 +2401,9 @@ async function handleToolCall(name, args, ctx) {
2237
2401
  type: note.type,
2238
2402
  status: note.status,
2239
2403
  message: note.message,
2404
+ scenarioId: note.scenarioId,
2405
+ stepNumber: note.stepNumber,
2406
+ scenarioTitle: note.scenarioTitle,
2240
2407
  route: note.route,
2241
2408
  url: note.url,
2242
2409
  viewport: note.viewport,
@@ -2494,6 +2661,7 @@ var TOOLS = [
2494
2661
  type: "object",
2495
2662
  properties: {
2496
2663
  projectId: { type: "string", description: "Filter notes by project ID or name" },
2664
+ scenarioId: { type: "string", description: "Filter notes by scenario / flow ID" },
2497
2665
  status: {
2498
2666
  type: "string",
2499
2667
  enum: ["OPEN", "IN_PROGRESS", "VERIFYING", "RESOLVED", "FAILED", "INCONCLUSIVE"],
@@ -2503,6 +2671,33 @@ var TOOLS = [
2503
2671
  }
2504
2672
  }
2505
2673
  },
2674
+ {
2675
+ name: "list_scenarios",
2676
+ description: "List recorded reproduction scenarios and multi-step user interaction flows",
2677
+ inputSchema: {
2678
+ type: "object",
2679
+ properties: {
2680
+ projectId: { type: "string", description: "Filter scenarios by project ID or name" },
2681
+ status: {
2682
+ type: "string",
2683
+ enum: ["OPEN", "RESOLVED"],
2684
+ description: "Filter by scenario status"
2685
+ },
2686
+ limit: { type: "number", description: "Maximum number of scenarios to return (default: 20)" }
2687
+ }
2688
+ }
2689
+ },
2690
+ {
2691
+ name: "get_scenario",
2692
+ description: "Retrieve full chronological step-by-step reproduction flow with selectors, route, screenshots, and action details",
2693
+ inputSchema: {
2694
+ type: "object",
2695
+ properties: {
2696
+ scenarioId: { type: "string", description: "The unique ID of the scenario / flow (e.g. scen_123)" }
2697
+ },
2698
+ required: ["scenarioId"]
2699
+ }
2700
+ },
2506
2701
  {
2507
2702
  name: "get_note",
2508
2703
  description: "Retrieve full debugging context for a visual note (message, route, viewport dimensions, target element selector, DOM context, screenshot file path, project path)",