@kage-core/kage-graph-mcp 1.1.28 → 1.1.30
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 +3 -2
- package/dist/index.js +1 -1
- package/dist/kernel.js +152 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,8 +37,9 @@ Restart your agent once after setup so MCP tools reload.
|
|
|
37
37
|
- repo-local memory for decisions, runbooks, bug fixes, gotchas, conventions,
|
|
38
38
|
and code explanations
|
|
39
39
|
- a code graph for files, symbols, imports, calls, routes, tests, and packages,
|
|
40
|
-
including generic call/test signals and
|
|
41
|
-
|
|
40
|
+
including generic call/test signals and mixed-language framework routes
|
|
41
|
+
- conservative cleanup candidates for unreferenced files, unused exports, and
|
|
42
|
+
internal-looking unused symbols
|
|
42
43
|
- memory-code links so project knowledge points at the code it affects
|
|
43
44
|
- decision intelligence for why-memory coverage, stale/weak packets, and
|
|
44
45
|
important files that still lack linked repo knowledge
|
package/dist/index.js
CHANGED
|
@@ -221,7 +221,7 @@ function listTools() {
|
|
|
221
221
|
},
|
|
222
222
|
{
|
|
223
223
|
name: "kage_cleanup_candidates",
|
|
224
|
-
description: "Find conservative cleanup candidates from Kage's code graph. Reports unreferenced source files with confidence and reasons; never auto-deletes.",
|
|
224
|
+
description: "Find conservative cleanup candidates from Kage's code graph. Reports unreferenced source files, unused exports, and internal-looking unused symbols with confidence and reasons; never auto-deletes.",
|
|
225
225
|
inputSchema: {
|
|
226
226
|
type: "object",
|
|
227
227
|
properties: {
|
package/dist/kernel.js
CHANGED
|
@@ -2862,6 +2862,25 @@ function parsePythonMethodList(value) {
|
|
|
2862
2862
|
const methods = [...value.matchAll(/["']([A-Za-z]+)["']/g)].map((match) => match[1].toUpperCase());
|
|
2863
2863
|
return methods.length ? unique(methods) : ["GET"];
|
|
2864
2864
|
}
|
|
2865
|
+
const SPRING_ROUTE_METHODS = {
|
|
2866
|
+
GetMapping: "GET",
|
|
2867
|
+
PostMapping: "POST",
|
|
2868
|
+
PutMapping: "PUT",
|
|
2869
|
+
PatchMapping: "PATCH",
|
|
2870
|
+
DeleteMapping: "DELETE",
|
|
2871
|
+
RequestMapping: "ANY",
|
|
2872
|
+
};
|
|
2873
|
+
const ASPNET_ROUTE_METHODS = {
|
|
2874
|
+
Get: "GET",
|
|
2875
|
+
Post: "POST",
|
|
2876
|
+
Put: "PUT",
|
|
2877
|
+
Patch: "PATCH",
|
|
2878
|
+
Delete: "DELETE",
|
|
2879
|
+
};
|
|
2880
|
+
function routeHandlerNearLine(lines, startIndex, pattern) {
|
|
2881
|
+
const handlerLine = lines.slice(startIndex + 1, Math.min(lines.length, startIndex + 8)).find((candidate) => pattern.test(candidate));
|
|
2882
|
+
return handlerLine?.match(pattern)?.[1] ?? null;
|
|
2883
|
+
}
|
|
2865
2884
|
function extractRoutes(path, text, symbols) {
|
|
2866
2885
|
const routes = [];
|
|
2867
2886
|
const addRoute = (method, routePath, offset, framework, handler = null) => {
|
|
@@ -2916,6 +2935,64 @@ function extractRoutes(path, text, symbols) {
|
|
|
2916
2935
|
}
|
|
2917
2936
|
}
|
|
2918
2937
|
}
|
|
2938
|
+
if (extensionOf(path) === ".rb") {
|
|
2939
|
+
for (const match of text.matchAll(/\b(get|post|put|patch|delete)\s+["']([^"']+)["']/gi)) {
|
|
2940
|
+
addRoute(match[1].toUpperCase(), match[2], match.index ?? 0, "rails");
|
|
2941
|
+
}
|
|
2942
|
+
}
|
|
2943
|
+
if (extensionOf(path) === ".php") {
|
|
2944
|
+
for (const match of text.matchAll(/\bRoute::(get|post|put|patch|delete|options|any)\s*\(\s*["']([^"']+)["']/gi)) {
|
|
2945
|
+
addRoute(match[1].toUpperCase(), match[2], match.index ?? 0, "laravel");
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2948
|
+
if ([".java", ".kt"].includes(extensionOf(path))) {
|
|
2949
|
+
const lines = text.split(/\r?\n/);
|
|
2950
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
2951
|
+
const line = lines[index];
|
|
2952
|
+
const mapping = line.match(/@(GetMapping|PostMapping|PutMapping|PatchMapping|DeleteMapping|RequestMapping)\s*(?:\(\s*(?:value\s*=\s*)?["']([^"']+)["'])?/);
|
|
2953
|
+
if (!mapping || !mapping[2])
|
|
2954
|
+
continue;
|
|
2955
|
+
let method = SPRING_ROUTE_METHODS[mapping[1]] ?? "ANY";
|
|
2956
|
+
if (mapping[1] === "RequestMapping") {
|
|
2957
|
+
const explicit = line.match(/RequestMethod\.(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)/);
|
|
2958
|
+
if (explicit)
|
|
2959
|
+
method = explicit[1];
|
|
2960
|
+
}
|
|
2961
|
+
const handler = routeHandlerNearLine(lines, index, /^\s*(?:public|private|protected)?\s*[\w<>\[\], ?]+\s+([A-Za-z_][\w]*)\s*\(/);
|
|
2962
|
+
addRoute(method, mapping[2], offsetForLine(text, index + 1), "spring", handler);
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2965
|
+
if (extensionOf(path) === ".go") {
|
|
2966
|
+
for (const match of text.matchAll(/\b[A-Za-z_][\w]*\.(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\s*\(\s*["`]([^"`]+)["`]\s*,\s*([A-Za-z_][\w.]*)?/g)) {
|
|
2967
|
+
addRoute(match[1], match[2], match.index ?? 0, "go-router", match[3]?.split(".").pop() ?? null);
|
|
2968
|
+
}
|
|
2969
|
+
}
|
|
2970
|
+
if (extensionOf(path) === ".rs") {
|
|
2971
|
+
for (const match of text.matchAll(/\.route\s*\(\s*["']([^"']+)["']\s*,\s*(get|post|put|patch|delete|options|head)\s*\(\s*([A-Za-z_][\w:]*)?/gi)) {
|
|
2972
|
+
addRoute(match[2].toUpperCase(), match[1], match.index ?? 0, "rust-router", match[3]?.split("::").pop() ?? null);
|
|
2973
|
+
}
|
|
2974
|
+
const lines = text.split(/\r?\n/);
|
|
2975
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
2976
|
+
const attr = lines[index].match(/#\[(get|post|put|patch|delete|options|head)\(\s*["']([^"']+)["']\s*\)\]/i);
|
|
2977
|
+
if (!attr)
|
|
2978
|
+
continue;
|
|
2979
|
+
const handler = routeHandlerNearLine(lines, index, /^\s*(?:pub\s+)?(?:async\s+)?fn\s+([A-Za-z_][\w]*)\s*\(/);
|
|
2980
|
+
addRoute(attr[1].toUpperCase(), attr[2], offsetForLine(text, index + 1), "rust-router", handler);
|
|
2981
|
+
}
|
|
2982
|
+
}
|
|
2983
|
+
if (extensionOf(path) === ".cs") {
|
|
2984
|
+
for (const match of text.matchAll(/\bMap(Get|Post|Put|Patch|Delete)\s*\(\s*["']([^"']+)["']\s*,\s*([A-Za-z_][\w.]*)?/g)) {
|
|
2985
|
+
addRoute(ASPNET_ROUTE_METHODS[match[1]] ?? match[1].toUpperCase(), match[2], match.index ?? 0, "aspnet", match[3]?.split(".").pop() ?? null);
|
|
2986
|
+
}
|
|
2987
|
+
const lines = text.split(/\r?\n/);
|
|
2988
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
2989
|
+
const attr = lines[index].match(/\[\s*Http(Get|Post|Put|Patch|Delete)?\s*\(\s*["']([^"']+)["']\s*\)\s*\]/);
|
|
2990
|
+
if (!attr)
|
|
2991
|
+
continue;
|
|
2992
|
+
const handler = routeHandlerNearLine(lines, index, /^\s*(?:public|private|protected|internal)?\s*(?:async\s+)?[\w<>\[\], ?]+\s+([A-Za-z_][\w]*)\s*\(/);
|
|
2993
|
+
addRoute(attr[1] ? ASPNET_ROUTE_METHODS[attr[1]] ?? attr[1].toUpperCase() : "ANY", attr[2], offsetForLine(text, index + 1), "aspnet", handler);
|
|
2994
|
+
}
|
|
2995
|
+
}
|
|
2919
2996
|
if (/app\/api\//.test(path)) {
|
|
2920
2997
|
for (const symbol of symbols.filter((symbol) => symbol.path === path && symbol.export && ["GET", "POST", "PUT", "PATCH", "DELETE"].includes(symbol.name))) {
|
|
2921
2998
|
const apiPath = `/${path.replace(/^.*app\/api\//, "").replace(/\/route\.[cm]?[jt]sx?$/, "").replace(/\[([^\]]+)\]/g, ":$1")}`;
|
|
@@ -5424,6 +5501,26 @@ function hasRuntimePathReference(projectDir, graph, target) {
|
|
|
5424
5501
|
}
|
|
5425
5502
|
return false;
|
|
5426
5503
|
}
|
|
5504
|
+
function cleanupSymbolKind(symbol) {
|
|
5505
|
+
return ["function", "method", "class", "constant"].includes(symbol.kind);
|
|
5506
|
+
}
|
|
5507
|
+
function symbolCleanupCandidate(symbol, kind, reasons, score, coveredByTests, git) {
|
|
5508
|
+
return {
|
|
5509
|
+
path: symbol.path,
|
|
5510
|
+
kind,
|
|
5511
|
+
symbol_id: symbol.id,
|
|
5512
|
+
symbol_name: symbol.name,
|
|
5513
|
+
line: symbol.line,
|
|
5514
|
+
confidence: cleanupConfidence(score),
|
|
5515
|
+
score,
|
|
5516
|
+
reasons,
|
|
5517
|
+
inbound_imports: 0,
|
|
5518
|
+
source_inbound_imports: 0,
|
|
5519
|
+
outbound_imports: 0,
|
|
5520
|
+
covered_by_tests: coveredByTests,
|
|
5521
|
+
last_commit_at: git?.last_commit_at ?? null,
|
|
5522
|
+
};
|
|
5523
|
+
}
|
|
5427
5524
|
function kageCleanupCandidates(projectDir) {
|
|
5428
5525
|
const graph = readCurrentCodeGraph(projectDir) ?? buildCodeGraph(projectDir);
|
|
5429
5526
|
const fileByPath = new Map(graph.files.map((file) => [file.path, file]));
|
|
@@ -5448,6 +5545,7 @@ function kageCleanupCandidates(projectDir) {
|
|
|
5448
5545
|
warnings.push("Git history is unavailable, so cleanup confidence does not use recency.");
|
|
5449
5546
|
const candidates = [];
|
|
5450
5547
|
const skippedRuntimeReferences = [];
|
|
5548
|
+
const wholeFileCandidates = new Set();
|
|
5451
5549
|
for (const file of graph.files) {
|
|
5452
5550
|
if (file.kind !== "source")
|
|
5453
5551
|
continue;
|
|
@@ -5500,6 +5598,60 @@ function kageCleanupCandidates(projectDir) {
|
|
|
5500
5598
|
covered_by_tests: coveredByTests,
|
|
5501
5599
|
last_commit_at: git?.last_commit_at ?? null,
|
|
5502
5600
|
});
|
|
5601
|
+
wholeFileCandidates.add(file.path);
|
|
5602
|
+
}
|
|
5603
|
+
const calledSymbols = new Set(graph.calls.map((call) => call.to_symbol));
|
|
5604
|
+
const routeHandlers = new Set(graph.routes.map((route) => route.handler_symbol).filter((value) => Boolean(value)));
|
|
5605
|
+
const coveredSymbolNames = new Set(graph.tests.map((test) => test.covers_symbol?.toLowerCase()).filter((value) => Boolean(value)));
|
|
5606
|
+
const symbolsByPath = new Map();
|
|
5607
|
+
for (const symbol of graph.symbols.filter(cleanupSymbolKind)) {
|
|
5608
|
+
const list = symbolsByPath.get(symbol.path) ?? [];
|
|
5609
|
+
list.push(symbol);
|
|
5610
|
+
symbolsByPath.set(symbol.path, list);
|
|
5611
|
+
}
|
|
5612
|
+
const importedNamesByPath = new Map();
|
|
5613
|
+
for (const edge of graph.imports) {
|
|
5614
|
+
if (!edge.to_path || !edge.imported.length)
|
|
5615
|
+
continue;
|
|
5616
|
+
const names = importedNamesByPath.get(edge.to_path) ?? new Set();
|
|
5617
|
+
for (const name of edge.imported)
|
|
5618
|
+
names.add(name);
|
|
5619
|
+
importedNamesByPath.set(edge.to_path, names);
|
|
5620
|
+
}
|
|
5621
|
+
for (const file of graph.files) {
|
|
5622
|
+
if (file.kind !== "source" || wholeFileCandidates.has(file.path))
|
|
5623
|
+
continue;
|
|
5624
|
+
if (isEntrypointLike(file.path) || routeFiles.has(file.path))
|
|
5625
|
+
continue;
|
|
5626
|
+
const fileSymbols = symbolsByPath.get(file.path) ?? [];
|
|
5627
|
+
if (!fileSymbols.length)
|
|
5628
|
+
continue;
|
|
5629
|
+
const git = hasGit ? gitFileSignal(projectDir, file.path, graphPaths) : null;
|
|
5630
|
+
const coveredByTests = hasTestCoverage(file.path, graph);
|
|
5631
|
+
const importedNames = importedNamesByPath.get(file.path) ?? new Set();
|
|
5632
|
+
const exportedSymbols = fileSymbols.filter((symbol) => symbol.export);
|
|
5633
|
+
const hasMatchedNamedExport = exportedSymbols.some((symbol) => importedNames.has(symbol.name));
|
|
5634
|
+
for (const symbol of fileSymbols) {
|
|
5635
|
+
const symbolReferenced = calledSymbols.has(symbol.id) || routeHandlers.has(symbol.id) || coveredSymbolNames.has(symbol.name.toLowerCase());
|
|
5636
|
+
if (symbolReferenced)
|
|
5637
|
+
continue;
|
|
5638
|
+
if (symbol.export) {
|
|
5639
|
+
if (!hasMatchedNamedExport || importedNames.has(symbol.name))
|
|
5640
|
+
continue;
|
|
5641
|
+
candidates.push(symbolCleanupCandidate(symbol, "unused_export", [
|
|
5642
|
+
`export "${symbol.name}" is not imported by current named import edges`,
|
|
5643
|
+
"symbol is not a known call target, route handler, or covered test target",
|
|
5644
|
+
"file has at least one other exported symbol imported by name",
|
|
5645
|
+
], 0.62, coveredByTests, git));
|
|
5646
|
+
}
|
|
5647
|
+
else if (/^_[A-Za-z0-9_]+/.test(symbol.name)) {
|
|
5648
|
+
candidates.push(symbolCleanupCandidate(symbol, "unused_internal_symbol", [
|
|
5649
|
+
`internal-looking symbol "${symbol.name}" has no known call edge`,
|
|
5650
|
+
"symbol is not a route handler or covered test target",
|
|
5651
|
+
"candidate is review input only; dynamic references may exist",
|
|
5652
|
+
], 0.5, coveredByTests, git));
|
|
5653
|
+
}
|
|
5654
|
+
}
|
|
5503
5655
|
}
|
|
5504
5656
|
candidates.sort((a, b) => b.score - a.score || a.path.localeCompare(b.path));
|
|
5505
5657
|
return {
|