@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 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 Python framework routes for
41
- non-TypeScript repos
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 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kage-core/kage-graph-mcp",
3
- "version": "1.1.28",
3
+ "version": "1.1.30",
4
4
  "description": "Local-first repo memory, code graph, and recall MCP server for coding agents",
5
5
  "main": "dist/index.js",
6
6
  "files": [