@esankhan3/code-search-mcp 0.4.2 → 0.4.4
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/dist/server.js +1 -1
- package/dist/tools/graph.js +177 -241
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -224,7 +224,7 @@ async function autoIndex(ctx) {
|
|
|
224
224
|
try {
|
|
225
225
|
const kbPath = getKnowledgeBasePath(ctx.projectName);
|
|
226
226
|
const hasLanceDB = existsSync(join(kbPath, "lancedb"));
|
|
227
|
-
const hasGraph = existsSync(join(kbPath, "system_graph_v2.json"));
|
|
227
|
+
const hasGraph = existsSync(join(kbPath, "system_graph.sqlite")) || existsSync(join(kbPath, "system_graph_v2.json"));
|
|
228
228
|
if (hasLanceDB && hasGraph) {
|
|
229
229
|
ctx.indexReady = true;
|
|
230
230
|
console.error(`[code-search-mcp] Index loaded for "${ctx.projectName}"`);
|
package/dist/tools/graph.js
CHANGED
|
@@ -5,7 +5,8 @@ import {
|
|
|
5
5
|
loadAllProfiles,
|
|
6
6
|
loadProfile,
|
|
7
7
|
getAllChanges,
|
|
8
|
-
getChangedFilesList
|
|
8
|
+
getChangedFilesList,
|
|
9
|
+
openSystemGraphStore
|
|
9
10
|
} from "@esankhan3/anvil-knowledge-core";
|
|
10
11
|
const GRAPH_TOOL_NAMES = [
|
|
11
12
|
"get_repo_graph",
|
|
@@ -19,43 +20,19 @@ const GRAPH_TOOL_NAMES = [
|
|
|
19
20
|
"search_graph",
|
|
20
21
|
"detect_changes"
|
|
21
22
|
];
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
function nodeLabel(node) {
|
|
33
|
-
return node.attributes?.label ?? node.key.split("::").slice(2).join("::") ?? node.key;
|
|
34
|
-
}
|
|
35
|
-
const edgeType = (e) => e.attributes?.type ?? e.attributes?.relation;
|
|
36
|
-
function resolveEntityNodes(nodes, name, repo, fuzzy = false) {
|
|
37
|
-
const inRepo = (key) => !repo || key.startsWith(repo + "::");
|
|
38
|
-
if (fuzzy) {
|
|
39
|
-
return nodes.filter((n) => inRepo(n.key) && ((n.attributes?.label ?? "").includes(name) || n.key.includes(name))).map((n) => n.key);
|
|
40
|
-
}
|
|
41
|
-
return nodes.filter((n) => inRepo(n.key) && (n.attributes?.label === name || n.key.endsWith("::" + name))).map((n) => n.key);
|
|
42
|
-
}
|
|
43
|
-
function buildAdjacency(edges, direction) {
|
|
44
|
-
const adj = /* @__PURE__ */ new Map();
|
|
45
|
-
const add = (a, b) => {
|
|
46
|
-
if (!adj.has(a)) adj.set(a, /* @__PURE__ */ new Set());
|
|
47
|
-
adj.get(a).add(b);
|
|
23
|
+
const repoOf = (key) => key.split("::")[0] ?? "";
|
|
24
|
+
function makeNeighborGetter(store, direction) {
|
|
25
|
+
const cache = /* @__PURE__ */ new Map();
|
|
26
|
+
return (key) => {
|
|
27
|
+
let v = cache.get(key);
|
|
28
|
+
if (v === void 0) {
|
|
29
|
+
v = store.neighborsOf(key, direction);
|
|
30
|
+
cache.set(key, v);
|
|
31
|
+
}
|
|
32
|
+
return v;
|
|
48
33
|
};
|
|
49
|
-
for (const e of edges) {
|
|
50
|
-
if (!e.source || !e.target) continue;
|
|
51
|
-
if (edgeType(e) === "contains") continue;
|
|
52
|
-
if ((e.attributes?.confidence ?? 0.8) < 0.7) continue;
|
|
53
|
-
if (direction === "callees" || direction === "both") add(e.source, e.target);
|
|
54
|
-
if (direction === "callers" || direction === "both") add(e.target, e.source);
|
|
55
|
-
}
|
|
56
|
-
return adj;
|
|
57
34
|
}
|
|
58
|
-
function shortestPath(
|
|
35
|
+
function shortestPath(getNeighbors, fromKeys, toKeys, maxDepth) {
|
|
59
36
|
const seed = fromKeys.find((k) => toKeys.has(k));
|
|
60
37
|
if (seed) return [seed];
|
|
61
38
|
const visited = new Set(fromKeys);
|
|
@@ -63,7 +40,7 @@ function shortestPath(adj, fromKeys, toKeys, maxDepth) {
|
|
|
63
40
|
for (let depth = 0; depth < maxDepth && frontier.length > 0; depth++) {
|
|
64
41
|
const next = [];
|
|
65
42
|
for (const path of frontier) {
|
|
66
|
-
for (const nb of
|
|
43
|
+
for (const nb of getNeighbors(path[path.length - 1])) {
|
|
67
44
|
if (visited.has(nb)) continue;
|
|
68
45
|
if (toKeys.has(nb)) return [...path, nb];
|
|
69
46
|
visited.add(nb);
|
|
@@ -74,14 +51,14 @@ function shortestPath(adj, fromKeys, toKeys, maxDepth) {
|
|
|
74
51
|
}
|
|
75
52
|
return null;
|
|
76
53
|
}
|
|
77
|
-
function reachable(
|
|
54
|
+
function reachable(getNeighbors, fromKeys, maxDepth, cap = 50) {
|
|
78
55
|
const visited = new Set(fromKeys);
|
|
79
56
|
const out = [];
|
|
80
57
|
let frontier = [...fromKeys];
|
|
81
58
|
for (let depth = 1; depth <= maxDepth && frontier.length > 0; depth++) {
|
|
82
59
|
const next = [];
|
|
83
60
|
for (const k of frontier) {
|
|
84
|
-
for (const nb of
|
|
61
|
+
for (const nb of getNeighbors(k)) {
|
|
85
62
|
if (visited.has(nb)) continue;
|
|
86
63
|
visited.add(nb);
|
|
87
64
|
out.push({ key: nb, depth });
|
|
@@ -219,14 +196,14 @@ function registerGraphTools() {
|
|
|
219
196
|
}
|
|
220
197
|
async function handleGraphTool(name, args, ctx) {
|
|
221
198
|
if (!GRAPH_TOOL_NAMES.includes(name)) return null;
|
|
199
|
+
const text = (t) => ({ content: [{ type: "text", text: t }] });
|
|
200
|
+
const NO_GRAPH = "No system graph found. Build KB first.";
|
|
222
201
|
try {
|
|
223
202
|
const kbPath = getKnowledgeBasePath(ctx.projectName);
|
|
224
203
|
if (name === "get_repo_graph") {
|
|
225
204
|
const repo = args.repo;
|
|
226
205
|
const graphPath = join(kbPath, repo, "graph.json");
|
|
227
|
-
if (!existsSync(graphPath)) {
|
|
228
|
-
return { content: [{ type: "text", text: `No graph found for repo "${repo}"` }] };
|
|
229
|
-
}
|
|
206
|
+
if (!existsSync(graphPath)) return text(`No graph found for repo "${repo}"`);
|
|
230
207
|
const graph = JSON.parse(readFileSync(graphPath, "utf-8"));
|
|
231
208
|
const summary = `# ${repo} AST Graph
|
|
232
209
|
|
|
@@ -237,140 +214,132 @@ async function handleGraphTool(name, args, ctx) {
|
|
|
237
214
|
${(graph.nodes ?? []).slice(0, 50).map((n) => `- \`${n.id}\` (${n.type})`).join("\n")}
|
|
238
215
|
|
|
239
216
|
${graph.nodes?.length > 50 ? `... and ${graph.nodes.length - 50} more` : ""}`;
|
|
240
|
-
return
|
|
217
|
+
return text(summary);
|
|
241
218
|
}
|
|
242
219
|
if (name === "get_cross_repo_edges") {
|
|
243
|
-
const
|
|
244
|
-
if (!
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
const
|
|
250
|
-
return
|
|
251
|
-
});
|
|
252
|
-
if (crossRepo.length === 0) {
|
|
253
|
-
return { content: [{ type: "text", text: repo ? `No cross-repo edges found for "${repo}"` : "No cross-repo edges found" }] };
|
|
254
|
-
}
|
|
255
|
-
const text = crossRepo.slice(0, 50).map(
|
|
256
|
-
(e) => `- ${e.source.split("::")[0]} \u2192 ${e.target.split("::")[0]} (${edgeType(e) ?? "edge"})`
|
|
257
|
-
).join("\n");
|
|
258
|
-
return { content: [{ type: "text", text: `# Cross-Repo Edges${repo ? ` for ${repo}` : ""}
|
|
220
|
+
const store = await openSystemGraphStore(kbPath);
|
|
221
|
+
if (!store) return text(NO_GRAPH);
|
|
222
|
+
try {
|
|
223
|
+
const repo = args.repo;
|
|
224
|
+
const { edges, total } = store.crossRepoEdges(repo, 50);
|
|
225
|
+
if (total === 0) return text(repo ? `No cross-repo edges found for "${repo}"` : "No cross-repo edges found");
|
|
226
|
+
const body = edges.map((e) => `- ${repoOf(e.source)} \u2192 ${repoOf(e.target)} (${e.type ?? "edge"})`).join("\n");
|
|
227
|
+
return text(`# Cross-Repo Edges${repo ? ` for ${repo}` : ""}
|
|
259
228
|
|
|
260
|
-
${
|
|
229
|
+
${total} edges found:
|
|
261
230
|
|
|
262
|
-
${
|
|
231
|
+
${body}${total > edges.length ? `
|
|
263
232
|
|
|
264
|
-
... and ${
|
|
233
|
+
... and ${total - edges.length} more` : ""}`);
|
|
234
|
+
} finally {
|
|
235
|
+
store.close();
|
|
236
|
+
}
|
|
265
237
|
}
|
|
266
238
|
if (name === "find_callers" || name === "find_dependencies") {
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
239
|
+
const store = await openSystemGraphStore(kbPath);
|
|
240
|
+
if (!store) return text("No system graph found.");
|
|
241
|
+
try {
|
|
242
|
+
const funcName = args.function;
|
|
243
|
+
const repoFilter = args.repo;
|
|
244
|
+
const fuzzy = args.fuzzy === true;
|
|
245
|
+
const matched = store.resolveNodes(funcName, repoFilter, fuzzy);
|
|
246
|
+
if (matched.length === 0) {
|
|
247
|
+
return text(`No entity found matching "${funcName}"${repoFilter ? ` in ${repoFilter}` : ""}. Try fuzzy:true for substring matching.`);
|
|
248
|
+
}
|
|
249
|
+
const keys = matched.map((m) => m.key);
|
|
250
|
+
const unique = name === "find_callers" ? store.callers(keys, 30) : store.dependencies(keys, 30);
|
|
251
|
+
const direction = name === "find_callers" ? "Callers of" : "Dependencies of";
|
|
252
|
+
if (unique.length === 0) {
|
|
253
|
+
return text(`# ${direction} "${funcName}"
|
|
281
254
|
|
|
282
|
-
Matched ${
|
|
283
|
-
|
|
284
|
-
|
|
255
|
+
Matched ${matched.length} entit${matched.length === 1 ? "y" : "ies"}, but no ${name === "find_callers" ? "callers" : "dependencies"} found.`);
|
|
256
|
+
}
|
|
257
|
+
return text(`# ${direction} "${funcName}"
|
|
285
258
|
|
|
286
259
|
${unique.length} found:
|
|
287
|
-
${unique.map((r) => `- \`${r}\``).join("\n")}`
|
|
260
|
+
${unique.map((r) => `- \`${r}\``).join("\n")}`);
|
|
261
|
+
} finally {
|
|
262
|
+
store.close();
|
|
263
|
+
}
|
|
288
264
|
}
|
|
289
265
|
if (name === "impact_analysis") {
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
const
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
].join("\n");
|
|
316
|
-
return { content: [{ type: "text", text }] };
|
|
266
|
+
const store = await openSystemGraphStore(kbPath);
|
|
267
|
+
if (!store) return text("No system graph found.");
|
|
268
|
+
try {
|
|
269
|
+
const file = args.file;
|
|
270
|
+
const repo = args.repo;
|
|
271
|
+
const entity = args.entity;
|
|
272
|
+
const fileKeys = store.nodesInFiles(repo, [file], entity);
|
|
273
|
+
const { edges: dependents, total, repos } = store.dependents(fileKeys, 30);
|
|
274
|
+
const body = [
|
|
275
|
+
`# Impact Analysis: ${repo}/${file}${entity ? `::${entity}` : ""}`,
|
|
276
|
+
"",
|
|
277
|
+
`## Entities in scope: ${fileKeys.length}`,
|
|
278
|
+
...fileKeys.slice(0, 20).map((k) => `- \`${k}\``),
|
|
279
|
+
"",
|
|
280
|
+
`## Dependents: ${total} edges from ${repos.length} repos`,
|
|
281
|
+
...dependents.map((e) => `- \`${e.source}\` \u2192 \`${e.target}\` (${e.type ?? "edge"})`),
|
|
282
|
+
total > dependents.length ? `
|
|
283
|
+
... and ${total - dependents.length} more` : "",
|
|
284
|
+
"",
|
|
285
|
+
`## Affected repos: ${repos.join(", ") || "none"}`
|
|
286
|
+
].join("\n");
|
|
287
|
+
return text(body);
|
|
288
|
+
} finally {
|
|
289
|
+
store.close();
|
|
290
|
+
}
|
|
317
291
|
}
|
|
318
292
|
if (name === "trace_path") {
|
|
319
|
-
const
|
|
320
|
-
if (!
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
return { content: [{ type: "text", text: `No path from "${from}" to "${to}" within ${maxDepth} hops (direction: ${direction}).` }] };
|
|
341
|
-
}
|
|
342
|
-
const arrow = direction === "callers" ? " \u2190 " : " \u2192 ";
|
|
343
|
-
const rendered = path.map((k) => labelOf.get(k) ?? k).join(arrow);
|
|
344
|
-
return { content: [{ type: "text", text: `# Path (${path.length - 1} hops)
|
|
293
|
+
const store = await openSystemGraphStore(kbPath);
|
|
294
|
+
if (!store) return text("No system graph found.");
|
|
295
|
+
try {
|
|
296
|
+
const from = args.from;
|
|
297
|
+
const to = args.to;
|
|
298
|
+
const repo = args.repo;
|
|
299
|
+
const fuzzy = args.fuzzy === true;
|
|
300
|
+
const direction = args.direction ?? "callees";
|
|
301
|
+
const maxDepth = Math.max(1, Math.min(10, args.maxDepth || 4));
|
|
302
|
+
const fromKeys = store.resolveNodes(from, repo, fuzzy).map((m) => m.key);
|
|
303
|
+
if (fromKeys.length === 0) return text(`No entity found matching "${from}". Try fuzzy:true.`);
|
|
304
|
+
const getNeighbors = makeNeighborGetter(store, direction);
|
|
305
|
+
if (to) {
|
|
306
|
+
const toKeys = new Set(store.resolveNodes(to, repo, fuzzy).map((m) => m.key));
|
|
307
|
+
if (toKeys.size === 0) return text(`No entity found matching target "${to}". Try fuzzy:true.`);
|
|
308
|
+
const path = shortestPath(getNeighbors, fromKeys, toKeys, maxDepth);
|
|
309
|
+
if (!path) return text(`No path from "${from}" to "${to}" within ${maxDepth} hops (direction: ${direction}).`);
|
|
310
|
+
const labelOf2 = store.labelsOf(path);
|
|
311
|
+
const arrow = direction === "callers" ? " \u2190 " : " \u2192 ";
|
|
312
|
+
const rendered = path.map((k) => labelOf2.get(k) ?? k).join(arrow);
|
|
313
|
+
return text(`# Path (${path.length - 1} hops)
|
|
345
314
|
|
|
346
315
|
${rendered}
|
|
347
316
|
|
|
348
|
-
${path.map((k) => `- \`${k}\``).join("\n")}`
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
const sections = [...byDepth.entries()].sort((a, b) => a[0] - b[0]).map(([d, ks]) => `## Depth ${d}
|
|
317
|
+
${path.map((k) => `- \`${k}\``).join("\n")}`);
|
|
318
|
+
}
|
|
319
|
+
const nodes = reachable(getNeighbors, fromKeys, maxDepth);
|
|
320
|
+
if (nodes.length === 0) return text(`No ${direction} reachable from "${from}" within ${maxDepth} hops.`);
|
|
321
|
+
const labelOf = store.labelsOf(nodes.map((n) => n.key));
|
|
322
|
+
const byDepth = /* @__PURE__ */ new Map();
|
|
323
|
+
for (const { key, depth } of nodes) {
|
|
324
|
+
if (!byDepth.has(depth)) byDepth.set(depth, []);
|
|
325
|
+
byDepth.get(depth).push(`\`${labelOf.get(key) ?? key}\` (${key})`);
|
|
326
|
+
}
|
|
327
|
+
const sections = [...byDepth.entries()].sort((a, b) => a[0] - b[0]).map(([d, ks]) => `## Depth ${d}
|
|
360
328
|
${ks.map((k) => `- ${k}`).join("\n")}`).join("\n\n");
|
|
361
|
-
|
|
329
|
+
return text(`# Reachable from "${from}" (${direction}, \u2264${maxDepth} hops)
|
|
362
330
|
|
|
363
331
|
${nodes.length} nodes:
|
|
364
332
|
|
|
365
|
-
${sections}`
|
|
333
|
+
${sections}`);
|
|
334
|
+
} finally {
|
|
335
|
+
store.close();
|
|
336
|
+
}
|
|
366
337
|
}
|
|
367
338
|
if (name === "find_dead_code") {
|
|
368
339
|
const repo = args.repo;
|
|
369
340
|
const limit = args.limit || 50;
|
|
370
341
|
const graphPath = join(kbPath, repo, "graph.json");
|
|
371
|
-
if (!existsSync(graphPath)) {
|
|
372
|
-
return { content: [{ type: "text", text: `No graph found for repo "${repo}"` }] };
|
|
373
|
-
}
|
|
342
|
+
if (!existsSync(graphPath)) return text(`No graph found for repo "${repo}"`);
|
|
374
343
|
const graph = JSON.parse(readFileSync(graphPath, "utf-8"));
|
|
375
344
|
const ENTITY_TYPES = /* @__PURE__ */ new Set(["function", "method", "class", "struct", "enum", "trait"]);
|
|
376
345
|
const inDegree = /* @__PURE__ */ new Map();
|
|
@@ -379,13 +348,11 @@ ${sections}` }] };
|
|
|
379
348
|
inDegree.set(l.target, (inDegree.get(l.target) ?? 0) + 1);
|
|
380
349
|
}
|
|
381
350
|
const dead = (graph.nodes ?? []).filter((n) => ENTITY_TYPES.has(n.type) && (inDegree.get(n.id) ?? 0) === 0);
|
|
382
|
-
if (dead.length === 0) {
|
|
383
|
-
return { content: [{ type: "text", text: `# Dead code in ${repo}
|
|
351
|
+
if (dead.length === 0) return text(`# Dead code in ${repo}
|
|
384
352
|
|
|
385
|
-
No zero-caller entities found.`
|
|
386
|
-
}
|
|
353
|
+
No zero-caller entities found.`);
|
|
387
354
|
const rows = dead.slice(0, limit).map((n) => `- \`${n.label ?? n.id}\` (${n.type})${n.file ? ` \u2014 ${n.file}` : ""}`).join("\n");
|
|
388
|
-
return
|
|
355
|
+
return text(`# Dead code in ${repo} (heuristic)
|
|
389
356
|
|
|
390
357
|
${dead.length} zero-caller entit${dead.length === 1 ? "y" : "ies"}:
|
|
391
358
|
|
|
@@ -393,17 +360,15 @@ ${rows}${dead.length > limit ? `
|
|
|
393
360
|
|
|
394
361
|
... and ${dead.length - limit} more` : ""}
|
|
395
362
|
|
|
396
|
-
_Note: exported APIs, reflection, and dynamic dispatch can cause false positives._`
|
|
363
|
+
_Note: exported APIs, reflection, and dynamic dispatch can cause false positives._`);
|
|
397
364
|
}
|
|
398
365
|
if (name === "get_architecture") {
|
|
399
366
|
const repo = args.repo;
|
|
400
367
|
if (repo) {
|
|
401
368
|
const profile = loadProfile(ctx.projectName, repo);
|
|
402
|
-
if (!profile) {
|
|
403
|
-
return { content: [{ type: "text", text: `No profile for "${repo}". Run profiling (requires LLM) to generate one.` }] };
|
|
404
|
-
}
|
|
369
|
+
if (!profile) return text(`No profile for "${repo}". Run profiling (requires LLM) to generate one.`);
|
|
405
370
|
const ep = (xs) => xs?.length ? xs.map((e) => ` - ${e.type}: ${e.identifier} \u2014 ${e.description}`).join("\n") : " - (none)";
|
|
406
|
-
const
|
|
371
|
+
const body = [
|
|
407
372
|
`# ${profile.name} \u2014 ${profile.role} (${profile.domain})`,
|
|
408
373
|
"",
|
|
409
374
|
profile.description,
|
|
@@ -417,7 +382,7 @@ _Note: exported APIs, reflection, and dynamic dispatch can cause false positives
|
|
|
417
382
|
"## Consumes",
|
|
418
383
|
ep(profile.consumes)
|
|
419
384
|
].join("\n");
|
|
420
|
-
return
|
|
385
|
+
return text(body);
|
|
421
386
|
}
|
|
422
387
|
const pgPath = join(kbPath, "PROJECT_GRAPH.json");
|
|
423
388
|
const pg = existsSync(pgPath) ? JSON.parse(readFileSync(pgPath, "utf-8")) : null;
|
|
@@ -425,15 +390,13 @@ _Note: exported APIs, reflection, and dynamic dispatch can cause false positives
|
|
|
425
390
|
const lines = ["# Project Architecture", "", pg.architectureSummary ?? ""];
|
|
426
391
|
if (pg.repoRoles && Object.keys(pg.repoRoles).length) {
|
|
427
392
|
lines.push("", "## Repo Roles");
|
|
428
|
-
for (const [
|
|
429
|
-
lines.push(`- **${
|
|
393
|
+
for (const [n, r] of Object.entries(pg.repoRoles)) {
|
|
394
|
+
lines.push(`- **${n}** \u2014 ${r.role} (${r.criticality}): ${(r.responsibilities ?? []).join("; ")}`);
|
|
430
395
|
}
|
|
431
396
|
}
|
|
432
397
|
if (pg.relationships?.length) {
|
|
433
398
|
lines.push("", "## Relationships");
|
|
434
|
-
for (const rel of pg.relationships) {
|
|
435
|
-
lines.push(`- ${rel.from} \u2192 ${rel.to} (${rel.type}): ${rel.description}`);
|
|
436
|
-
}
|
|
399
|
+
for (const rel of pg.relationships) lines.push(`- ${rel.from} \u2192 ${rel.to} (${rel.type}): ${rel.description}`);
|
|
437
400
|
}
|
|
438
401
|
if (pg.keyFlows?.length) {
|
|
439
402
|
lines.push("", "## Key Flows");
|
|
@@ -442,77 +405,51 @@ _Note: exported APIs, reflection, and dynamic dispatch can cause false positives
|
|
|
442
405
|
for (const s of f.steps ?? []) lines.push(` - ${s.repo}/${s.component}: ${s.action} [${s.protocol}]`);
|
|
443
406
|
}
|
|
444
407
|
}
|
|
445
|
-
return
|
|
408
|
+
return text(lines.join("\n"));
|
|
446
409
|
}
|
|
447
410
|
const summaryPath = join(kbPath, "PROJECT_SUMMARY.md");
|
|
448
|
-
if (existsSync(summaryPath))
|
|
449
|
-
return { content: [{ type: "text", text: readFileSync(summaryPath, "utf-8") }] };
|
|
450
|
-
}
|
|
411
|
+
if (existsSync(summaryPath)) return text(readFileSync(summaryPath, "utf-8"));
|
|
451
412
|
const profiles = loadAllProfiles(ctx.projectName);
|
|
452
413
|
if (profiles.length) {
|
|
453
|
-
|
|
414
|
+
return text(`# Project Repos
|
|
454
415
|
|
|
455
416
|
${profiles.map((p) => `- **${p.name}** \u2014 ${p.role} (${p.domain}): ${p.description}`).join("\n")}
|
|
456
417
|
|
|
457
|
-
_Run project-graph generation (requires LLM) for a full architecture view._
|
|
458
|
-
return { content: [{ type: "text", text }] };
|
|
418
|
+
_Run project-graph generation (requires LLM) for a full architecture view._`);
|
|
459
419
|
}
|
|
460
|
-
return
|
|
420
|
+
return text("No architecture data yet. Index the project, then run profiling / project-graph generation (requires an LLM) for an architecture overview.");
|
|
461
421
|
}
|
|
462
422
|
if (name === "search_graph") {
|
|
463
|
-
const
|
|
464
|
-
if (!
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
} catch {
|
|
477
|
-
const lc = nameArg.toLowerCase();
|
|
478
|
-
nameTest = (s) => s.toLowerCase().includes(lc);
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
const degree = /* @__PURE__ */ new Map();
|
|
482
|
-
for (const e of sys.edges) {
|
|
483
|
-
if (edgeType(e) === "contains") continue;
|
|
484
|
-
degree.set(e.source, (degree.get(e.source) ?? 0) + 1);
|
|
485
|
-
degree.set(e.target, (degree.get(e.target) ?? 0) + 1);
|
|
486
|
-
}
|
|
487
|
-
const matches = sys.nodes.filter((n) => {
|
|
488
|
-
if (repo && !n.key.startsWith(repo + "::")) return false;
|
|
489
|
-
if (type && n.attributes?.type !== type) return false;
|
|
490
|
-
const f = n.attributes?.file ?? n.key.split("::")[1] ?? "";
|
|
491
|
-
if (file && !f.includes(file)) return false;
|
|
492
|
-
if (nameArg && !(nameTest(n.attributes?.label ?? "") || nameTest(n.key))) return false;
|
|
493
|
-
return (degree.get(n.key) ?? 0) >= minDegree;
|
|
494
|
-
}).map((n) => ({ key: n.key, label: nodeLabel(n), type: n.attributes?.type ?? "?", file: n.attributes?.file ?? n.key.split("::")[1] ?? "", deg: degree.get(n.key) ?? 0 })).sort((a, b) => b.deg - a.deg);
|
|
495
|
-
if (matches.length === 0) {
|
|
496
|
-
return { content: [{ type: "text", text: "No entities match the given filters." }] };
|
|
497
|
-
}
|
|
498
|
-
const rows = matches.slice(0, limit).map((m) => `- \`${m.label}\` (${m.type}, ${m.file}) \u2014 degree ${m.deg} \`${m.key}\``).join("\n");
|
|
499
|
-
return { content: [{ type: "text", text: `# search_graph \u2014 ${matches.length} match${matches.length === 1 ? "" : "es"}
|
|
423
|
+
const store = await openSystemGraphStore(kbPath);
|
|
424
|
+
if (!store) return text(NO_GRAPH);
|
|
425
|
+
try {
|
|
426
|
+
const { rows, total } = store.searchNodes({
|
|
427
|
+
name: args.name,
|
|
428
|
+
type: args.type,
|
|
429
|
+
file: args.file,
|
|
430
|
+
repo: args.repo,
|
|
431
|
+
minDegree: args.minDegree ?? 0
|
|
432
|
+
}, args.limit || 50);
|
|
433
|
+
if (total === 0) return text("No entities match the given filters.");
|
|
434
|
+
const body = rows.map((m) => `- \`${m.label}\` (${m.type}, ${m.file}) \u2014 degree ${m.degree} \`${m.key}\``).join("\n");
|
|
435
|
+
return text(`# search_graph \u2014 ${total} match${total === 1 ? "" : "es"}
|
|
500
436
|
|
|
501
|
-
${
|
|
437
|
+
${body}${total > rows.length ? `
|
|
502
438
|
|
|
503
|
-
... and ${
|
|
439
|
+
... and ${total - rows.length} more` : ""}`);
|
|
440
|
+
} finally {
|
|
441
|
+
store.close();
|
|
442
|
+
}
|
|
504
443
|
}
|
|
505
444
|
if (name === "detect_changes") {
|
|
506
445
|
const repo = args.repo;
|
|
507
446
|
const limit = args.limit || 50;
|
|
508
447
|
if (!ctx.directoryPath) {
|
|
509
|
-
return
|
|
448
|
+
return text("detect_changes needs a local repo path \u2014 available in local/serve mode only (not remote-proxy mode).");
|
|
510
449
|
}
|
|
511
450
|
const candidate = join(ctx.directoryPath, repo);
|
|
512
451
|
const repoPath = existsSync(join(candidate, ".git")) ? candidate : ctx.directoryPath;
|
|
513
|
-
if (!existsSync(repoPath)) {
|
|
514
|
-
return { content: [{ type: "text", text: `Repo path not found: ${repoPath}` }] };
|
|
515
|
-
}
|
|
452
|
+
if (!existsSync(repoPath)) return text(`Repo path not found: ${repoPath}`);
|
|
516
453
|
let baseSha = args.baseSha;
|
|
517
454
|
if (!baseSha) {
|
|
518
455
|
const metaPath = join(kbPath, repo, "index_meta.json");
|
|
@@ -523,43 +460,42 @@ ${rows}${matches.length > limit ? `
|
|
|
523
460
|
}
|
|
524
461
|
}
|
|
525
462
|
}
|
|
526
|
-
if (!baseSha)
|
|
527
|
-
return { content: [{ type: "text", text: "No base commit available. Pass baseSha, or index the repo first so a last-indexed SHA exists." }] };
|
|
528
|
-
}
|
|
463
|
+
if (!baseSha) return text("No base commit available. Pass baseSha, or index the repo first so a last-indexed SHA exists.");
|
|
529
464
|
const diff = getAllChanges(repoPath, baseSha);
|
|
530
|
-
if (diff.fallbackToFull) {
|
|
531
|
-
return { content: [{ type: "text", text: `git diff against ${baseSha.slice(0, 7)} failed or is too large to map incrementally. Check the base SHA.` }] };
|
|
532
|
-
}
|
|
465
|
+
if (diff.fallbackToFull) return text(`git diff against ${baseSha.slice(0, 7)} failed or is too large to map incrementally. Check the base SHA.`);
|
|
533
466
|
const changedFiles = getChangedFilesList(diff);
|
|
534
|
-
if (changedFiles.length === 0 && diff.deleted.length === 0) {
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
467
|
+
if (changedFiles.length === 0 && diff.deleted.length === 0) return text(`No source changes since ${baseSha.slice(0, 7)}.`);
|
|
468
|
+
const store = await openSystemGraphStore(kbPath);
|
|
469
|
+
let changedKeys = [];
|
|
470
|
+
let dependents = [];
|
|
471
|
+
let depTotal = 0;
|
|
472
|
+
let depRepos = [];
|
|
473
|
+
if (store) {
|
|
474
|
+
try {
|
|
475
|
+
changedKeys = store.nodesInFiles(repo, changedFiles);
|
|
476
|
+
const d = store.dependents(changedKeys, limit);
|
|
477
|
+
dependents = d.edges;
|
|
478
|
+
depTotal = d.total;
|
|
479
|
+
depRepos = d.repos;
|
|
480
|
+
} finally {
|
|
481
|
+
store.close();
|
|
544
482
|
}
|
|
545
483
|
}
|
|
546
|
-
const dependents = sys ? sys.edges.filter((e) => changedKeys.has(e.target) && !changedKeys.has(e.source) && edgeType(e) !== "contains") : [];
|
|
547
|
-
const dependentRepos = new Set(dependents.map((e) => e.source.split("::")[0]));
|
|
548
484
|
const lines = [
|
|
549
485
|
`# Changes since ${baseSha.slice(0, 7)} \u2014 ${repo}`,
|
|
550
486
|
"",
|
|
551
487
|
`## Changed files: ${changedFiles.length} (+${diff.added.length} / ~${diff.modified.length}), deleted ${diff.deleted.length}`,
|
|
552
488
|
...changedFiles.slice(0, 30).map((f) => `- ${f}`),
|
|
553
489
|
"",
|
|
554
|
-
`## Affected entities: ${changedKeys.
|
|
555
|
-
...
|
|
490
|
+
`## Affected entities: ${changedKeys.length}`,
|
|
491
|
+
...changedKeys.slice(0, 30).map((k) => `- \`${k}\``),
|
|
556
492
|
"",
|
|
557
|
-
`## Dependents: ${
|
|
558
|
-
...dependents.
|
|
493
|
+
`## Dependents: ${depTotal} edges from ${depRepos.length} repo(s)`,
|
|
494
|
+
...dependents.map((e) => `- \`${e.source}\` \u2192 \`${e.target}\` (${e.type ?? "edge"})`),
|
|
559
495
|
"",
|
|
560
|
-
`## Affected repos: ${
|
|
496
|
+
`## Affected repos: ${depRepos.join(", ") || "none"}`
|
|
561
497
|
];
|
|
562
|
-
return
|
|
498
|
+
return text(lines.join("\n"));
|
|
563
499
|
}
|
|
564
500
|
return null;
|
|
565
501
|
} catch (err) {
|