tracegist-mcp-bridge 0.2.7 → 0.2.11

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
@@ -89,6 +89,14 @@ const SECTION_HINTS = {
89
89
  "marker visual evidence": "Screenshot references for each marker",
90
90
  "environment at marker time": "Environment snapshot at a specific marker",
91
91
  "marker timeline logs (context window)": "Logs within the ±5 s marker window",
92
+ "webapp testing reproduction":
93
+ "Server setup, Python repro script, verification points, key selectors",
94
+ "tester intent summary": "Classified intent: specifications, issues found, observations",
95
+ "iterative context": "Previous session reference, verification checklist for build-test cycle",
96
+ "tier 0: quick summary": "Session overview, intent, reproduction command (~200 tokens)",
97
+ "tier 1: findings": "Marker timeline with structured findings (~500 tokens)",
98
+ "tier 2: full context": "Interaction timeline, network/console context, environment",
99
+ "tier 3: deep diagnostics": "Full session timelines (deep profile only)",
92
100
  };
93
101
 
94
102
  function getSectionHint(sectionName) {
@@ -928,7 +928,9 @@ async function transcribeLiveVoice(voiceBlobDataUrl) {
928
928
  const text = result.choices?.[0]?.message?.content?.trim();
929
929
  return text ? { transcription: text } : { error: "Empty transcription response" };
930
930
  } catch (err) {
931
- return { error: `OpenRouter transcription failed: ${err instanceof Error ? err.message : String(err)}` };
931
+ return {
932
+ error: `OpenRouter transcription failed: ${err instanceof Error ? err.message : String(err)}`,
933
+ };
932
934
  }
933
935
  }
934
936
 
@@ -950,16 +952,21 @@ async function transcribeLiveVoice(voiceBlobDataUrl) {
950
952
  await fs.rm(tmpDir, { recursive: true, force: true });
951
953
  }
952
954
  } catch (err) {
953
- return { error: `Whisper transcription failed: ${err instanceof Error ? err.message : String(err)}` };
955
+ return {
956
+ error: `Whisper transcription failed: ${err instanceof Error ? err.message : String(err)}`,
957
+ };
954
958
  }
955
959
  }
956
960
 
957
961
  return { error: "No transcription available (set OPENROUTER_API_KEY or install Whisper)" };
958
962
  }
959
963
 
960
- /** Push a live event into the ring buffer with auto-incrementing seq. */
964
+ /** Push a live event into the ring buffer with auto-incrementing seq.
965
+ * Seq is always higher than any existing event (including extension-originated ones). */
961
966
  function pushLiveEvent(eventType, data) {
962
- liveEventSeq++;
967
+ // Ensure our seq is always higher than the max seq in the buffer
968
+ const maxExistingSeq = liveEvents.length > 0 ? liveEvents[liveEvents.length - 1].seq : 0;
969
+ liveEventSeq = Math.max(liveEventSeq, maxExistingSeq) + 1;
963
970
  liveEvents.push({ seq: liveEventSeq, ts: Date.now(), eventType, data });
964
971
  if (liveEvents.length > LIVE_SHADOW_EVENT_BUFFER_SIZE) {
965
972
  const evictCount = Math.floor(LIVE_SHADOW_EVENT_BUFFER_SIZE * 0.25);
@@ -1045,6 +1052,7 @@ function handleExtensionMessage(msg) {
1045
1052
  startedAt: Date.now(),
1046
1053
  };
1047
1054
  liveEvents.length = 0;
1055
+ liveEventSeq = 0;
1048
1056
  pendingQuestions.length = 0;
1049
1057
  questionResponses.clear();
1050
1058
  liveInteractions.length = 0;
@@ -1089,6 +1097,54 @@ function handleExtensionMessage(msg) {
1089
1097
  highlightCount: msg.highlightCaptures?.length || 0,
1090
1098
  hint: "Use get_tester_response to retrieve the full response with transcription and images.",
1091
1099
  });
1100
+ // Eagerly transcribe the voice response and log the interaction immediately
1101
+ // (don't wait for get_tester_response to be called)
1102
+ if (msg.voiceBlobDataUrl) {
1103
+ transcribeLiveVoice(msg.voiceBlobDataUrl)
1104
+ .then((result) => {
1105
+ // Store transcription with the response for later retrieval
1106
+ const stored = questionResponses.get(msg.questionId);
1107
+ if (stored && !stored.dismissed) {
1108
+ stored.transcription = result.transcription || null;
1109
+ }
1110
+ // Log the interaction with transcription
1111
+ liveInteractions.push({
1112
+ type: "tester-response",
1113
+ questionId: msg.questionId,
1114
+ timestamp: msg.timestamp || Date.now(),
1115
+ transcription: result.transcription || null,
1116
+ screenshot: !!msg.screenshot,
1117
+ highlightCount: msg.highlightCaptures?.length || 0,
1118
+ });
1119
+ // Push transcription as a live event so polling agents see it
1120
+ pushLiveEvent("question-response-transcribed", {
1121
+ questionId: msg.questionId,
1122
+ transcription: result.transcription || null,
1123
+ error: result.error || undefined,
1124
+ });
1125
+ })
1126
+ .catch((err) => {
1127
+ console.error(`[${BRIDGE_NAME}] Question response transcription failed:`, err);
1128
+ liveInteractions.push({
1129
+ type: "tester-response",
1130
+ questionId: msg.questionId,
1131
+ timestamp: msg.timestamp || Date.now(),
1132
+ transcription: null,
1133
+ screenshot: !!msg.screenshot,
1134
+ highlightCount: msg.highlightCaptures?.length || 0,
1135
+ });
1136
+ });
1137
+ } else {
1138
+ // No voice — log interaction immediately
1139
+ liveInteractions.push({
1140
+ type: "tester-response",
1141
+ questionId: msg.questionId,
1142
+ timestamp: msg.timestamp || Date.now(),
1143
+ transcription: null,
1144
+ screenshot: !!msg.screenshot,
1145
+ highlightCount: msg.highlightCaptures?.length || 0,
1146
+ });
1147
+ }
1092
1148
  break;
1093
1149
  }
