tracegist-mcp-bridge 0.2.2 → 0.2.3
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/bin/lib.mjs +152 -0
- package/bin/tracegist-mcp-bridge.mjs +141 -141
- package/package.json +1 -1
package/bin/lib.mjs
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
const PACKAGE_SUFFIX = "-package.zip";
|
|
4
|
+
const HANDOFF_SUFFIXES = ["-coding-agent-handoff.md", "-cursor-handoff.md"];
|
|
5
|
+
|
|
6
|
+
export function isTraceGistPackageFile(fileName) {
|
|
7
|
+
return fileName.startsWith("tracegist-") && fileName.endsWith(PACKAGE_SUFFIX);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function normalizeZipBaseName(entryName) {
|
|
11
|
+
return path.posix.basename(entryName).toLowerCase();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function renderPackagesText(packages, directory) {
|
|
15
|
+
if (packages.length === 0) {
|
|
16
|
+
return [
|
|
17
|
+
`No TraceGist package zips found in ${directory}.`,
|
|
18
|
+
"Expected naming pattern: tracegist-...-package.zip",
|
|
19
|
+
].join("\n");
|
|
20
|
+
}
|
|
21
|
+
const lines = [`Found ${packages.length} TraceGist package(s) in ${directory}:`, ""];
|
|
22
|
+
for (const pkg of packages) {
|
|
23
|
+
lines.push(
|
|
24
|
+
`- ${pkg.name}`,
|
|
25
|
+
` path: ${pkg.path}`,
|
|
26
|
+
` size: ${pkg.sizeBytes} bytes`,
|
|
27
|
+
` modified: ${pkg.modifiedAt}`,
|
|
28
|
+
"",
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
return lines.join("\n");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function parseMarkdownSections(markdown) {
|
|
35
|
+
const sections = [];
|
|
36
|
+
const lines = markdown.split("\n");
|
|
37
|
+
let currentName = null;
|
|
38
|
+
let currentStart = 0;
|
|
39
|
+
|
|
40
|
+
for (let i = 0; i < lines.length; i++) {
|
|
41
|
+
if (lines[i].startsWith("## ")) {
|
|
42
|
+
if (currentName !== null) {
|
|
43
|
+
sections.push({
|
|
44
|
+
name: currentName,
|
|
45
|
+
content: lines.slice(currentStart, i).join("\n"),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
currentName = lines[i].slice(3).trim();
|
|
49
|
+
currentStart = i;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (currentName !== null) {
|
|
54
|
+
sections.push({
|
|
55
|
+
name: currentName,
|
|
56
|
+
content: lines.slice(currentStart).join("\n"),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return sections;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Brief purpose hints for known handoff sections.
|
|
65
|
+
* Keys are matched case-insensitively against section names.
|
|
66
|
+
* Unknown sections get a generic hint so the TOC is never incomplete.
|
|
67
|
+
*/
|
|
68
|
+
const SECTION_HINTS = {
|
|
69
|
+
"session triage": "Tester summary: expected vs actual, severity",
|
|
70
|
+
"agent guidance": "Suggested investigation focus areas",
|
|
71
|
+
"session context note": "Starting state before the bug (not a bug itself)",
|
|
72
|
+
"notable anomalies": "Pre-flagged JS errors, failed requests, warnings",
|
|
73
|
+
"marker timeline": "Timestamped markers with ±5 s event windows — start here for analysis",
|
|
74
|
+
"user interaction timeline": "Full click/fill/navigation sequence — key for reproduction",
|
|
75
|
+
"session context": "Page URL, title, session metadata",
|
|
76
|
+
"session environment": "Browser, viewport, OS — match for reproduction",
|
|
77
|
+
"package files": "Paths to Playwright script, manifest, network bodies",
|
|
78
|
+
"marker-to-file mapping": "Which screenshots/voice files belong to each marker",
|
|
79
|
+
"backend log correlation": "Absolute timestamps for server-side log alignment",
|
|
80
|
+
"full session console timeline": "All console output (deep exports only)",
|
|
81
|
+
"full session network timeline": "All network requests (deep exports only)",
|
|
82
|
+
"full session network timeline (third-party only)": "Third-party network calls (deep exports only)",
|
|
83
|
+
"full session timelines": "Placeholder when deep timelines are omitted",
|
|
84
|
+
"context": "AI-generated analysis context (when available)",
|
|
85
|
+
"tracegist agent-processed context": "LLM-processed summary of the session",
|
|
86
|
+
"tester intent": "What the tester was trying to accomplish",
|
|
87
|
+
"handover tasks for coding agent": "Specific tasks the tester wants the agent to do",
|
|
88
|
+
"marker visual evidence": "Screenshot references for each marker",
|
|
89
|
+
"environment at marker time": "Environment snapshot at a specific marker",
|
|
90
|
+
"marker timeline logs (context window)": "Logs within the ±5 s marker window",
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
function getSectionHint(sectionName) {
|
|
94
|
+
return SECTION_HINTS[sectionName.toLowerCase()] || "Additional section";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function renderSectionToc(sections, zipPath) {
|
|
98
|
+
const totalChars = sections.reduce((sum, s) => sum + s.content.length, 0);
|
|
99
|
+
const lines = [
|
|
100
|
+
`Handoff document for ${path.basename(zipPath)} — ${sections.length} sections, ${totalChars.toLocaleString()} chars total.`,
|
|
101
|
+
"",
|
|
102
|
+
"| # | Section | Size | Purpose |",
|
|
103
|
+
"|---|---------|------|---------|",
|
|
104
|
+
];
|
|
105
|
+
for (let i = 0; i < sections.length; i++) {
|
|
106
|
+
const hint = getSectionHint(sections[i].name);
|
|
107
|
+
lines.push(
|
|
108
|
+
`| ${i + 1} | ${sections[i].name} | ${sections[i].content.length.toLocaleString()} chars | ${hint} |`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
lines.push(
|
|
112
|
+
"",
|
|
113
|
+
"**Reading guide:** For analysis, start with Session Triage → Marker Timeline → Notable Anomalies.",
|
|
114
|
+
"For reproduction, start with Session Environment → User Interaction Timeline → Package Files (Playwright script path).",
|
|
115
|
+
"",
|
|
116
|
+
'Pass `section` with a section name (e.g. "Marker Timeline") to retrieve its full content.',
|
|
117
|
+
);
|
|
118
|
+
return lines.join("\n");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function looksLikeVoiceNote(entryName) {
|
|
122
|
+
const base = path.posix.basename(entryName).toLowerCase();
|
|
123
|
+
return /voice/.test(base) && /\.(webm|wav|mp3|m4a|ogg|flac)$/i.test(base);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function mapConcurrent(items, fn, concurrency) {
|
|
127
|
+
const results = new Array(items.length);
|
|
128
|
+
let i = 0;
|
|
129
|
+
const worker = async () => {
|
|
130
|
+
while (i < items.length) {
|
|
131
|
+
const idx = i++;
|
|
132
|
+
results[idx] = await fn(items[idx], idx);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, worker));
|
|
136
|
+
return results;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function toolError(err) {
|
|
140
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
141
|
+
return {
|
|
142
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
143
|
+
isError: true,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function formatExecError(err) {
|
|
148
|
+
if (err instanceof Error && err.message) return err.message;
|
|
149
|
+
return String(err);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export { HANDOFF_SUFFIXES };
|
|
@@ -9,6 +9,18 @@ import JSZip from "jszip";
|
|
|
9
9
|
import { z } from "zod";
|
|
10
10
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
11
11
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
12
|
+
import {
|
|
13
|
+
isTraceGistPackageFile,
|
|
14
|
+
normalizeZipBaseName,
|
|
15
|
+
renderPackagesText,
|
|
16
|
+
parseMarkdownSections,
|
|
17
|
+
renderSectionToc,
|
|
18
|
+
looksLikeVoiceNote,
|
|
19
|
+
mapConcurrent,
|
|
20
|
+
toolError,
|
|
21
|
+
formatExecError,
|
|
22
|
+
HANDOFF_SUFFIXES,
|
|
23
|
+
} from "./lib.mjs";
|
|
12
24
|
|
|
13
25
|
const BRIDGE_NAME = "tracegist-mcp-bridge";
|
|
14
26
|
const { version: BRIDGE_VERSION } = JSON.parse(
|
|
@@ -25,9 +37,6 @@ function resolveDefaultDirectory() {
|
|
|
25
37
|
}
|
|
26
38
|
|
|
27
39
|
const DEFAULT_DOWNLOADS_DIR = resolveDefaultDirectory();
|
|
28
|
-
const PACKAGE_SUFFIX = "-package.zip";
|
|
29
|
-
const HANDOFF_SUFFIXES = ["-coding-agent-handoff.md", "-cursor-handoff.md"];
|
|
30
|
-
const MAX_PREVIEW_CHARS = 12_000;
|
|
31
40
|
const ZIP_CACHE_MAX = 5;
|
|
32
41
|
const execFileAsync = promisify(execFile);
|
|
33
42
|
let whisperDependencyWarnings = [];
|
|
@@ -35,6 +44,9 @@ let whisperDependencyWarnings = [];
|
|
|
35
44
|
/** @type {Map<string, { zip: JSZip, entryNames: string[], mtimeMs: number }>} */
|
|
36
45
|
const zipCache = new Map();
|
|
37
46
|
|
|
47
|
+
/** @type {Map<string, { enrichedMarkdown: string, mtimeMs: number }>} */
|
|
48
|
+
const transcriptCache = new Map();
|
|
49
|
+
|
|
38
50
|
async function checkWhisperDependencies() {
|
|
39
51
|
const warnings = [];
|
|
40
52
|
|
|
@@ -70,15 +82,6 @@ async function checkWhisperDependencies() {
|
|
|
70
82
|
return warnings;
|
|
71
83
|
}
|
|
72
84
|
|
|
73
|
-
function formatExecError(err) {
|
|
74
|
-
if (err instanceof Error && err.message) return err.message;
|
|
75
|
-
return String(err);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function isTraceGistPackageFile(fileName) {
|
|
79
|
-
return fileName.startsWith("tracegist-") && fileName.endsWith(PACKAGE_SUFFIX);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
85
|
async function listTraceGistPackages(directory, limit) {
|
|
83
86
|
const entries = await fs.readdir(directory, { withFileTypes: true });
|
|
84
87
|
const zipNames = entries
|
|
@@ -144,10 +147,6 @@ async function tryReadManifest(zip) {
|
|
|
144
147
|
}
|
|
145
148
|
}
|
|
146
149
|
|
|
147
|
-
function normalizeZipBaseName(entryName) {
|
|
148
|
-
return path.posix.basename(entryName).toLowerCase();
|
|
149
|
-
}
|
|
150
|
-
|
|
151
150
|
async function tryReadHandoffMarkdown(zip, entryNames, manifest = null) {
|
|
152
151
|
const manifestNamedHandoff =
|
|
153
152
|
typeof manifest?.handoffMarkdownFilename === "string"
|
|
@@ -181,100 +180,8 @@ async function tryReadHandoffMarkdown(zip, entryNames, manifest = null) {
|
|
|
181
180
|
return { handoffMarkdown, handoffEntryName: selectedEntryName, debugCandidates: [] };
|
|
182
181
|
}
|
|
183
182
|
|
|
184
|
-
function renderPackagesText(packages, directory) {
|
|
185
|
-
if (packages.length === 0) {
|
|
186
|
-
return [
|
|
187
|
-
`No TraceGist package zips found in ${directory}.`,
|
|
188
|
-
"Expected naming pattern: tracegist-...-package.zip",
|
|
189
|
-
].join("\n");
|
|
190
|
-
}
|
|
191
|
-
const lines = [`Found ${packages.length} TraceGist package(s) in ${directory}:`, ""];
|
|
192
|
-
for (const pkg of packages) {
|
|
193
|
-
lines.push(
|
|
194
|
-
`- ${pkg.name}`,
|
|
195
|
-
` path: ${pkg.path}`,
|
|
196
|
-
` size: ${pkg.sizeBytes} bytes`,
|
|
197
|
-
` modified: ${pkg.modifiedAt}`,
|
|
198
|
-
"",
|
|
199
|
-
);
|
|
200
|
-
}
|
|
201
|
-
return lines.join("\n");
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function truncateText(value, maxChars = MAX_PREVIEW_CHARS) {
|
|
205
|
-
if (value.length <= maxChars) return value;
|
|
206
|
-
return `${value.slice(0, maxChars)}\n\n... [truncated ${value.length - maxChars} chars]`;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function parseMarkdownSections(markdown) {
|
|
210
|
-
const sections = [];
|
|
211
|
-
const lines = markdown.split("\n");
|
|
212
|
-
let currentName = null;
|
|
213
|
-
let currentStart = 0;
|
|
214
|
-
|
|
215
|
-
for (let i = 0; i < lines.length; i++) {
|
|
216
|
-
if (lines[i].startsWith("## ")) {
|
|
217
|
-
if (currentName !== null) {
|
|
218
|
-
sections.push({
|
|
219
|
-
name: currentName,
|
|
220
|
-
content: lines.slice(currentStart, i).join("\n"),
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
currentName = lines[i].slice(3).trim();
|
|
224
|
-
currentStart = i;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
if (currentName !== null) {
|
|
229
|
-
sections.push({
|
|
230
|
-
name: currentName,
|
|
231
|
-
content: lines.slice(currentStart).join("\n"),
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
return sections;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function renderSectionToc(sections, zipPath) {
|
|
239
|
-
const totalChars = sections.reduce((sum, s) => sum + s.content.length, 0);
|
|
240
|
-
const lines = [
|
|
241
|
-
`Handoff document for ${path.basename(zipPath)} — ${sections.length} sections, ${totalChars.toLocaleString()} chars total.`,
|
|
242
|
-
"",
|
|
243
|
-
"| # | Section | Size |",
|
|
244
|
-
"|---|---------|------|",
|
|
245
|
-
];
|
|
246
|
-
for (let i = 0; i < sections.length; i++) {
|
|
247
|
-
lines.push(
|
|
248
|
-
`| ${i + 1} | ${sections[i].name} | ${sections[i].content.length.toLocaleString()} chars |`,
|
|
249
|
-
);
|
|
250
|
-
}
|
|
251
|
-
lines.push(
|
|
252
|
-
"",
|
|
253
|
-
'Pass `section` with a section name (e.g. "Marker Timeline") to retrieve its full content.',
|
|
254
|
-
);
|
|
255
|
-
return lines.join("\n");
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function looksLikeVoiceNote(entryName) {
|
|
259
|
-
const base = path.posix.basename(entryName).toLowerCase();
|
|
260
|
-
return /voice/.test(base) && /\.(webm|wav|mp3|m4a|ogg|flac)$/i.test(base);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
183
|
const WHISPER_CONCURRENCY = 2;
|
|
264
184
|
|
|
265
|
-
async function mapConcurrent(items, fn, concurrency) {
|
|
266
|
-
const results = new Array(items.length);
|
|
267
|
-
let i = 0;
|
|
268
|
-
const worker = async () => {
|
|
269
|
-
while (i < items.length) {
|
|
270
|
-
const idx = i++;
|
|
271
|
-
results[idx] = await fn(items[idx], idx);
|
|
272
|
-
}
|
|
273
|
-
};
|
|
274
|
-
await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, worker));
|
|
275
|
-
return results;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
185
|
async function transcribeWithLocalWhisper(audioPath, model, language) {
|
|
279
186
|
const args = [
|
|
280
187
|
"-m",
|
|
@@ -337,7 +244,9 @@ async function injectTranscriptsIntoMarkdown(markdown, zip, entryNames) {
|
|
|
337
244
|
transcriptByShortId.set(shortId, "[VOICE NOTE NOT FOUND IN ZIP]");
|
|
338
245
|
return;
|
|
339
246
|
}
|
|
340
|
-
const
|
|
247
|
+
const markerDir = path.join(tmpRoot, shortId);
|
|
248
|
+
await fs.mkdir(markerDir, { recursive: true });
|
|
249
|
+
const outputPath = path.join(markerDir, path.basename(entryName));
|
|
341
250
|
const data = await entry.async("nodebuffer");
|
|
342
251
|
await fs.writeFile(outputPath, data);
|
|
343
252
|
try {
|
|
@@ -411,27 +320,58 @@ server.registerPrompt(
|
|
|
411
320
|
"Each package contains screenshots, voice notes, network logs, interaction events, and a",
|
|
412
321
|
"pre-generated handoff markdown file.",
|
|
413
322
|
"",
|
|
414
|
-
"##
|
|
323
|
+
"## Step 1 — Discover & orient",
|
|
415
324
|
"",
|
|
416
325
|
"1. `list_tracegist_packages` — find available packages (searches TRACEGIST_DIR env var, or ~/Downloads by default).",
|
|
417
|
-
"2. `get_tracegist_handoff_markdown` — call without a `section` parameter to get a table of contents",
|
|
418
|
-
|
|
326
|
+
"2. `get_tracegist_handoff_markdown` — call **without** a `section` parameter to get a table of contents (TOC)",
|
|
327
|
+
" showing all sections, their sizes, and a brief purpose hint for each section.",
|
|
328
|
+
' Then request specific sections by name (e.g. `section: "Marker Timeline"`).',
|
|
419
329
|
' Use `section: "all"` to load the full document at once (can be large for deep exports).',
|
|
420
330
|
" **Voice note transcripts are injected automatically** into the marker sections — no separate step needed.",
|
|
421
331
|
" If Whisper is unavailable, a placeholder is injected with setup instructions.",
|
|
422
|
-
"
|
|
423
|
-
"
|
|
424
|
-
"
|
|
425
|
-
"
|
|
426
|
-
"
|
|
427
|
-
"
|
|
428
|
-
"
|
|
332
|
+
"",
|
|
333
|
+
"## Step 2 — Decide: analyze-only vs. reproduce-first",
|
|
334
|
+
"",
|
|
335
|
+
"After reading the TOC, choose one of two paths:",
|
|
336
|
+
"",
|
|
337
|
+
"### Path A: Analyze and fix (no reproduction needed)",
|
|
338
|
+
"",
|
|
339
|
+
"Best when the bug is clear from logs/screenshots and you can identify the root cause from the trace alone.",
|
|
340
|
+
"",
|
|
341
|
+
'1. Read **Session Triage** — the tester\'s summary: what was expected, what happened, severity.',
|
|
342
|
+
'2. Read **Marker Timeline** — timestamped markers with ±5 s event windows (errors, network, interactions).',
|
|
343
|
+
'3. Read **Notable Anomalies** — pre-flagged JS errors, failed requests, and console warnings.',
|
|
344
|
+
'4. If needed: **Full Session Console/Network Timelines** (deep exports only), **Session Environment**.',
|
|
345
|
+
"5. Diagnose and fix directly in the codebase.",
|
|
346
|
+
"",
|
|
347
|
+
"### Path B: Reproduce first, then develop against the reproduction",
|
|
348
|
+
"",
|
|
349
|
+
"Best when you need to see the failure live, the bug is intermittent, or you want a test harness to develop against.",
|
|
350
|
+
"",
|
|
351
|
+
"1. `get_tracegist_package_overview` — get the manifest. Note `playwrightScriptPath`.",
|
|
352
|
+
"2. `read_tracegist_package_file({ zipPath, entryName: manifest.playwrightScriptPath })` — read the",
|
|
353
|
+
" auto-generated Playwright repro script (click/fill steps derived from the interaction log).",
|
|
354
|
+
'3. Read **Session Environment** — browser, viewport, URL, OS. Match your local setup.',
|
|
355
|
+
'4. Read **User Interaction Timeline** — the full sequence of user actions to understand the reproduction flow.',
|
|
356
|
+
"5. Extract and run the Playwright script:",
|
|
357
|
+
" - `extract_tracegist_package_file` to get the script + config to disk.",
|
|
358
|
+
" - The script needs authentication handled separately (storageState or global setup).",
|
|
359
|
+
" - `npx playwright test <script>.spec.ts` — adapt the script as needed, then develop your fix against it.",
|
|
360
|
+
"6. Once the bug reproduces, switch to Path A sections for root-cause analysis.",
|
|
361
|
+
"",
|
|
362
|
+
"## Additional tools",
|
|
363
|
+
"",
|
|
364
|
+
"- `read_tracegist_package_file` — read any text file directly from the ZIP without writing to disk.",
|
|
365
|
+
" - **Network bodies**: `network/api-requests.jsonl` has POST/PUT/PATCH bodies + response statuses.",
|
|
366
|
+
" - Also useful for the raw manifest JSON or any other text entry.",
|
|
367
|
+
"- `extract_tracegist_package_file` — extract a file to disk.",
|
|
368
|
+
" Use for: screenshots (PNG) and other binary files, or when a downstream tool needs a real file path.",
|
|
429
369
|
"",
|
|
430
370
|
"## Key facts",
|
|
431
371
|
"",
|
|
432
|
-
"- **Overview vs. full handoff**: `get_tracegist_package_overview` returns
|
|
433
|
-
"
|
|
434
|
-
"
|
|
372
|
+
"- **Overview vs. full handoff**: `get_tracegist_package_overview` returns only metadata (manifest + file list).",
|
|
373
|
+
" It does NOT include handoff content. Always use `get_tracegist_handoff_markdown`",
|
|
374
|
+
" (no section → TOC, then load sections by name) for the actual handoff document.",
|
|
435
375
|
"- Voice notes answer the triage prompts shown to the tester during recording.",
|
|
436
376
|
" The first marker within 30 s of session start is a session context note (starting state, not a bug).",
|
|
437
377
|
"- Marker timestamps are relative to session start. Each marker has a ±5 s event window",
|
|
@@ -454,13 +394,20 @@ server.registerTool(
|
|
|
454
394
|
{
|
|
455
395
|
description:
|
|
456
396
|
"Transcribe voice-note files inside a TraceGist package zip using local Python Whisper (no external API).",
|
|
397
|
+
annotations: {
|
|
398
|
+
readOnlyHint: true,
|
|
399
|
+
destructiveHint: false,
|
|
400
|
+
idempotentHint: true,
|
|
401
|
+
openWorldHint: false,
|
|
402
|
+
},
|
|
457
403
|
inputSchema: z.object({
|
|
458
|
-
zipPath: z.string(),
|
|
459
|
-
model: z.string().optional(),
|
|
460
|
-
language: z.string().optional(),
|
|
404
|
+
zipPath: z.string().describe("Absolute path to the TraceGist package zip file."),
|
|
405
|
+
model: z.string().optional().describe('Whisper model size (default: "base"). Options: tiny, base, small, medium, large.'),
|
|
406
|
+
language: z.string().optional().describe("ISO 639-1 language code (e.g. en, de, ja). Omit for auto-detection."),
|
|
461
407
|
}),
|
|
462
408
|
},
|
|
463
409
|
async ({ zipPath, model = "base", language }) => {
|
|
410
|
+
try {
|
|
464
411
|
if (whisperDependencyWarnings.length > 0) {
|
|
465
412
|
return {
|
|
466
413
|
content: [
|
|
@@ -521,7 +468,9 @@ server.registerTool(
|
|
|
521
468
|
failures.push({ entryName, error: "Entry missing from zip" });
|
|
522
469
|
continue;
|
|
523
470
|
}
|
|
524
|
-
const
|
|
471
|
+
const entryDir = path.join(tmpRoot, String(extracted.length));
|
|
472
|
+
await fs.mkdir(entryDir, { recursive: true });
|
|
473
|
+
const outputPath = path.join(entryDir, path.basename(entryName));
|
|
525
474
|
const data = await entry.async("nodebuffer");
|
|
526
475
|
await fs.writeFile(outputPath, data);
|
|
527
476
|
extracted.push({ entryName, outputPath });
|
|
@@ -561,6 +510,7 @@ server.registerTool(
|
|
|
561
510
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
562
511
|
...(transcriptions.length === 0 && failures.length > 0 ? { isError: true } : {}),
|
|
563
512
|
};
|
|
513
|
+
} catch (err) { return toolError(err); }
|
|
564
514
|
},
|
|
565
515
|
);
|
|
566
516
|
|
|
@@ -569,17 +519,25 @@ server.registerTool(
|
|
|
569
519
|
{
|
|
570
520
|
description:
|
|
571
521
|
"List TraceGist marker package zip files. Searches TRACEGIST_DIR (env var), falls back to ~/Downloads, or uses the given directory.",
|
|
522
|
+
annotations: {
|
|
523
|
+
readOnlyHint: true,
|
|
524
|
+
destructiveHint: false,
|
|
525
|
+
idempotentHint: true,
|
|
526
|
+
openWorldHint: false,
|
|
527
|
+
},
|
|
572
528
|
inputSchema: z.object({
|
|
573
|
-
directory: z.string().optional(),
|
|
574
|
-
limit: z.number().int().min(1).max(200).optional(),
|
|
529
|
+
directory: z.string().optional().describe("Directory to search for packages. Defaults to TRACEGIST_DIR or ~/Downloads."),
|
|
530
|
+
limit: z.number().int().min(1).max(200).optional().describe("Maximum number of packages to return (default: 20)."),
|
|
575
531
|
}),
|
|
576
532
|
},
|
|
577
533
|
async ({ directory, limit = 20 }) => {
|
|
534
|
+
try {
|
|
578
535
|
const searchDir = directory || DEFAULT_DOWNLOADS_DIR;
|
|
579
536
|
const packages = await listTraceGistPackages(searchDir, limit);
|
|
580
537
|
return {
|
|
581
538
|
content: [{ type: "text", text: renderPackagesText(packages, searchDir) }],
|
|
582
539
|
};
|
|
540
|
+
} catch (err) { return toolError(err); }
|
|
583
541
|
},
|
|
584
542
|
);
|
|
585
543
|
|
|
@@ -587,17 +545,24 @@ server.registerTool(
|
|
|
587
545
|
"get_tracegist_package_overview",
|
|
588
546
|
{
|
|
589
547
|
description:
|
|
590
|
-
"Quick overview of a TraceGist package: manifest
|
|
591
|
-
"
|
|
548
|
+
"Quick metadata overview of a TraceGist package: manifest and file list. " +
|
|
549
|
+
"Does NOT include handoff content — call get_tracegist_handoff_markdown (no section param → table of contents, then load sections by name) for the actual handoff. " +
|
|
592
550
|
"The manifest includes playwrightScriptPath — use read_tracegist_package_file with that path to get the Playwright repro script.",
|
|
551
|
+
annotations: {
|
|
552
|
+
readOnlyHint: true,
|
|
553
|
+
destructiveHint: false,
|
|
554
|
+
idempotentHint: true,
|
|
555
|
+
openWorldHint: false,
|
|
556
|
+
},
|
|
593
557
|
inputSchema: z.object({
|
|
594
|
-
zipPath: z.string(),
|
|
558
|
+
zipPath: z.string().describe("Absolute path to the TraceGist package zip file."),
|
|
595
559
|
}),
|
|
596
560
|
},
|
|
597
561
|
async ({ zipPath }) => {
|
|
562
|
+
try {
|
|
598
563
|
const { zip, entryNames } = await readZipEntries(zipPath);
|
|
599
564
|
const { manifest, manifestEntryName } = await tryReadManifest(zip);
|
|
600
|
-
const {
|
|
565
|
+
const { handoffEntryName } = await tryReadHandoffMarkdown(
|
|
601
566
|
zip,
|
|
602
567
|
entryNames,
|
|
603
568
|
manifest,
|
|
@@ -609,13 +574,14 @@ server.registerTool(
|
|
|
609
574
|
manifestEntryName,
|
|
610
575
|
handoffEntryName,
|
|
611
576
|
manifest,
|
|
612
|
-
handoffPreview: handoffMarkdown ? truncateText(handoffMarkdown, MAX_PREVIEW_CHARS) : null,
|
|
613
577
|
entries: entryNames,
|
|
578
|
+
hint: "Use get_tracegist_handoff_markdown (no section param) for a table of contents, then load sections by name.",
|
|
614
579
|
};
|
|
615
580
|
|
|
616
581
|
return {
|
|
617
582
|
content: [{ type: "text", text: JSON.stringify(overview, null, 2) }],
|
|
618
583
|
};
|
|
584
|
+
} catch (err) { return toolError(err); }
|
|
619
585
|
},
|
|
620
586
|
);
|
|
621
587
|
|
|
@@ -629,12 +595,19 @@ server.registerTool(
|
|
|
629
595
|
"Without a `section` parameter, returns a table of contents with section names and sizes. " +
|
|
630
596
|
"With a `section` parameter, returns just that section's content. " +
|
|
631
597
|
'Use section name "all" to retrieve the full document.',
|
|
598
|
+
annotations: {
|
|
599
|
+
readOnlyHint: true,
|
|
600
|
+
destructiveHint: false,
|
|
601
|
+
idempotentHint: true,
|
|
602
|
+
openWorldHint: false,
|
|
603
|
+
},
|
|
632
604
|
inputSchema: z.object({
|
|
633
|
-
zipPath: z.string(),
|
|
634
|
-
section: z.string().optional(),
|
|
605
|
+
zipPath: z.string().describe("Absolute path to the TraceGist package zip file."),
|
|
606
|
+
section: z.string().optional().describe('Section name from the TOC (e.g. "Marker Timeline"), or "all" for the full document. Omit to get table of contents.'),
|
|
635
607
|
}),
|
|
636
608
|
},
|
|
637
609
|
async ({ zipPath, section }) => {
|
|
610
|
+
try {
|
|
638
611
|
const { zip, entryNames } = await readZipEntries(zipPath);
|
|
639
612
|
const { manifest } = await tryReadManifest(zip);
|
|
640
613
|
const { handoffMarkdown, handoffEntryName, debugCandidates } = await tryReadHandoffMarkdown(
|
|
@@ -664,7 +637,17 @@ server.registerTool(
|
|
|
664
637
|
};
|
|
665
638
|
}
|
|
666
639
|
|
|
667
|
-
|
|
640
|
+
// Cache transcription results to avoid re-running Whisper on repeated calls
|
|
641
|
+
const cachedZip = zipCache.get(zipPath);
|
|
642
|
+
const currentMtime = cachedZip?.mtimeMs ?? 0;
|
|
643
|
+
const cached = transcriptCache.get(zipPath);
|
|
644
|
+
let enrichedMarkdown;
|
|
645
|
+
if (cached && cached.mtimeMs === currentMtime) {
|
|
646
|
+
enrichedMarkdown = cached.enrichedMarkdown;
|
|
647
|
+
} else {
|
|
648
|
+
enrichedMarkdown = await injectTranscriptsIntoMarkdown(handoffMarkdown, zip, entryNames);
|
|
649
|
+
transcriptCache.set(zipPath, { enrichedMarkdown, mtimeMs: currentMtime });
|
|
650
|
+
}
|
|
668
651
|
|
|
669
652
|
// Full document retrieval
|
|
670
653
|
if (section && section.toLowerCase() === "all") {
|
|
@@ -715,6 +698,7 @@ server.registerTool(
|
|
|
715
698
|
},
|
|
716
699
|
],
|
|
717
700
|
};
|
|
701
|
+
} catch (err) { return toolError(err); }
|
|
718
702
|
},
|
|
719
703
|
);
|
|
720
704
|
|
|
@@ -725,12 +709,19 @@ server.registerTool(
|
|
|
725
709
|
"Read a text file directly from inside a TraceGist package zip and return its content without writing to disk. " +
|
|
726
710
|
"Common uses: `network/api-requests.jsonl` for request/response bodies, or the Playwright repro script " +
|
|
727
711
|
"(its path is in the manifest's `playwrightScriptPath` field, returned by get_tracegist_package_overview).",
|
|
712
|
+
annotations: {
|
|
713
|
+
readOnlyHint: true,
|
|
714
|
+
destructiveHint: false,
|
|
715
|
+
idempotentHint: true,
|
|
716
|
+
openWorldHint: false,
|
|
717
|
+
},
|
|
728
718
|
inputSchema: z.object({
|
|
729
|
-
zipPath: z.string(),
|
|
730
|
-
entryName: z.string(),
|
|
719
|
+
zipPath: z.string().describe("Absolute path to the TraceGist package zip file."),
|
|
720
|
+
entryName: z.string().describe("Path of the file inside the zip (e.g. network/api-requests.jsonl)."),
|
|
731
721
|
}),
|
|
732
722
|
},
|
|
733
723
|
async ({ zipPath, entryName }) => {
|
|
724
|
+
try {
|
|
734
725
|
const { zip } = await readZipEntries(zipPath);
|
|
735
726
|
const entry = zip.file(entryName);
|
|
736
727
|
if (!entry) {
|
|
@@ -743,6 +734,7 @@ server.registerTool(
|
|
|
743
734
|
return {
|
|
744
735
|
content: [{ type: "text", text }],
|
|
745
736
|
};
|
|
737
|
+
} catch (err) { return toolError(err); }
|
|
746
738
|
},
|
|
747
739
|
);
|
|
748
740
|
|
|
@@ -750,13 +742,20 @@ server.registerTool(
|
|
|
750
742
|
"extract_tracegist_package_file",
|
|
751
743
|
{
|
|
752
744
|
description: "Extract one file from a TraceGist package zip to a local directory.",
|
|
745
|
+
annotations: {
|
|
746
|
+
readOnlyHint: false,
|
|
747
|
+
destructiveHint: false,
|
|
748
|
+
idempotentHint: true,
|
|
749
|
+
openWorldHint: false,
|
|
750
|
+
},
|
|
753
751
|
inputSchema: z.object({
|
|
754
|
-
zipPath: z.string(),
|
|
755
|
-
entryName: z.string(),
|
|
756
|
-
outputDirectory: z.string().optional(),
|
|
752
|
+
zipPath: z.string().describe("Absolute path to the TraceGist package zip file."),
|
|
753
|
+
entryName: z.string().describe("Path of the file inside the zip to extract."),
|
|
754
|
+
outputDirectory: z.string().optional().describe("Directory to write the file to. Defaults to the zip file's directory."),
|
|
757
755
|
}),
|
|
758
756
|
},
|
|
759
757
|
async ({ zipPath, entryName, outputDirectory }) => {
|
|
758
|
+
try {
|
|
760
759
|
const { zip } = await readZipEntries(zipPath);
|
|
761
760
|
const entry = zip.file(entryName);
|
|
762
761
|
if (!entry) {
|
|
@@ -781,6 +780,7 @@ server.registerTool(
|
|
|
781
780
|
return {
|
|
782
781
|
content: [{ type: "text", text: `Extracted ${entryName} -> ${outputPath}` }],
|
|
783
782
|
};
|
|
783
|
+
} catch (err) { return toolError(err); }
|
|
784
784
|
},
|
|
785
785
|
);
|
|
786
786
|
|