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 +4 -0
- package/bin/lib.mjs +8 -0
- package/bin/tracegist-mcp-bridge.mjs +129 -36
- package/package.json +1 -1
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 {
|
|
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 {
|
|
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
|
-
|
|
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:
|
|
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
|
|
1245
|
-
// Session ended — include full interaction log
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
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
|
-
|
|
1434
|
-
if (
|
|
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${
|
|
1532
|
+
text: `Voice transcription:\n${response.transcription}`,
|
|
1439
1533
|
});
|
|
1440
1534
|
} else {
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
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.",
|