1094
1150
 
@@ -1188,8 +1244,11 @@ server.tool(
1188
1244
  "- You MUST keep calling this tool every 2-4 seconds in a loop until sessionActive becomes false.\n" +
1189
1245
  "- Do NOT stop polling just because there are no new events — the tester is still active.\n" +
1190
1246
  "- When you see 'voice-transcription' events, the tester recorded a voice marker — read the transcription and react to it.\n" +
1191
- "- When you see 'question-response-received' events, use get_tester_response to retrieve the full answer.\n" +
1247
+ "- When you see 'question-response-received' events, use get_tester_response to retrieve the full answer with images.\n" +
1248
+ "- When you see 'question-response-transcribed' events, the tester's voice response has been transcribed — read it for context.\n" +
1249
+ "- Marker events with hasPendingVoice=true will have their voiceTranscription field populated once transcription completes.\n" +
1192
1250
  "- The tester may set voice markers at any time to communicate with you — keep watching for them.\n" +
1251
+ "- If you need clarification, use ask_tester_question to show a toast notification to the tester.\n" +
1193
1252
  "- Only stop polling when sessionActive is false (the recording ended).",
1194
1253
  {
1195
1254
  since_seq: z
@@ -1209,12 +1268,37 @@ server.tool(
1209
1268
 
1210
1269
  const latestSeq = liveEvents.length > 0 ? liveEvents[liveEvents.length - 1].seq : 0;
1211
1270
 
1271
+ // Enrich marker events with their transcriptions (if available in the buffer)
1272
+ // so agents don't have to correlate separate marker + voice-transcription events
1273
+ const transcriptionsByMarkerId = new Map();
1274
+ for (const e of liveEvents) {
1275
+ if (e.eventType === "voice-transcription" && e.data?.markerId) {
1276
+ transcriptionsByMarkerId.set(e.data.markerId, e.data.transcription);
1277
+ }
1278
+ }
1279
+ const enrichedEvents = events.map((e) => {
1280
+ if (
1281
+ e.eventType === "marker" &&
1282
+ e.data?.markerId &&
1283
+ transcriptionsByMarkerId.has(e.data.markerId)
1284
+ ) {
1285
+ return {
1286
+ ...e,
1287
+ data: {
1288
+ ...e.data,
1289
+ voiceTranscription: transcriptionsByMarkerId.get(e.data.markerId),
1290
+ },
1291
+ };
1292
+ }
1293
+ return e;
1294
+ });
1295
+
1212
1296
  const responseData = {
1213
1297
  sessionActive: liveSessionActive,
1214
1298
  session: liveSessionMeta,
1215
- events,
1299
+ events: enrichedEvents,
1216
1300
  latestSeq,
1217
- eventCount: events.length,
1301
+ eventCount: enrichedEvents.length,
1218
1302
  };
1219
1303
 
1220
1304
  if (liveSessionActive) {
@@ -1230,8 +1314,7 @@ server.tool(
1230
1314
  },
1231
1315
  {
1232
1316
  tool: "get_live_screenshot",
1233
- description:
1234
- "Capture a screenshot of the tester's current browser tab.",
1317
+ description: "Capture a screenshot of the tester's current browser tab.",
1235
1318
  },
1236
1319
  ];
1237
1320
  if (pendingQuestions.length > 0) {
@@ -1241,12 +1324,26 @@ server.tool(
1241
1324
  waitingSince: Math.round((Date.now() - q.sentAt) / 1000) + "s ago",
1242
1325
  }));
1243
1326
  }
1244
- } else if (liveInteractions.length > 0) {
1245
- // Session ended — include full interaction log for the agent to incorporate
1246
- responseData.sessionEndedNote =
1247
- "Session has ended. The interactionLog below contains all agent-tester exchanges from this session. " +
1248
- "Include these in your analysis — they represent collaborative context between agent and tester.";
1249
- responseData.interactionLog = liveInteractions;
1327
+ } else {
1328
+ // Session ended — include full interaction log and voice transcriptions
1329
+ if (
1330
+ liveInteractions.length > 0 ||
1331
+ liveEvents.some((e) => e.eventType === "voice-transcription")
1332
+ ) {
1333
+ responseData.sessionEndedNote =
1334
+ "Session has ended. The interactionLog and voiceMarkerTranscriptions below contain all agent-tester exchanges from this session. " +
1335
+ "Include these in your analysis — they represent collaborative context between agent and tester. " +
1336
+ "You can also call get_live_session_summary for the complete summary.";
1337
+ responseData.interactionLog = liveInteractions;
1338
+ // Include all voice marker transcriptions
1339
+ responseData.voiceMarkerTranscriptions = liveEvents
1340
+ .filter((e) => e.eventType === "voice-transcription" && e.data?.transcription)
1341
+ .map((e) => ({
1342
+ timestamp: e.ts,
1343
+ markerId: e.data.markerId,
1344
+ transcription: e.data.transcription,
1345
+ }));
1346
+ }
1250
1347
  }
