flow-tracer 0.2.0
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 +131 -0
- package/bin/flow-tracer.js +39 -0
- package/package.json +29 -0
- package/src/indexer.js +208 -0
- package/src/llm.js +297 -0
- package/src/mcp.js +229 -0
- package/src/public/index.html +286 -0
- package/src/server.js +275 -0
- package/src/summarizer.js +302 -0
package/src/llm.js
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM integration — supports both Claude CLI and Anthropic API.
|
|
3
|
+
*
|
|
4
|
+
* Mode selection:
|
|
5
|
+
* - If ANTHROPIC_API_KEY is set → uses Anthropic SDK (works for anyone)
|
|
6
|
+
* - Otherwise → uses `claude` CLI (works with Claude Code Pro subscription)
|
|
7
|
+
*
|
|
8
|
+
* Token optimization:
|
|
9
|
+
* - Follow-ups strip code blocks from history (saves ~80% tokens)
|
|
10
|
+
* - File selection uses cheaper model (Haiku via API, same CLI otherwise)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { spawn } from "child_process";
|
|
14
|
+
import { writeFileSync, unlinkSync, mkdirSync } from "fs";
|
|
15
|
+
import { join, dirname } from "path";
|
|
16
|
+
import crypto from "crypto";
|
|
17
|
+
|
|
18
|
+
const TMP_DIR = join(dirname(new URL(import.meta.url).pathname), "../.tmp");
|
|
19
|
+
mkdirSync(TMP_DIR, { recursive: true });
|
|
20
|
+
|
|
21
|
+
const API_KEY = process.env.ANTHROPIC_API_KEY;
|
|
22
|
+
const API_BASE_URL = process.env.ANTHROPIC_BASE_URL;
|
|
23
|
+
const FILE_SELECT_MODEL = process.env.FILE_SELECT_MODEL || "claude-haiku-4-5-20251001";
|
|
24
|
+
const ANALYSIS_MODEL = process.env.ANALYSIS_MODEL || "claude-sonnet-4-20250514";
|
|
25
|
+
|
|
26
|
+
if (API_KEY) {
|
|
27
|
+
console.log(`[llm] Using Anthropic API (file selection: ${FILE_SELECT_MODEL}, analysis: ${ANALYSIS_MODEL})`);
|
|
28
|
+
} else {
|
|
29
|
+
console.log("[llm] Using Claude CLI (no ANTHROPIC_API_KEY set)");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ── System Prompt ───────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
const SYSTEM_PROMPT = `You are a senior software architect who deeply understands codebases.
|
|
35
|
+
Your job is to analyze code from multiple repositories and answer questions about how code flows work.
|
|
36
|
+
|
|
37
|
+
CRITICAL — CROSS-REPO TRACING (this is the CORE purpose of this tool):
|
|
38
|
+
|
|
39
|
+
You are given code from MULTIPLE repositories. Your #1 job is to show how they CONNECT.
|
|
40
|
+
|
|
41
|
+
RULES:
|
|
42
|
+
1. ALWAYS start from the USER-FACING entry point — the page, route, or API endpoint that a user/browser/client calls first. Trace what happens on load (onMount, useEffect, init, etc.).
|
|
43
|
+
2. Trace the FULL call chain across repos. If Repo A calls an API in Repo B, show BOTH repos in the diagram with the actual endpoint/function name on the edge.
|
|
44
|
+
3. When you find a function that dispatches/delegates to multiple implementations (e.g. a switch/case, pattern match, or if-else on a type), show ALL branches — not just one or two.
|
|
45
|
+
4. EVERY repo that has relevant code MUST appear in the diagram. If code from 3 repos is provided, all 3 should be in the diagram.
|
|
46
|
+
5. Show the actual function/endpoint names that connect the repos — these are what developers search for.
|
|
47
|
+
|
|
48
|
+
MULTIPLE IMPLEMENTATIONS:
|
|
49
|
+
If a central function routes to multiple handlers based on type/config/platform:
|
|
50
|
+
- For generic questions ("how does X work?"), show ONE overview diagram with ALL branches, then a SEPARATE detailed diagram for each branch.
|
|
51
|
+
- For specific questions ("how does X work for Y?"), show that specific branch's full chain but still start from the entry point.
|
|
52
|
+
|
|
53
|
+
MULTIPLE ENTRY POINTS:
|
|
54
|
+
If multiple entry points converge on the same backend logic, show ALL entry points as separate starting nodes converging to the common function.
|
|
55
|
+
|
|
56
|
+
RESPONSE FORMAT (follow this strictly for EVERY flow):
|
|
57
|
+
1. ALWAYS include a Mermaid diagram for each flow. Use \`\`\`mermaid blocks.
|
|
58
|
+
2. IMMEDIATELY after EACH mermaid diagram, add a "### Step-by-Step" section that explains EVERY node and edge in the diagram:
|
|
59
|
+
- Number each step (Step 1, Step 2, ...)
|
|
60
|
+
- For each step: what function is called, which file it's in, what it does, and what it passes to the next step
|
|
61
|
+
- Include the actual file path (e.g. \`src/routes/api/order/+server.ts\`)
|
|
62
|
+
- Mention the data being passed between steps (request body, response shape, etc.)
|
|
63
|
+
3. Show API calls between services as labeled edges.
|
|
64
|
+
4. If you're unsure about something, say so — don't guess.
|
|
65
|
+
5. Keep explanations clear enough for a new developer to understand.
|
|
66
|
+
6. NEVER skip the step-by-step explanation. A diagram without explanation is useless.
|
|
67
|
+
|
|
68
|
+
CRITICAL mermaid syntax rules (violations cause render failures):
|
|
69
|
+
- Every \`end\` keyword MUST be alone on its own line
|
|
70
|
+
- Every \`end\` MUST have a matching \`subgraph\` — do NOT add an extra \`end\` at the bottom of the diagram. The number of \`end\` keywords must EXACTLY equal the number of \`subgraph\` keywords.
|
|
71
|
+
- NEVER put \`end\` and \`subgraph\` on the same line
|
|
72
|
+
- NEVER use \`%%\` comments — they break rendering
|
|
73
|
+
- Every node definition and every edge must be on its own line
|
|
74
|
+
- Node labels MUST be on a single line — use \`<br/>\` for line breaks, NOT actual newlines
|
|
75
|
+
- Keep edge labels under 50 characters
|
|
76
|
+
- NEVER put two edges or two statements on the same line
|
|
77
|
+
|
|
78
|
+
For mermaid diagrams:
|
|
79
|
+
- Use graph TD (top-down) for flows
|
|
80
|
+
- Use subgraph to group by repo/service
|
|
81
|
+
- Label edges with API paths, function calls, or data being passed
|
|
82
|
+
- Use different node shapes: ["UI"] for frontend, [["API"]] for backend, [("DB")] for data
|
|
83
|
+
|
|
84
|
+
Example (note: every statement on its own line, end on its own line):
|
|
85
|
+
\`\`\`mermaid
|
|
86
|
+
graph TD
|
|
87
|
+
subgraph Nimble["Nimble (Frontend)"]
|
|
88
|
+
A["Order Page<br/>src/routes/order/+page.svelte"]
|
|
89
|
+
B["API Route<br/>src/routes/api/order/+server.ts"]
|
|
90
|
+
end
|
|
91
|
+
subgraph Vayu["Vayu (Backend)"]
|
|
92
|
+
C[["Order Handler<br/>src/Order/Handler.hs"]]
|
|
93
|
+
D[("Orders DB")]
|
|
94
|
+
end
|
|
95
|
+
A -->|"onMount → getOrderStatus()"| B
|
|
96
|
+
B -->|"POST /api/v1/order"| C
|
|
97
|
+
C -->|"INSERT order"| D
|
|
98
|
+
\`\`\`
|
|
99
|
+
|
|
100
|
+
Always ground your answers in the actual code provided. Reference specific files.`;
|
|
101
|
+
|
|
102
|
+
// ── Claude CLI Backend ─────────────────────────────────
|
|
103
|
+
|
|
104
|
+
function callCLI(prompt) {
|
|
105
|
+
return new Promise((resolve, reject) => {
|
|
106
|
+
mkdirSync(TMP_DIR, { recursive: true });
|
|
107
|
+
const tmpFile = join(TMP_DIR, `prompt-${crypto.randomUUID()}.txt`);
|
|
108
|
+
writeFileSync(tmpFile, prompt);
|
|
109
|
+
|
|
110
|
+
const proc = spawn("sh", ["-c", `cat "${tmpFile}" | claude -p --output-format text`], {
|
|
111
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
let stdout = "", stderr = "";
|
|
115
|
+
proc.stdout.on("data", (d) => { stdout += d.toString(); });
|
|
116
|
+
proc.stderr.on("data", (d) => { stderr += d.toString(); });
|
|
117
|
+
|
|
118
|
+
proc.on("close", (code) => {
|
|
119
|
+
try { unlinkSync(tmpFile); } catch {}
|
|
120
|
+
if (code !== 0) reject(new Error(`Claude CLI error (code ${code}): ${stderr.slice(0, 300)}`));
|
|
121
|
+
else resolve(stdout.trim());
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
proc.on("error", (err) => {
|
|
125
|
+
try { unlinkSync(tmpFile); } catch {}
|
|
126
|
+
reject(new Error(`Failed to run claude CLI: ${err.message}`));
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ── Anthropic API Backend ──────────────────────────────
|
|
132
|
+
|
|
133
|
+
async function callAPI(prompt, model) {
|
|
134
|
+
// Dynamic import so the SDK is only loaded if API key is set
|
|
135
|
+
const { default: Anthropic } = await import("@anthropic-ai/sdk");
|
|
136
|
+
const client = new Anthropic({ apiKey: API_KEY, ...(API_BASE_URL && { baseURL: API_BASE_URL }) });
|
|
137
|
+
|
|
138
|
+
const response = await client.messages.create({
|
|
139
|
+
model,
|
|
140
|
+
max_tokens: 8192,
|
|
141
|
+
messages: [{ role: "user", content: prompt }],
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
return response.content
|
|
145
|
+
.filter((b) => b.type === "text")
|
|
146
|
+
.map((b) => b.text)
|
|
147
|
+
.join("\n");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ── Unified Call Function ──────────────────────────────
|
|
151
|
+
|
|
152
|
+
export async function callClaude(prompt, model = ANALYSIS_MODEL) {
|
|
153
|
+
const sizeKB = (prompt.length / 1024).toFixed(0);
|
|
154
|
+
console.log(`[llm] Sending ${sizeKB}KB to ${API_KEY ? model : "claude CLI"}`);
|
|
155
|
+
|
|
156
|
+
if (API_KEY) {
|
|
157
|
+
return callAPI(prompt, model);
|
|
158
|
+
}
|
|
159
|
+
return callCLI(prompt);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ── History Compression ────────────────────────────────
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Strip code blocks from a message to reduce history size.
|
|
166
|
+
* Keeps the text around them so the LLM remembers what was discussed.
|
|
167
|
+
*/
|
|
168
|
+
function compressHistoryMessage(content) {
|
|
169
|
+
if (typeof content !== "string") return content;
|
|
170
|
+
|
|
171
|
+
// Replace code blocks with a placeholder showing the file path
|
|
172
|
+
return content.replace(/\*\*([^*]+)\*\*:\n```[\s\S]*?```/g, "[code: $1]")
|
|
173
|
+
.replace(/```[\s\S]*?```/g, "[code block omitted]");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Compress conversation history for follow-ups.
|
|
178
|
+
* Keeps questions and answers readable but strips the large code context.
|
|
179
|
+
*/
|
|
180
|
+
function compressHistory(history) {
|
|
181
|
+
return history.map((msg) => {
|
|
182
|
+
if (msg.role === "user") {
|
|
183
|
+
const compressed = compressHistoryMessage(msg.content);
|
|
184
|
+
// For user messages that contain code context, keep only the question
|
|
185
|
+
if (compressed.length > 500) {
|
|
186
|
+
// Extract just the question part (after the --- separator)
|
|
187
|
+
const questionPart = msg.content.match(/\*\*Question:\*\*\s*(.*)/s);
|
|
188
|
+
if (questionPart) return { role: "user", content: questionPart[1].slice(0, 300) };
|
|
189
|
+
return { role: "user", content: compressed.slice(0, 500) };
|
|
190
|
+
}
|
|
191
|
+
return { role: "user", content: compressed };
|
|
192
|
+
}
|
|
193
|
+
// Keep assistant responses (they contain the analysis the user wants to build on)
|
|
194
|
+
// but strip any code blocks from them too
|
|
195
|
+
return { role: "assistant", content: compressHistoryMessage(msg.content) };
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ── Code Context Builder ───────────────────────────────
|
|
200
|
+
|
|
201
|
+
function buildCodeContext(codeFiles, repoInfos) {
|
|
202
|
+
let context = "## Repositories\n\n";
|
|
203
|
+
for (const info of repoInfos) {
|
|
204
|
+
context += `- **${info.name}**: ${info.type} (${info.languages.join(", ")}`;
|
|
205
|
+
if (info.frameworks.length) context += `, ${info.frameworks.join(", ")}`;
|
|
206
|
+
context += ")\n";
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
context += "\n## Relevant Code\n\n";
|
|
210
|
+
const byRepo = {};
|
|
211
|
+
for (const file of codeFiles) {
|
|
212
|
+
(byRepo[file.repo] ??= []).push(file);
|
|
213
|
+
}
|
|
214
|
+
for (const [repo, files] of Object.entries(byRepo)) {
|
|
215
|
+
context += `### ${repo}\n\n`;
|
|
216
|
+
for (const f of files) {
|
|
217
|
+
context += `**${f.file}**:\n\`\`\`\n${f.content}\n\`\`\`\n\n`;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return context;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ── Main Question Function ─────────────────────────────
|
|
224
|
+
|
|
225
|
+
export async function askQuestion(question, relevantCode, repoInfos, conversationHistory = []) {
|
|
226
|
+
const isFollowUp = conversationHistory.length > 0;
|
|
227
|
+
let fullPrompt;
|
|
228
|
+
|
|
229
|
+
if (isFollowUp) {
|
|
230
|
+
// OPTIMIZED: compress history to strip code blocks (~80% smaller)
|
|
231
|
+
const compressed = compressHistory(conversationHistory);
|
|
232
|
+
fullPrompt = `${SYSTEM_PROMPT}\n\n[Previous conversation — code was already analyzed]\n\n`;
|
|
233
|
+
for (const msg of compressed) {
|
|
234
|
+
const role = msg.role === "user" ? "User" : "Assistant";
|
|
235
|
+
fullPrompt += `${role}: ${msg.content}\n\n`;
|
|
236
|
+
}
|
|
237
|
+
fullPrompt += `User: ${question}`;
|
|
238
|
+
} else {
|
|
239
|
+
const codeContext = buildCodeContext(relevantCode, repoInfos);
|
|
240
|
+
fullPrompt = `${SYSTEM_PROMPT}\n\n${codeContext}\n\n---\n\n**Question:** ${question}`;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const answer = await callClaude(fullPrompt, ANALYSIS_MODEL);
|
|
244
|
+
|
|
245
|
+
// Store uncompressed history (compression happens on next follow-up)
|
|
246
|
+
const userMessage = isFollowUp
|
|
247
|
+
? question
|
|
248
|
+
: `${buildCodeContext(relevantCode, repoInfos)}\n\n---\n\n**Question:** ${question}`;
|
|
249
|
+
|
|
250
|
+
const updatedHistory = [
|
|
251
|
+
...conversationHistory,
|
|
252
|
+
{ role: "user", content: userMessage },
|
|
253
|
+
{ role: "assistant", content: answer },
|
|
254
|
+
];
|
|
255
|
+
|
|
256
|
+
return { answer, history: updatedHistory };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ── File Selection (LLM Pre-Pass) ──────────────────────
|
|
260
|
+
|
|
261
|
+
export async function selectFilesViaLLM(manifest, question) {
|
|
262
|
+
const prompt = `You are a code navigation expert. Select files needed to trace the code flow.
|
|
263
|
+
|
|
264
|
+
RULES:
|
|
265
|
+
1. Select 15-25 files maximum.
|
|
266
|
+
2. Start from entry points (routes, pages, HTTP handlers).
|
|
267
|
+
3. Follow call chain: entry → orchestrator → dispatcher → implementations.
|
|
268
|
+
4. Include files from EVERY repository involved.
|
|
269
|
+
5. Include ALL branch implementations if a dispatcher routes to multiple.
|
|
270
|
+
6. EXCLUDE utilities (logging, formatting, error helpers).
|
|
271
|
+
7. Order by importance.
|
|
272
|
+
|
|
273
|
+
Return ONLY a JSON array: [{"repo":"name","file":"path"},...]
|
|
274
|
+
|
|
275
|
+
## Files
|
|
276
|
+
${manifest}
|
|
277
|
+
|
|
278
|
+
## Question
|
|
279
|
+
${question}`;
|
|
280
|
+
|
|
281
|
+
// Use cheaper model for file selection when using API
|
|
282
|
+
const response = await callClaude(prompt, FILE_SELECT_MODEL);
|
|
283
|
+
|
|
284
|
+
const jsonMatch = response.match(/\[[\s\S]*\]/);
|
|
285
|
+
if (!jsonMatch) {
|
|
286
|
+
console.error("[llm-select] No JSON in response:", response.slice(0, 200));
|
|
287
|
+
return [];
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
292
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
293
|
+
} catch (err) {
|
|
294
|
+
console.error("[llm-select] JSON parse error:", err.message);
|
|
295
|
+
return [];
|
|
296
|
+
}
|
|
297
|
+
}
|
package/src/mcp.js
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FlowTracer MCP Server
|
|
3
|
+
*
|
|
4
|
+
* Exposes code flow tracing as MCP tools for Claude Code / Claude Desktop.
|
|
5
|
+
*
|
|
6
|
+
* Setup:
|
|
7
|
+
* claude mcp add flow-tracer -- npx flow-tracer
|
|
8
|
+
* # or
|
|
9
|
+
* claude mcp add flow-tracer -- node /path/to/flow-tracer/bin/flow-tracer.js
|
|
10
|
+
*
|
|
11
|
+
* Tools:
|
|
12
|
+
* - register_repos: Register repo paths to index
|
|
13
|
+
* - trace_flow: Ask a question about code flows
|
|
14
|
+
* - follow_up: Follow-up question on previous trace
|
|
15
|
+
* - list_repos: List registered repo groups
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
19
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
20
|
+
import { z } from "zod";
|
|
21
|
+
import { indexRepos, selectRelevantCode } from "./indexer.js";
|
|
22
|
+
import { askQuestion } from "./llm.js";
|
|
23
|
+
import { buildManifest } from "./summarizer.js";
|
|
24
|
+
|
|
25
|
+
// ── State (same as server.js but in-process) ───────────
|
|
26
|
+
|
|
27
|
+
const repoStore = new Map();
|
|
28
|
+
const sessions = new Map();
|
|
29
|
+
|
|
30
|
+
// ── MCP Server ──────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
const server = new McpServer({
|
|
33
|
+
name: "flow-tracer",
|
|
34
|
+
version: "0.2.0",
|
|
35
|
+
description: "Trace code flows across repos — get mermaid diagrams and step-by-step explanations",
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// ── Tool: register_repos ────────────────────────────────
|
|
39
|
+
|
|
40
|
+
server.tool(
|
|
41
|
+
"register_repos",
|
|
42
|
+
"Register local repo paths to index. Must be called before trace_flow.",
|
|
43
|
+
{
|
|
44
|
+
name: z.string().describe("Group name for this set of repos (e.g. 'my-platform')"),
|
|
45
|
+
paths: z.array(z.string()).describe("Array of absolute paths to repo directories"),
|
|
46
|
+
},
|
|
47
|
+
async ({ name, paths }) => {
|
|
48
|
+
try {
|
|
49
|
+
const indexed = indexRepos(paths);
|
|
50
|
+
const totalFiles = indexed.reduce((sum, r) => sum + r.stats.totalFiles, 0);
|
|
51
|
+
const manifest = buildManifest(indexed);
|
|
52
|
+
|
|
53
|
+
repoStore.set(name, { paths, indexed, manifest, indexedAt: new Date().toISOString() });
|
|
54
|
+
|
|
55
|
+
const repoSummary = indexed.map((r) =>
|
|
56
|
+
`${r.repo.name}: ${r.stats.totalFiles} files (${r.repo.languages.join(", ")})`
|
|
57
|
+
).join("\n");
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
content: [{
|
|
61
|
+
type: "text",
|
|
62
|
+
text: `Indexed ${paths.length} repos as "${name}" (${totalFiles} files total).\n\n${repoSummary}\n\nManifest: ${(manifest.length / 1024).toFixed(0)}KB\n\nYou can now use trace_flow with repos="${name}".`,
|
|
63
|
+
}],
|
|
64
|
+
};
|
|
65
|
+
} catch (err) {
|
|
66
|
+
return { content: [{ type: "text", text: `Error indexing: ${err.message}` }] };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
// ── Tool: trace_flow ────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
server.tool(
|
|
74
|
+
"trace_flow",
|
|
75
|
+
"Trace a code flow across repositories. Returns mermaid diagrams and step-by-step explanation. Call register_repos first.",
|
|
76
|
+
{
|
|
77
|
+
repos: z.string().describe("Repo group name (from register_repos)"),
|
|
78
|
+
question: z.string().describe("Question about the code flow (e.g. 'How does order creation work?', 'What happens when a user clicks checkout?')"),
|
|
79
|
+
},
|
|
80
|
+
async ({ repos: repoGroupName, question }) => {
|
|
81
|
+
const repoGroup = repoStore.get(repoGroupName);
|
|
82
|
+
if (!repoGroup) {
|
|
83
|
+
const available = [...repoStore.keys()];
|
|
84
|
+
return {
|
|
85
|
+
content: [{
|
|
86
|
+
type: "text",
|
|
87
|
+
text: `Repo group "${repoGroupName}" not found. ${available.length ? `Available: ${available.join(", ")}` : "Call register_repos first."}`,
|
|
88
|
+
}],
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const relevantCode = await selectRelevantCode(repoGroup.indexed, question, repoGroup.manifest);
|
|
94
|
+
const repoInfos = repoGroup.indexed.map((r) => r.repo);
|
|
95
|
+
const { answer, history } = await askQuestion(question, relevantCode, repoInfos);
|
|
96
|
+
|
|
97
|
+
// Store session for follow-ups
|
|
98
|
+
const sessionId = `session-${Date.now()}`;
|
|
99
|
+
sessions.set(sessionId, {
|
|
100
|
+
history,
|
|
101
|
+
repoGroup: repoGroupName,
|
|
102
|
+
lastQuestion: question,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Keep only last 5 sessions
|
|
106
|
+
if (sessions.size > 5) {
|
|
107
|
+
const oldest = sessions.keys().next().value;
|
|
108
|
+
sessions.delete(oldest);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
content: [{
|
|
113
|
+
type: "text",
|
|
114
|
+
text: `${answer}\n\n---\n_Session: ${sessionId} | Files analyzed: ${relevantCode.length} | Use follow_up tool to ask more._`,
|
|
115
|
+
}],
|
|
116
|
+
};
|
|
117
|
+
} catch (err) {
|
|
118
|
+
return { content: [{ type: "text", text: `Error: ${err.message}` }] };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
// ── Tool: follow_up ─────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
server.tool(
|
|
126
|
+
"follow_up",
|
|
127
|
+
"Ask a follow-up question about a previous trace. Uses conversation context from the last trace_flow call.",
|
|
128
|
+
{
|
|
129
|
+
question: z.string().describe("Follow-up question (e.g. 'What happens if payment fails?', 'Show me the refund flow')"),
|
|
130
|
+
session_id: z.string().optional().describe("Session ID from a previous trace_flow. If omitted, uses the most recent session."),
|
|
131
|
+
},
|
|
132
|
+
async ({ question, session_id }) => {
|
|
133
|
+
// Find session
|
|
134
|
+
let session;
|
|
135
|
+
if (session_id) {
|
|
136
|
+
session = sessions.get(session_id);
|
|
137
|
+
} else {
|
|
138
|
+
// Use most recent session
|
|
139
|
+
const entries = [...sessions.entries()];
|
|
140
|
+
if (entries.length > 0) {
|
|
141
|
+
[, session] = entries[entries.length - 1];
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!session) {
|
|
146
|
+
return {
|
|
147
|
+
content: [{
|
|
148
|
+
type: "text",
|
|
149
|
+
text: "No previous session found. Use trace_flow first to start a conversation.",
|
|
150
|
+
}],
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const repoGroup = repoStore.get(session.repoGroup);
|
|
155
|
+
if (!repoGroup) {
|
|
156
|
+
return { content: [{ type: "text", text: `Repo group "${session.repoGroup}" no longer registered.` }] };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
const repoInfos = repoGroup.indexed.map((r) => r.repo);
|
|
161
|
+
|
|
162
|
+
// Check if follow-up needs new code context
|
|
163
|
+
const additionalCode = await selectRelevantCode(repoGroup.indexed, question, repoGroup.manifest);
|
|
164
|
+
let enrichedQuestion = question;
|
|
165
|
+
|
|
166
|
+
const newFiles = additionalCode
|
|
167
|
+
.filter((c) => !session.history.some((m) =>
|
|
168
|
+
typeof m.content === "string" && m.content.includes(c.file)
|
|
169
|
+
))
|
|
170
|
+
.slice(0, 15);
|
|
171
|
+
|
|
172
|
+
if (newFiles.length > 0) {
|
|
173
|
+
let extra = "\n\n[Additional code context for this follow-up]\n\n";
|
|
174
|
+
for (const f of newFiles) {
|
|
175
|
+
extra += `**${f.repo}/${f.file}**:\n\`\`\`\n${f.content}\n\`\`\`\n\n`;
|
|
176
|
+
}
|
|
177
|
+
enrichedQuestion = extra + question;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const { answer, history } = await askQuestion(enrichedQuestion, [], repoInfos, session.history);
|
|
181
|
+
session.history = history;
|
|
182
|
+
session.lastQuestion = question;
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
content: [{
|
|
186
|
+
type: "text",
|
|
187
|
+
text: `${answer}\n\n---\n_Conversation: ${session.history.length} messages | Use follow_up for more questions._`,
|
|
188
|
+
}],
|
|
189
|
+
};
|
|
190
|
+
} catch (err) {
|
|
191
|
+
return { content: [{ type: "text", text: `Error: ${err.message}` }] };
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
// ── Tool: list_repos ────────────────────────────────────
|
|
197
|
+
|
|
198
|
+
server.tool(
|
|
199
|
+
"list_repos",
|
|
200
|
+
"List all registered repo groups and their contents.",
|
|
201
|
+
{},
|
|
202
|
+
async () => {
|
|
203
|
+
if (repoStore.size === 0) {
|
|
204
|
+
return {
|
|
205
|
+
content: [{
|
|
206
|
+
type: "text",
|
|
207
|
+
text: "No repos registered. Use register_repos to add repo paths.",
|
|
208
|
+
}],
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
let output = "# Registered Repo Groups\n\n";
|
|
213
|
+
for (const [name, data] of repoStore) {
|
|
214
|
+
output += `## ${name}\n`;
|
|
215
|
+
output += `Indexed: ${data.indexedAt}\n\n`;
|
|
216
|
+
for (const r of data.indexed) {
|
|
217
|
+
output += `- **${r.repo.name}**: ${r.stats.totalFiles} files (${r.repo.languages.join(", ")})\n`;
|
|
218
|
+
}
|
|
219
|
+
output += "\n";
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return { content: [{ type: "text", text: output }] };
|
|
223
|
+
}
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
// ── Start ───────────────────────────────────────────────
|
|
227
|
+
|
|
228
|
+
const transport = new StdioServerTransport();
|
|
229
|
+
await server.connect(transport);
|