tracegist-mcp-bridge 0.2.8 → 0.2.12

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/README.md CHANGED
@@ -43,6 +43,10 @@ Set `TRACEGIST_DIR` to change where the bridge looks for packages (defaults to `
43
43
  - `read_tracegist_package_file` — read any text file from inside the ZIP without writing to disk (e.g. `network/api-requests.jsonl`, the Playwright repro script)
44
44
  - `extract_tracegist_package_file` — write a binary or text file from the ZIP to a local directory
45
45
  - `transcribe_tracegist_package_voice_notes` — transcribe voice notes using local Python Whisper
46
+ - `watch_live_session` — poll real-time session events while recording is active
47
+ - `get_live_screenshot` — request and retrieve a screenshot of the current tab
48
+ - `ask_tester_question` — send a question to the tester (shown as a browser toast, max 200 chars)
49
+ - `get_tester_response` — retrieve the tester's voice + screenshot answer
46
50
 
47
51
  ## Package contents
48
52
 
package/bin/lib.mjs CHANGED
@@ -11,14 +11,17 @@ export function normalizeZipBaseName(entryName) {
11
11
  return path.posix.basename(entryName).toLowerCase();
12
12
  }
13
13
 
14
- export function renderPackagesText(packages, directory) {
14
+ export function renderPackagesText(packages, directory, totalCount = null, hasMore = false, offset = 0) {
15
15
  if (packages.length === 0) {
16
16
  return [
17
17
  `No TraceGist package zips found in ${directory}.`,
18
18
  "Expected naming pattern: tracegist-...-package.zip",
19
19
  ].join("\n");
20
20
  }
21
- const lines = [`Found ${packages.length} TraceGist package(s) in ${directory}:`, ""];
21
+ const countLabel = totalCount != null
22
+ ? `Showing ${packages.length} of ${totalCount} TraceGist package(s) in ${directory}:`
23
+ : `Found ${packages.length} TraceGist package(s) in ${directory}:`;
24
+ const lines = [countLabel, ""];
22
25
  for (const pkg of packages) {
23
26
  lines.push(
24
27
  `- ${pkg.name}`,
@@ -28,6 +31,16 @@ export function renderPackagesText(packages, directory) {
28
31
  "",
29
32
  );
30
33
  }
34
+ if (hasMore) {
35
+ lines.push(
36
+ `More packages available. Use offset: ${offset + packages.length} to see the next page.`,
37
+ "",
38
+ );
39
+ }
40
+ lines.push(
41
+ "Each package includes a ready-to-run Playwright repro script.",
42
+ "Call get_tracegist_handoff_markdown (no section param) on a package to see the TOC with repro script paths.",
43
+ );
31
44
  return lines.join("\n");
32
45
  }
33
46
 
@@ -74,7 +87,8 @@ const SECTION_HINTS = {
74
87
  "user interaction timeline": "Full click/fill/navigation sequence — key for reproduction",
75
88
  "session context": "Page URL, title, session metadata",
76
89
  "session environment": "Browser, viewport, OS — match for reproduction",
77
- "package files": "Paths to Playwright script, manifest, network bodies",
90
+ "package files":
91
+ "ACTION: Read the Python repro script path here — use read_tracegist_package_file before writing any automation",
78
92
  "marker-to-file mapping": "Which screenshots/voice files belong to each marker",
79
93
  "backend log correlation": "Absolute timestamps for server-side log alignment",
80
94
  "full session console timeline": "All console output (deep exports only)",
@@ -89,20 +103,54 @@ const SECTION_HINTS = {
89
103
  "marker visual evidence": "Screenshot references for each marker",
90
104
  "environment at marker time": "Environment snapshot at a specific marker",
91
105
  "marker timeline logs (context window)": "Logs within the ±5 s marker window",
106
+ "webapp testing reproduction":
107
+ "ACTION: Ready-to-run Python repro script path + key selectors — read before writing any automation",
108
+ "tester intent summary": "Classified intent: specifications, issues found, observations",
109
+ "iterative context": "Previous session reference, verification checklist for build-test cycle",
110
+ "tier 0: quick summary": "Session overview, intent, reproduction command (~200 tokens)",
111
+ "tier 1: findings": "Marker timeline with structured findings (~500 tokens)",
112
+ "tier 2: full context": "Interaction timeline, network/console context, environment",
113
+ "tier 3: deep diagnostics": "Full session timelines (deep profile only)",
92
114
  };
93
115
 
94
116
  function getSectionHint(sectionName) {
95
117
  return SECTION_HINTS[sectionName.toLowerCase()] || "Additional section";
96
118
  }
97
119
 
98
- export function renderSectionToc(sections, zipPath) {
120
+ export function renderSectionToc(sections, zipPath, manifest = null) {
99
121
  const totalChars = sections.reduce((sum, s) => sum + s.content.length, 0);
100
122
  const lines = [
101
123
  `Handoff document for ${path.basename(zipPath)} — ${sections.length} sections, ${totalChars.toLocaleString()} chars total.`,
102
124
  "",
125
+ ];
126
+
127
+ // Prominent callout BEFORE the table — shown before any section is read
128
+ const pythonScript = manifest?.pythonPlaywrightScriptPath;
129
+ const tsScript = manifest?.playwrightScriptPath;
130
+ if (pythonScript || tsScript) {
131
+ lines.push(
132
+ "**Playwright repro script is ready — read it before writing any automation code:**",
133
+ );
134
+ if (pythonScript) {
135
+ lines.push(
136
+ `- Python (webapp-testing skill): \`read_tracegist_package_file(zipPath, "${pythonScript}")\``,
137
+ );
138
+ }
139
+ if (tsScript) {
140
+ lines.push(
141
+ `- TypeScript (Playwright Test): \`read_tracegist_package_file(zipPath, "${tsScript}")\``,
142
+ );
143
+ }
144
+ lines.push("");
145
+ }
146
+
147
+ lines.push(
148
+ "**Reading guide:** For analysis, start with Session Triage → Marker Timeline → Notable Anomalies.",
149
+ "For reproduction, start with the Playwright repro script above → Session Environment → User Interaction Timeline.",
150
+ "",
103
151
  "| # | Section | Size | Purpose |",
104
152
  "|---|---------|------|---------|",
105
- ];
153
+ );
106
154
  for (let i = 0; i < sections.length; i++) {
107
155
  const hint = getSectionHint(sections[i].name);
108
156
  lines.push(
@@ -110,9 +158,6 @@ export function renderSectionToc(sections, zipPath) {
110
158
  );
111
159
  }
112
160
  lines.push(
113
- "",
114
- "**Reading guide:** For analysis, start with Session Triage → Marker Timeline → Notable Anomalies.",
115
- "For reproduction, start with Session Environment → User Interaction Timeline → Package Files (Playwright script path).",
116
161
  "",
117
162
  'Pass `section` with a section name (e.g. "Marker Timeline") to retrieve its full content.',
118
163
  );
@@ -85,7 +85,7 @@ async function checkWhisperDependencies() {
85
85
  return warnings;
86
86
  }
87
87
 
88
- async function listTraceGistPackages(directory, limit) {
88
+ async function listTraceGistPackages(directory, limit, offset = 0) {
89
89
  const entries = await fs.readdir(directory, { withFileTypes: true });
90
90
  const zipNames = entries
91
91
  .filter((entry) => entry.isFile() && isTraceGistPackageFile(entry.name))
@@ -105,7 +105,9 @@ async function listTraceGistPackages(directory, limit) {
105
105
  );
106
106
 
107
107
  packages.sort((a, b) => b.modifiedAt.localeCompare(a.modifiedAt));
108
- return packages.slice(0, limit);
108
+ const totalCount = packages.length;
109
+ const sliced = packages.slice(offset, offset + limit);
110
+ return { packages: sliced, totalCount, hasMore: offset + limit < totalCount };
109
111
  }
110
112
 
111
113
  async function readZipEntries(zipPath) {
@@ -569,14 +571,26 @@ server.registerTool(
569
571
  .max(200)
570
572
  .optional()
571
573
  .describe("Maximum number of packages to return (default: 20)."),
574
+ offset: z
575
+ .number()
576
+ .int()
577
+ .min(0)
578
+ .optional()
579
+ .describe("Number of packages to skip for pagination (default: 0)."),
572
580
  }),
573
581
  },
574
- async ({ directory, limit = 20 }) => {
582
+ async ({ directory, limit = 20, offset = 0 }) => {
575
583
  try {
576
584
  const searchDir = directory || DEFAULT_DOWNLOADS_DIR;
577
- const packages = await listTraceGistPackages(searchDir, limit);
585
+ const { packages, totalCount, hasMore } = await listTraceGistPackages(
586
+ searchDir,
587
+ limit,
588
+ offset,
589
+ );
578
590
  return {
579
- content: [{ type: "text", text: renderPackagesText(packages, searchDir) }],
591
+ content: [
592
+ { type: "text", text: renderPackagesText(packages, searchDir, totalCount, hasMore, offset) },
593
+ ],
580
594
  };
581
595
  } catch (err) {
582
596
  return toolError(err);
@@ -715,7 +729,7 @@ server.registerTool(
715
729
  content: [
716
730
  {
717
731
  type: "text",
718
- text: `# Source: ${handoffEntryName}\n\n${renderSectionToc(sections, zipPath)}`,
732
+ text: `# Source: ${handoffEntryName}\n\n${renderSectionToc(sections, zipPath, manifest)}`,
719
733
  },
720
734
  ],
721
735
  };
@@ -928,7 +942,9 @@ async function transcribeLiveVoice(voiceBlobDataUrl) {
928
942
  const text = result.choices?.[0]?.message?.content?.trim();
929
943
  return text ? { transcription: text } : { error: "Empty transcription response" };
930
944
  } catch (err) {
931
- return { error: `OpenRouter transcription failed: ${err instanceof Error ? err.message : String(err)}` };
945
+ return {
946
+ error: `OpenRouter transcription failed: ${err instanceof Error ? err.message : String(err)}`,
947
+ };
932
948
  }
933
949
  }
934
950
 
@@ -950,7 +966,9 @@ async function transcribeLiveVoice(voiceBlobDataUrl) {
950
966
  await fs.rm(tmpDir, { recursive: true, force: true });
951
967
  }
952
968
  } catch (err) {
953
- return { error: `Whisper transcription failed: ${err instanceof Error ? err.message : String(err)}` };
969
+ return {
970
+ error: `Whisper transcription failed: ${err instanceof Error ? err.message : String(err)}`,
971
+ };
954
972
  }
955
973
  }
956
974
 
@@ -1233,24 +1251,33 @@ server.resource(
1233
1251
  );
1234
1252
 
1235
1253
  // Tool: watch_live_session
1236
- server.tool(
1254
+ server.registerTool(
1237
1255
  "watch_live_session",
1238
- "Watch a live TraceGist shadowing session in real-time. Returns buffered events since the given sequence number.\n\n" +
1239
- "IMPORTANT POLLING BEHAVIOR:\n" +
1240
- "- You MUST keep calling this tool every 2-4 seconds in a loop until sessionActive becomes false.\n" +
1241
- "- Do NOT stop polling just because there are no new events — the tester is still active.\n" +
1242
- "- When you see 'voice-transcription' events, the tester recorded a voice marker — read the transcription and react to it.\n" +
1243
- "- When you see 'question-response-received' events, use get_tester_response to retrieve the full answer with images.\n" +
1244
- "- When you see 'question-response-transcribed' events, the tester's voice response has been transcribed — read it for context.\n" +
1245
- "- Marker events with hasPendingVoice=true will have their voiceTranscription field populated once transcription completes.\n" +
1246
- "- The tester may set voice markers at any time to communicate with you — keep watching for them.\n" +
1247
- "- If you need clarification, use ask_tester_question to show a toast notification to the tester.\n" +
1248
- "- Only stop polling when sessionActive is false (the recording ended).",
1249
1256
  {
1250
- since_seq: z
1251
- .number()
1252
- .optional()
1253
- .describe("Return events after this sequence number. Omit for all buffered events."),
1257
+ description:
1258
+ "Watch a live TraceGist shadowing session in real-time. Returns buffered events since the given sequence number.\n\n" +
1259
+ "IMPORTANT POLLING BEHAVIOR:\n" +
1260
+ "- You MUST keep calling this tool every 2-4 seconds in a loop until sessionActive becomes false.\n" +
1261
+ "- Do NOT stop polling just because there are no new events — the tester is still active.\n" +
1262
+ "- When you see 'voice-transcription' events, the tester recorded a voice marker — read the transcription and react to it.\n" +
1263
+ "- When you see 'question-response-received' events, use get_tester_response to retrieve the full answer with images.\n" +
1264
+ "- When you see 'question-response-transcribed' events, the tester's voice response has been transcribed — read it for context.\n" +
1265
+ "- Marker events with hasPendingVoice=true will have their voiceTranscription field populated once transcription completes.\n" +
1266
+ "- The tester may set voice markers at any time to communicate with you — keep watching for them.\n" +
1267
+ "- If you need clarification, use ask_tester_question to show a toast notification to the tester.\n" +
1268
+ "- Only stop polling when sessionActive is false (the recording ended).",
1269
+ annotations: {
1270
+ readOnlyHint: true,
1271
+ destructiveHint: false,
1272
+ idempotentHint: true,
1273
+ openWorldHint: false,
1274
+ },
1275
+ inputSchema: z.object({
1276
+ since_seq: z
1277
+ .number()
1278
+ .optional()
1279
+ .describe("Return events after this sequence number. Omit for all buffered events."),
1280
+ }),
1254
1281
  },
1255
1282
  async ({ since_seq }) => {
1256
1283
  if (!liveSessionActive && liveEvents.length === 0) {
@@ -1273,7 +1300,11 @@ server.tool(
1273
1300
  }
1274
1301
  }
1275
1302
  const enrichedEvents = events.map((e) => {
1276
- if (e.eventType === "marker" && e.data?.markerId && transcriptionsByMarkerId.has(e.data.markerId)) {
1303
+ if (
1304
+ e.eventType === "marker" &&
1305
+ e.data?.markerId &&
1306
+ transcriptionsByMarkerId.has(e.data.markerId)
1307
+ ) {
1277
1308
  return {
1278
1309
  ...e,
1279
1310
  data: {
@@ -1306,8 +1337,7 @@ server.tool(
1306
1337
  },
1307
1338
  {
1308
1339
  tool: "get_live_screenshot",
1309
- description:
1310
- "Capture a screenshot of the tester's current browser tab.",
1340
+ description: "Capture a screenshot of the tester's current browser tab.",
1311
1341
  },
1312
1342
  ];
1313
1343
  if (pendingQuestions.length > 0) {
@@ -1319,7 +1349,10 @@ server.tool(
1319
1349
  }
1320
1350
  } else {
1321
1351
  // Session ended — include full interaction log and voice transcriptions
1322
- if (liveInteractions.length > 0 || liveEvents.some((e) => e.eventType === "voice-transcription")) {
1352
+ if (
1353
+ liveInteractions.length > 0 ||
1354
+ liveEvents.some((e) => e.eventType === "voice-transcription")
1355
+ ) {
1323
1356
  responseData.sessionEndedNote =
1324
1357
  "Session has ended. The interactionLog and voiceMarkerTranscriptions below contain all agent-tester exchanges from this session. " +
1325
1358
  "Include these in your analysis — they represent collaborative context between agent and tester. " +
@@ -1353,20 +1386,29 @@ server.tool(
1353
1386
  );
1354
1387
 
1355
1388
  // Tool: ask_tester_question
1356
- server.tool(
1389
+ server.registerTool(
1357
1390
  "ask_tester_question",
1358
- "Ask the tester a short, concise question during a live shadowing session. " +
1359
- "The question appears as a toast notification in their browser. " +
1360
- "The tester responds with a voice marker (Alt+Shift+M) and optional highlight captures (Alt+Shift+S). " +
1361
- "Use get_tester_response to check for their answer.",
1362
1391
  {
1363
- question: z
1364
- .string()
1365
- .max(200)
1366
- .describe(
1367
- "Short, concise question for the tester (max 200 chars). " +
1368
- "Example: 'Can you click the Save button again?' or 'Does the error appear with a different email?'",
1369
- ),
1392
+ description:
1393
+ "Ask the tester a short, concise question during a live shadowing session. " +
1394
+ "The question appears as a toast notification in their browser. " +
1395
+ "The tester responds with a voice marker (Alt+Shift+M) and optional highlight captures (Alt+Shift+S). " +
1396
+ "Use get_tester_response to check for their answer.",
1397
+ annotations: {
1398
+ readOnlyHint: false,
1399
+ destructiveHint: false,
1400
+ idempotentHint: false,
1401
+ openWorldHint: true,
1402
+ },
1403
+ inputSchema: z.object({
1404
+ question: z
1405
+ .string()
1406
+ .max(200)
1407
+ .describe(
1408
+ "Short, concise question for the tester (max 200 chars). " +
1409
+ "Example: 'Can you click the Save button again?' or 'Does the error appear with a different email?'",
1410
+ ),
1411
+ }),
1370
1412
  },
1371
1413
  async ({ question }) => {
1372
1414
  if (!liveSessionActive || !activeConnection) {
@@ -1425,16 +1467,25 @@ server.tool(
1425
1467
  );
1426
1468
 
1427
1469
  // Tool: get_tester_response
1428
- server.tool(
1470
+ server.registerTool(
1429
1471
  "get_tester_response",
1430
- "Check for the tester's response to a question asked during live shadowing. " +
1431
- "Returns the tester's screenshot, highlight captures, and transcribed voice note.\n\n" +
1432
- "IMPORTANT: The tester needs time to record their answer (10-60 seconds). " +
1433
- "Instead of calling this tool repeatedly, prefer polling watch_live_session — " +
1434
- "you will see a 'question-response-received' event when the response arrives. " +
1435
- "Then call this tool once to get the full response with transcription and images.",
1436
1472
  {
1437
- question_id: z.string().describe("The question ID returned by ask_tester_question."),
1473
+ description:
1474
+ "Check for the tester's response to a question asked during live shadowing. " +
1475
+ "Returns the tester's screenshot, highlight captures, and transcribed voice note.\n\n" +
1476
+ "IMPORTANT: The tester needs time to record their answer (10-60 seconds). " +
1477
+ "Instead of calling this tool repeatedly, prefer polling watch_live_session — " +
1478
+ "you will see a 'question-response-received' event when the response arrives. " +
1479
+ "Then call this tool once to get the full response with transcription and images.",
1480
+ annotations: {
1481
+ readOnlyHint: true,
1482
+ destructiveHint: false,
1483
+ idempotentHint: true,
1484
+ openWorldHint: false,
1485
+ },
1486
+ inputSchema: z.object({
1487
+ question_id: z.string().describe("The question ID returned by ask_tester_question."),
1488
+ }),
1438
1489
  },
1439
1490
  async ({ question_id }) => {
1440
1491
  const response = questionResponses.get(question_id);
@@ -1448,8 +1499,7 @@ server.tool(
1448
1499
  {
1449
1500
  type: "text",
1450
1501
  text: [
1451
- "No response yet." +
1452
- (waitingSec !== null ? ` Waiting for ${waitingSec}s.` : ""),
1502
+ "No response yet." + (waitingSec !== null ? ` Waiting for ${waitingSec}s.` : ""),
1453
1503
  "The tester may still be recording their voice answer.",
1454
1504
  "",
1455
1505
  "TIP: Instead of polling this tool, go back to polling watch_live_session — " +
@@ -1552,10 +1602,19 @@ server.tool(
1552
1602
  );
1553
1603
 
1554
1604
  // Tool: get_live_screenshot
1555
- server.tool(
1605
+ server.registerTool(
1556
1606
  "get_live_screenshot",
1557
- "Capture a screenshot of the tester's current browser tab during a live shadowing session.",
1558
- {},
1607
+ {
1608
+ description:
1609
+ "Capture a screenshot of the tester's current browser tab during a live shadowing session.",
1610
+ annotations: {
1611
+ readOnlyHint: true,
1612
+ destructiveHint: false,
1613
+ idempotentHint: true,
1614
+ openWorldHint: false,
1615
+ },
1616
+ inputSchema: z.object({}),
1617
+ },
1559
1618
  async () => {
1560
1619
  if (!liveSessionActive || !activeConnection) {
1561
1620
  return toolError("No active live shadowing session or extension not connected.");
@@ -1606,13 +1665,22 @@ server.tool(
1606
1665
  );
1607
1666
 
1608
1667
  // Tool: get_live_session_summary
1609
- server.tool(
1668
+ server.registerTool(
1610
1669
  "get_live_session_summary",
1611
- "Get a summary of the live shadowing session including all agent-tester interactions. " +
1612
- "Call this after the session ends (sessionActive becomes false) to get the complete interaction log " +
1613
- "for incorporating into your analysis. This includes all questions asked, tester responses, " +
1614
- "voice transcriptions, and voice marker transcriptions from the session.",
1615
- {},
1670
+ {
1671
+ description:
1672
+ "Get a summary of the live shadowing session including all agent-tester interactions. " +
1673
+ "Call this after the session ends (sessionActive becomes false) to get the complete interaction log " +
1674
+ "for incorporating into your analysis. This includes all questions asked, tester responses, " +
1675
+ "voice transcriptions, and voice marker transcriptions from the session.",
1676
+ annotations: {
1677
+ readOnlyHint: true,
1678
+ destructiveHint: false,
1679
+ idempotentHint: true,
1680
+ openWorldHint: false,
1681
+ },
1682
+ inputSchema: z.object({}),
1683
+ },
1616
1684
  async () => {
1617
1685
  if (!liveSessionMeta && liveInteractions.length === 0) {
1618
1686
  return toolError("No live session data available. Start a live shadowing session first.");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tracegist-mcp-bridge",
3
- "version": "0.2.8",
3
+ "version": "0.2.12",
4
4
  "description": "Local-first MCP bridge for reading and transcribing TraceGist package zips.",
5
5
  "type": "module",
6
6
  "bin": {