1251
1348
 
1252
1349
  // Always include interaction count so the agent knows exchanges happened
@@ -1361,8 +1458,7 @@ server.tool(
1361
1458
  {
1362
1459
  type: "text",
1363
1460
  text: [
1364
- "No response yet." +
1365
- (waitingSec !== null ? ` Waiting for ${waitingSec}s.` : ""),
1461
+ "No response yet." + (waitingSec !== null ? ` Waiting for ${waitingSec}s.` : ""),
1366
1462
  "The tester may still be recording their voice answer.",
1367
1463
  "",
1368
1464
  "TIP: Instead of polling this tool, go back to polling watch_live_session — " +
@@ -1428,33 +1524,30 @@ server.tool(
1428
1524
  }
1429
1525
  }
1430
1526
 
1431
- let transcriptionText = null;
1432
1527
  if (response.voiceBlobDataUrl) {
1433
- const voiceResult = await transcribeLiveVoice(response.voiceBlobDataUrl);
1434
- if (voiceResult.transcription) {
1435
- transcriptionText = voiceResult.transcription;
1528
+ // Use pre-transcribed result if available (from eager transcription on response arrival)
1529
+ if (response.transcription) {
1436
1530
  content.push({
1437
1531
  type: "text",
1438
- text: `Voice transcription:\n${voiceResult.transcription}`,
1532
+ text: `Voice transcription:\n${response.transcription}`,
1439
1533
  });
1440
1534
  } else {
1441
- content.push({
1442
- type: "text",
1443
- text: `Voice note is attached but could not be transcribed${voiceResult.error ? ` (${voiceResult.error})` : ""}. Set OPENROUTER_API_KEY or install local Whisper for transcription.`,
1444
- });
1535
+ // Fallback: transcribe now if eager transcription hasn't completed yet
1536
+ const voiceResult = await transcribeLiveVoice(response.voiceBlobDataUrl);
1537
+ if (voiceResult.transcription) {
1538
+ content.push({
1539
+ type: "text",
1540
+ text: `Voice transcription:\n${voiceResult.transcription}`,
1541
+ });
1542
+ } else {
1543
+ content.push({
1544
+ type: "text",
1545
+ text: `Voice note is attached but could not be transcribed${voiceResult.error ? ` (${voiceResult.error})` : ""}. Set OPENROUTER_API_KEY or install local Whisper for transcription.`,
1546
+ });
1547
+ }
1445
1548
  }
1446
1549
  }
1447
1550
 
1448
- // Log the interaction for the session history
1449
- liveInteractions.push({
1450
- type: "tester-response",
1451
- questionId: question_id,
1452
- timestamp: Date.now(),
1453
- transcription: transcriptionText,
1454
- screenshot: !!response.screenshot,
1455
- highlightCount: response.highlightCaptures?.length || 0,
1456
- });
1457
-
1458
1551
  content.push({
1459
1552
  type: "text",
1460
1553
  text: "\nIMPORTANT: Resume polling watch_live_session to continue watching the session.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tracegist-mcp-bridge",
3
- "version": "0.2.7",
3
+ "version": "0.2.11",
4
4
  "description": "Local-first MCP bridge for reading and transcribing TraceGist package zips.",
5
5
  "type": "module",
6
6
  "bin": {