tracegist-mcp-bridge 0.2.0 → 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/README.md +6 -6
- package/bin/lib.mjs +152 -0
- package/bin/tracegist-mcp-bridge.mjs +240 -218
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -48,13 +48,13 @@ Set `TRACEGIST_DIR` to change where the bridge looks for packages (defaults to `
|
|
|
48
48
|
|
|
49
49
|
Each session ZIP may include:
|
|
50
50
|
|
|
51
|
-
| Path
|
|
52
|
-
|
|
53
|
-
| `*-coding-agent-handoff.md`
|
|
54
|
-
| `*-manifest.json`
|
|
55
|
-
| `*-playwright-repro.spec.ts` | Auto-generated Playwright repro script skeleton
|
|
51
|
+
| Path | Description |
|
|
52
|
+
| ---------------------------- | -------------------------------------------------------------------------------------- |
|
|
53
|
+
| `*-coding-agent-handoff.md` | Structured handoff for the coding agent |
|
|
54
|
+
| `*-manifest.json` | Session metadata and file index |
|
|
55
|
+
| `*-playwright-repro.spec.ts` | Auto-generated Playwright repro script skeleton |
|
|
56
56
|
| `network/api-requests.jsonl` | API request/response bodies (opt-in, requires Deep Diagnostics + body capture enabled) |
|
|
57
|
-
| `markers/marker-NN-*/`
|
|
57
|
+
| `markers/marker-NN-*/` | Per-marker screenshots, voice notes, highlight captures |
|
|
58
58
|
|
|
59
59
|
## Prerequisites for transcription
|
|
60
60
|
|
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(
|
|
@@ -18,18 +30,13 @@ const { version: BRIDGE_VERSION } = JSON.parse(
|
|
|
18
30
|
function resolveDefaultDirectory() {
|
|
19
31
|
const envDir = process.env.TRACEGIST_DIR;
|
|
20
32
|
if (envDir) {
|
|
21
|
-
const resolved = envDir.startsWith("~")
|
|
22
|
-
? path.join(os.homedir(), envDir.slice(1))
|
|
23
|
-
: envDir;
|
|
33
|
+
const resolved = envDir.startsWith("~") ? path.join(os.homedir(), envDir.slice(1)) : envDir;
|
|
24
34
|
return path.resolve(resolved);
|
|
25
35
|
}
|
|
26
36
|
return path.join(os.homedir(), "Downloads");
|
|
27
37
|
}
|
|
28
38
|
|
|
29
39
|
const DEFAULT_DOWNLOADS_DIR = resolveDefaultDirectory();
|
|
30
|
-
const PACKAGE_SUFFIX = "-package.zip";
|
|
31
|
-
const HANDOFF_SUFFIXES = ["-coding-agent-handoff.md", "-cursor-handoff.md"];
|
|
32
|
-
const MAX_PREVIEW_CHARS = 12_000;
|
|
33
40
|
const ZIP_CACHE_MAX = 5;
|
|
34
41
|
const execFileAsync = promisify(execFile);
|
|
35
42
|
let whisperDependencyWarnings = [];
|
|
@@ -37,6 +44,9 @@ let whisperDependencyWarnings = [];
|
|
|
37
44
|
/** @type {Map<string, { zip: JSZip, entryNames: string[], mtimeMs: number }>} */
|
|
38
45
|
const zipCache = new Map();
|
|
39
46
|
|
|
47
|
+
/** @type {Map<string, { enrichedMarkdown: string, mtimeMs: number }>} */
|
|
48
|
+
const transcriptCache = new Map();
|
|
49
|
+
|
|
40
50
|
async function checkWhisperDependencies() {
|
|
41
51
|
const warnings = [];
|
|
42
52
|
|
|
@@ -72,15 +82,6 @@ async function checkWhisperDependencies() {
|
|
|
72
82
|
return warnings;
|
|
73
83
|
}
|
|
74
84
|
|
|
75
|
-
function formatExecError(err) {
|
|
76
|
-
if (err instanceof Error && err.message) return err.message;
|
|
77
|
-
return String(err);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function isTraceGistPackageFile(fileName) {
|
|
81
|
-
return fileName.startsWith("tracegist-") && fileName.endsWith(PACKAGE_SUFFIX);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
85
|
async function listTraceGistPackages(directory, limit) {
|
|
85
86
|
const entries = await fs.readdir(directory, { withFileTypes: true });
|
|
86
87
|
const zipNames = entries
|
|
@@ -128,7 +129,9 @@ async function readZipEntries(zipPath) {
|
|
|
128
129
|
}
|
|
129
130
|
|
|
130
131
|
async function tryReadManifest(zip) {
|
|
131
|
-
const manifestEntry = Object.values(zip.files).find((entry) =>
|
|
132
|
+
const manifestEntry = Object.values(zip.files).find((entry) =>
|
|
133
|
+
entry.name.endsWith("-manifest.json"),
|
|
134
|
+
);
|
|
132
135
|
if (!manifestEntry) return { manifest: null, manifestEntryName: null };
|
|
133
136
|
const manifestText = await manifestEntry.async("text");
|
|
134
137
|
try {
|
|
@@ -144,14 +147,11 @@ 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
|
-
const manifestNamedHandoff =
|
|
153
|
-
|
|
154
|
-
|
|
151
|
+
const manifestNamedHandoff =
|
|
152
|
+
typeof manifest?.handoffMarkdownFilename === "string"
|
|
153
|
+
? normalizeZipBaseName(manifest.handoffMarkdownFilename)
|
|
154
|
+
: null;
|
|
155
155
|
const byBaseName = new Map();
|
|
156
156
|
for (const name of entryNames) {
|
|
157
157
|
byBaseName.set(normalizeZipBaseName(name), name);
|
|
@@ -162,11 +162,12 @@ async function tryReadHandoffMarkdown(zip, entryNames, manifest = null) {
|
|
|
162
162
|
selectedEntryName = byBaseName.get(manifestNamedHandoff) || null;
|
|
163
163
|
}
|
|
164
164
|
if (!selectedEntryName) {
|
|
165
|
-
selectedEntryName =
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
165
|
+
selectedEntryName =
|
|
166
|
+
entryNames.find((name) => {
|
|
167
|
+
const baseName = normalizeZipBaseName(name);
|
|
168
|
+
if (HANDOFF_SUFFIXES.some((suffix) => baseName.endsWith(suffix))) return true;
|
|
169
|
+
return /handoff.*\.md$/i.test(baseName);
|
|
170
|
+
}) || null;
|
|
170
171
|
}
|
|
171
172
|
if (!selectedEntryName) {
|
|
172
173
|
return { handoffMarkdown: null, handoffEntryName: null, debugCandidates: [] };
|
|
@@ -179,100 +180,8 @@ async function tryReadHandoffMarkdown(zip, entryNames, manifest = null) {
|
|
|
179
180
|
return { handoffMarkdown, handoffEntryName: selectedEntryName, debugCandidates: [] };
|
|
180
181
|
}
|
|
181
182
|
|
|
182
|
-
function renderPackagesText(packages, directory) {
|
|
183
|
-
if (packages.length === 0) {
|
|
184
|
-
return [
|
|
185
|
-
`No TraceGist package zips found in ${directory}.`,
|
|
186
|
-
"Expected naming pattern: tracegist-...-package.zip",
|
|
187
|
-
].join("\n");
|
|
188
|
-
}
|
|
189
|
-
const lines = [`Found ${packages.length} TraceGist package(s) in ${directory}:`, ""];
|
|
190
|
-
for (const pkg of packages) {
|
|
191
|
-
lines.push(
|
|
192
|
-
`- ${pkg.name}`,
|
|
193
|
-
` path: ${pkg.path}`,
|
|
194
|
-
` size: ${pkg.sizeBytes} bytes`,
|
|
195
|
-
` modified: ${pkg.modifiedAt}`,
|
|
196
|
-
"",
|
|
197
|
-
);
|
|
198
|
-
}
|
|
199
|
-
return lines.join("\n");
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function truncateText(value, maxChars = MAX_PREVIEW_CHARS) {
|
|
203
|
-
if (value.length <= maxChars) return value;
|
|
204
|
-
return `${value.slice(0, maxChars)}\n\n... [truncated ${value.length - maxChars} chars]`;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function parseMarkdownSections(markdown) {
|
|
208
|
-
const sections = [];
|
|
209
|
-
const lines = markdown.split("\n");
|
|
210
|
-
let currentName = null;
|
|
211
|
-
let currentStart = 0;
|
|
212
|
-
|
|
213
|
-
for (let i = 0; i < lines.length; i++) {
|
|
214
|
-
if (lines[i].startsWith("## ")) {
|
|
215
|
-
if (currentName !== null) {
|
|
216
|
-
sections.push({
|
|
217
|
-
name: currentName,
|
|
218
|
-
content: lines.slice(currentStart, i).join("\n"),
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
currentName = lines[i].slice(3).trim();
|
|
222
|
-
currentStart = i;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
if (currentName !== null) {
|
|
227
|
-
sections.push({
|
|
228
|
-
name: currentName,
|
|
229
|
-
content: lines.slice(currentStart).join("\n"),
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
return sections;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function renderSectionToc(sections, zipPath) {
|
|
237
|
-
const totalChars = sections.reduce((sum, s) => sum + s.content.length, 0);
|
|
238
|
-
const lines = [
|
|
239
|
-
`Handoff document for ${path.basename(zipPath)} — ${sections.length} sections, ${totalChars.toLocaleString()} chars total.`,
|
|
240
|
-
"",
|
|
241
|
-
"| # | Section | Size |",
|
|
242
|
-
"|---|---------|------|",
|
|
243
|
-
];
|
|
244
|
-
for (let i = 0; i < sections.length; i++) {
|
|
245
|
-
lines.push(
|
|
246
|
-
`| ${i + 1} | ${sections[i].name} | ${sections[i].content.length.toLocaleString()} chars |`,
|
|
247
|
-
);
|
|
248
|
-
}
|
|
249
|
-
lines.push(
|
|
250
|
-
"",
|
|
251
|
-
'Pass `section` with a section name (e.g. "Marker Timeline") to retrieve its full content.',
|
|
252
|
-
);
|
|
253
|
-
return lines.join("\n");
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function looksLikeVoiceNote(entryName) {
|
|
257
|
-
const base = path.posix.basename(entryName).toLowerCase();
|
|
258
|
-
return /voice/.test(base) && /\.(webm|wav|mp3|m4a|ogg|flac)$/i.test(base);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
183
|
const WHISPER_CONCURRENCY = 2;
|
|
262
184
|
|
|
263
|
-
async function mapConcurrent(items, fn, concurrency) {
|
|
264
|
-
const results = new Array(items.length);
|
|
265
|
-
let i = 0;
|
|
266
|
-
const worker = async () => {
|
|
267
|
-
while (i < items.length) {
|
|
268
|
-
const idx = i++;
|
|
269
|
-
results[idx] = await fn(items[idx], idx);
|
|
270
|
-
}
|
|
271
|
-
};
|
|
272
|
-
await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, worker));
|
|
273
|
-
return results;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
185
|
async function transcribeWithLocalWhisper(audioPath, model, language) {
|
|
277
186
|
const args = [
|
|
278
187
|
"-m",
|
|
@@ -335,7 +244,9 @@ async function injectTranscriptsIntoMarkdown(markdown, zip, entryNames) {
|
|
|
335
244
|
transcriptByShortId.set(shortId, "[VOICE NOTE NOT FOUND IN ZIP]");
|
|
336
245
|
return;
|
|
337
246
|
}
|
|
338
|
-
const
|
|
247
|
+
const markerDir = path.join(tmpRoot, shortId);
|
|
248
|
+
await fs.mkdir(markerDir, { recursive: true });
|
|
249
|
+
const outputPath = path.join(markerDir, path.basename(entryName));
|
|
339
250
|
const data = await entry.async("nodebuffer");
|
|
340
251
|
await fs.writeFile(outputPath, data);
|
|
341
252
|
try {
|
|
@@ -356,10 +267,7 @@ async function injectTranscriptsIntoMarkdown(markdown, zip, entryNames) {
|
|
|
356
267
|
} else {
|
|
357
268
|
const hint = whisperDependencyWarnings.join(" | ");
|
|
358
269
|
for (const shortId of voiceByShortId.keys()) {
|
|
359
|
-
transcriptByShortId.set(
|
|
360
|
-
shortId,
|
|
361
|
-
`[TRANSCRIPTION REQUIRED — Whisper not available: ${hint}]`,
|
|
362
|
-
);
|
|
270
|
+
transcriptByShortId.set(shortId, `[TRANSCRIPTION REQUIRED — Whisper not available: ${hint}]`);
|
|
363
271
|
}
|
|
364
272
|
}
|
|
365
273
|
|
|
@@ -412,27 +320,58 @@ server.registerPrompt(
|
|
|
412
320
|
"Each package contains screenshots, voice notes, network logs, interaction events, and a",
|
|
413
321
|
"pre-generated handoff markdown file.",
|
|
414
322
|
"",
|
|
415
|
-
"##
|
|
323
|
+
"## Step 1 — Discover & orient",
|
|
416
324
|
"",
|
|
417
325
|
"1. `list_tracegist_packages` — find available packages (searches TRACEGIST_DIR env var, or ~/Downloads by default).",
|
|
418
|
-
"2. `get_tracegist_handoff_markdown` — call without a `section` parameter to get a table of contents",
|
|
419
|
-
" showing all sections
|
|
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"`).',
|
|
420
329
|
' Use `section: "all"` to load the full document at once (can be large for deep exports).',
|
|
421
330
|
" **Voice note transcripts are injected automatically** into the marker sections — no separate step needed.",
|
|
422
331
|
" If Whisper is unavailable, a placeholder is injected with setup instructions.",
|
|
423
|
-
"
|
|
424
|
-
"
|
|
425
|
-
"
|
|
426
|
-
"
|
|
427
|
-
"
|
|
428
|
-
"
|
|
429
|
-
"
|
|
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.",
|
|
430
369
|
"",
|
|
431
370
|
"## Key facts",
|
|
432
371
|
"",
|
|
433
|
-
"- **Overview vs. full handoff**: `get_tracegist_package_overview` returns
|
|
434
|
-
"
|
|
435
|
-
"
|
|
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.",
|
|
436
375
|
"- Voice notes answer the triage prompts shown to the tester during recording.",
|
|
437
376
|
" The first marker within 30 s of session start is a session context note (starting state, not a bug).",
|
|
438
377
|
"- Marker timestamps are relative to session start. Each marker has a ±5 s event window",
|
|
@@ -455,32 +394,41 @@ server.registerTool(
|
|
|
455
394
|
{
|
|
456
395
|
description:
|
|
457
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
|
+
},
|
|
458
403
|
inputSchema: z.object({
|
|
459
|
-
zipPath: z.string(),
|
|
460
|
-
model: z.string().optional(),
|
|
461
|
-
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."),
|
|
462
407
|
}),
|
|
463
408
|
},
|
|
464
409
|
async ({ zipPath, model = "base", language }) => {
|
|
410
|
+
try {
|
|
465
411
|
if (whisperDependencyWarnings.length > 0) {
|
|
466
412
|
return {
|
|
467
|
-
content: [
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
413
|
+
content: [
|
|
414
|
+
{
|
|
415
|
+
type: "text",
|
|
416
|
+
text: JSON.stringify(
|
|
417
|
+
{
|
|
418
|
+
zipPath,
|
|
419
|
+
model,
|
|
420
|
+
language: language || null,
|
|
421
|
+
transcriptions: [],
|
|
422
|
+
failures: [],
|
|
423
|
+
warning:
|
|
424
|
+
"Local Whisper dependencies are missing. Fix the dependency warnings and retry transcription.",
|
|
425
|
+
dependencyWarnings: whisperDependencyWarnings,
|
|
426
|
+
},
|
|
427
|
+
null,
|
|
428
|
+
2,
|
|
429
|
+
),
|
|
430
|
+
},
|
|
431
|
+
],
|
|
484
432
|
isError: true,
|
|
485
433
|
};
|
|
486
434
|
}
|
|
@@ -489,20 +437,22 @@ server.registerTool(
|
|
|
489
437
|
const voiceEntries = entryNames.filter((entryName) => looksLikeVoiceNote(entryName));
|
|
490
438
|
if (voiceEntries.length === 0) {
|
|
491
439
|
return {
|
|
492
|
-
content: [
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
440
|
+
content: [
|
|
441
|
+
{
|
|
442
|
+
type: "text",
|
|
443
|
+
text: JSON.stringify(
|
|
444
|
+
{
|
|
445
|
+
zipPath,
|
|
446
|
+
model,
|
|
447
|
+
language: language || null,
|
|
448
|
+
transcriptions: [],
|
|
449
|
+
warning: "No voice-note files found in package.",
|
|
450
|
+
},
|
|
451
|
+
null,
|
|
452
|
+
2,
|
|
453
|
+
),
|
|
454
|
+
},
|
|
455
|
+
],
|
|
506
456
|
};
|
|
507
457
|
}
|
|
508
458
|
|
|
@@ -518,7 +468,9 @@ server.registerTool(
|
|
|
518
468
|
failures.push({ entryName, error: "Entry missing from zip" });
|
|
519
469
|
continue;
|
|
520
470
|
}
|
|
521
|
-
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));
|
|
522
474
|
const data = await entry.async("nodebuffer");
|
|
523
475
|
await fs.writeFile(outputPath, data);
|
|
524
476
|
extracted.push({ entryName, outputPath });
|
|
@@ -558,24 +510,34 @@ server.registerTool(
|
|
|
558
510
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
559
511
|
...(transcriptions.length === 0 && failures.length > 0 ? { isError: true } : {}),
|
|
560
512
|
};
|
|
513
|
+
} catch (err) { return toolError(err); }
|
|
561
514
|
},
|
|
562
515
|
);
|
|
563
516
|
|
|
564
517
|
server.registerTool(
|
|
565
518
|
"list_tracegist_packages",
|
|
566
519
|
{
|
|
567
|
-
description:
|
|
520
|
+
description:
|
|
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
|
+
},
|
|
568
528
|
inputSchema: z.object({
|
|
569
|
-
directory: z.string().optional(),
|
|
570
|
-
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)."),
|
|
571
531
|
}),
|
|
572
532
|
},
|
|
573
533
|
async ({ directory, limit = 20 }) => {
|
|
534
|
+
try {
|
|
574
535
|
const searchDir = directory || DEFAULT_DOWNLOADS_DIR;
|
|
575
536
|
const packages = await listTraceGistPackages(searchDir, limit);
|
|
576
537
|
return {
|
|
577
538
|
content: [{ type: "text", text: renderPackagesText(packages, searchDir) }],
|
|
578
539
|
};
|
|
540
|
+
} catch (err) { return toolError(err); }
|
|
579
541
|
},
|
|
580
542
|
);
|
|
581
543
|
|
|
@@ -583,17 +545,28 @@ server.registerTool(
|
|
|
583
545
|
"get_tracegist_package_overview",
|
|
584
546
|
{
|
|
585
547
|
description:
|
|
586
|
-
"Quick overview of a TraceGist package: manifest
|
|
587
|
-
"
|
|
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. " +
|
|
588
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
|
+
},
|
|
589
557
|
inputSchema: z.object({
|
|
590
|
-
zipPath: z.string(),
|
|
558
|
+
zipPath: z.string().describe("Absolute path to the TraceGist package zip file."),
|
|
591
559
|
}),
|
|
592
560
|
},
|
|
593
561
|
async ({ zipPath }) => {
|
|
562
|
+
try {
|
|
594
563
|
const { zip, entryNames } = await readZipEntries(zipPath);
|
|
595
564
|
const { manifest, manifestEntryName } = await tryReadManifest(zip);
|
|
596
|
-
const {
|
|
565
|
+
const { handoffEntryName } = await tryReadHandoffMarkdown(
|
|
566
|
+
zip,
|
|
567
|
+
entryNames,
|
|
568
|
+
manifest,
|
|
569
|
+
);
|
|
597
570
|
|
|
598
571
|
const overview = {
|
|
599
572
|
zipPath,
|
|
@@ -601,13 +574,14 @@ server.registerTool(
|
|
|
601
574
|
manifestEntryName,
|
|
602
575
|
handoffEntryName,
|
|
603
576
|
manifest,
|
|
604
|
-
handoffPreview: handoffMarkdown ? truncateText(handoffMarkdown, MAX_PREVIEW_CHARS) : null,
|
|
605
577
|
entries: entryNames,
|
|
578
|
+
hint: "Use get_tracegist_handoff_markdown (no section param) for a table of contents, then load sections by name.",
|
|
606
579
|
};
|
|
607
580
|
|
|
608
581
|
return {
|
|
609
582
|
content: [{ type: "text", text: JSON.stringify(overview, null, 2) }],
|
|
610
583
|
};
|
|
584
|
+
} catch (err) { return toolError(err); }
|
|
611
585
|
},
|
|
612
586
|
);
|
|
613
587
|
|
|
@@ -621,12 +595,19 @@ server.registerTool(
|
|
|
621
595
|
"Without a `section` parameter, returns a table of contents with section names and sizes. " +
|
|
622
596
|
"With a `section` parameter, returns just that section's content. " +
|
|
623
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
|
+
},
|
|
624
604
|
inputSchema: z.object({
|
|
625
|
-
zipPath: z.string(),
|
|
626
|
-
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.'),
|
|
627
607
|
}),
|
|
628
608
|
},
|
|
629
609
|
async ({ zipPath, section }) => {
|
|
610
|
+
try {
|
|
630
611
|
const { zip, entryNames } = await readZipEntries(zipPath);
|
|
631
612
|
const { manifest } = await tryReadManifest(zip);
|
|
632
613
|
const { handoffMarkdown, handoffEntryName, debugCandidates } = await tryReadHandoffMarkdown(
|
|
@@ -639,30 +620,44 @@ server.registerTool(
|
|
|
639
620
|
.filter((name) => /handoff|coding-agent/i.test(path.posix.basename(name)))
|
|
640
621
|
.slice(0, 5);
|
|
641
622
|
return {
|
|
642
|
-
content: [
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
623
|
+
content: [
|
|
624
|
+
{
|
|
625
|
+
type: "text",
|
|
626
|
+
text: [
|
|
627
|
+
`No handoff markdown entry found in ${zipPath}.`,
|
|
628
|
+
manifest?.handoffMarkdownFilename
|
|
629
|
+
? `Manifest handoff filename: ${manifest.handoffMarkdownFilename}`
|
|
630
|
+
: "Manifest handoff filename: (missing)",
|
|
631
|
+
`Matching entry candidates: ${hintedEntries.length > 0 ? hintedEntries.join(", ") : "(none)"}`,
|
|
632
|
+
`Debug candidates: ${debugCandidates.length > 0 ? debugCandidates.join(", ") : "(none)"}`,
|
|
633
|
+
].join("\n"),
|
|
634
|
+
},
|
|
635
|
+
],
|
|
653
636
|
isError: true,
|
|
654
637
|
};
|
|
655
638
|
}
|
|
656
639
|
|
|
657
|
-
|
|
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
|
+
}
|
|
658
651
|
|
|
659
652
|
// Full document retrieval
|
|
660
653
|
if (section && section.toLowerCase() === "all") {
|
|
661
654
|
return {
|
|
662
|
-
content: [
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
655
|
+
content: [
|
|
656
|
+
{
|
|
657
|
+
type: "text",
|
|
658
|
+
text: `# Source: ${handoffEntryName}\n\n${enrichedMarkdown}`,
|
|
659
|
+
},
|
|
660
|
+
],
|
|
666
661
|
};
|
|
667
662
|
}
|
|
668
663
|
|
|
@@ -671,10 +666,12 @@ server.registerTool(
|
|
|
671
666
|
// No section requested — return table of contents
|
|
672
667
|
if (!section) {
|
|
673
668
|
return {
|
|
674
|
-
content: [
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
669
|
+
content: [
|
|
670
|
+
{
|
|
671
|
+
type: "text",
|
|
672
|
+
text: `# Source: ${handoffEntryName}\n\n${renderSectionToc(sections, zipPath)}`,
|
|
673
|
+
},
|
|
674
|
+
],
|
|
678
675
|
};
|
|
679
676
|
}
|
|
680
677
|
|
|
@@ -684,19 +681,24 @@ server.registerTool(
|
|
|
684
681
|
if (!match) {
|
|
685
682
|
const available = sections.map((s) => s.name).join(", ");
|
|
686
683
|
return {
|
|
687
|
-
content: [
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
684
|
+
content: [
|
|
685
|
+
{
|
|
686
|
+
type: "text",
|
|
687
|
+
text: `Section "${section}" not found. Available sections: ${available}`,
|
|
688
|
+
},
|
|
689
|
+
],
|
|
691
690
|
isError: true,
|
|
692
691
|
};
|
|
693
692
|
}
|
|
694
693
|
return {
|
|
695
|
-
content: [
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
694
|
+
content: [
|
|
695
|
+
{
|
|
696
|
+
type: "text",
|
|
697
|
+
text: match.content,
|
|
698
|
+
},
|
|
699
|
+
],
|
|
699
700
|
};
|
|
701
|
+
} catch (err) { return toolError(err); }
|
|
700
702
|
},
|
|
701
703
|
);
|
|
702
704
|
|
|
@@ -707,12 +709,19 @@ server.registerTool(
|
|
|
707
709
|
"Read a text file directly from inside a TraceGist package zip and return its content without writing to disk. " +
|
|
708
710
|
"Common uses: `network/api-requests.jsonl` for request/response bodies, or the Playwright repro script " +
|
|
709
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
|
+
},
|
|
710
718
|
inputSchema: z.object({
|
|
711
|
-
zipPath: z.string(),
|
|
712
|
-
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)."),
|
|
713
721
|
}),
|
|
714
722
|
},
|
|
715
723
|
async ({ zipPath, entryName }) => {
|
|
724
|
+
try {
|
|
716
725
|
const { zip } = await readZipEntries(zipPath);
|
|
717
726
|
const entry = zip.file(entryName);
|
|
718
727
|
if (!entry) {
|
|
@@ -725,6 +734,7 @@ server.registerTool(
|
|
|
725
734
|
return {
|
|
726
735
|
content: [{ type: "text", text }],
|
|
727
736
|
};
|
|
737
|
+
} catch (err) { return toolError(err); }
|
|
728
738
|
},
|
|
729
739
|
);
|
|
730
740
|
|
|
@@ -732,13 +742,20 @@ server.registerTool(
|
|
|
732
742
|
"extract_tracegist_package_file",
|
|
733
743
|
{
|
|
734
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
|
+
},
|
|
735
751
|
inputSchema: z.object({
|
|
736
|
-
zipPath: z.string(),
|
|
737
|
-
entryName: z.string(),
|
|
738
|
-
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."),
|
|
739
755
|
}),
|
|
740
756
|
},
|
|
741
757
|
async ({ zipPath, entryName, outputDirectory }) => {
|
|
758
|
+
try {
|
|
742
759
|
const { zip } = await readZipEntries(zipPath);
|
|
743
760
|
const entry = zip.file(entryName);
|
|
744
761
|
if (!entry) {
|
|
@@ -751,7 +768,9 @@ server.registerTool(
|
|
|
751
768
|
const outputPath = path.resolve(outDir, path.basename(entryName));
|
|
752
769
|
if (!outputPath.startsWith(outDir + path.sep) && outputPath !== outDir) {
|
|
753
770
|
return {
|
|
754
|
-
content: [
|
|
771
|
+
content: [
|
|
772
|
+
{ type: "text", text: `Refusing to write outside output directory: ${outputPath}` },
|
|
773
|
+
],
|
|
755
774
|
isError: true,
|
|
756
775
|
};
|
|
757
776
|
}
|
|
@@ -761,11 +780,14 @@ server.registerTool(
|
|
|
761
780
|
return {
|
|
762
781
|
content: [{ type: "text", text: `Extracted ${entryName} -> ${outputPath}` }],
|
|
763
782
|
};
|
|
783
|
+
} catch (err) { return toolError(err); }
|
|
764
784
|
},
|
|
765
785
|
);
|
|
766
786
|
|
|
767
787
|
whisperDependencyWarnings = await checkWhisperDependencies();
|
|
768
|
-
console.error(
|
|
788
|
+
console.error(
|
|
789
|
+
`[${BRIDGE_NAME}] Default package directory: ${DEFAULT_DOWNLOADS_DIR}${process.env.TRACEGIST_DIR ? " (from TRACEGIST_DIR)" : ""}`,
|
|
790
|
+
);
|
|
769
791
|
if (whisperDependencyWarnings.length > 0) {
|
|
770
792
|
for (const warning of whisperDependencyWarnings) {
|
|
771
793
|
console.error(`[${BRIDGE_NAME}] WARNING: ${warning}`);
|