@kage-core/kage-graph-mcp 1.1.29 → 1.1.31

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
@@ -36,8 +36,11 @@ Restart your agent once after setup so MCP tools reload.
36
36
 
37
37
  - repo-local memory for decisions, runbooks, bug fixes, gotchas, conventions,
38
38
  and code explanations
39
- - a code graph for files, symbols, imports, calls, routes, tests, and packages,
40
- including generic call/test signals and mixed-language framework routes
39
+ - a code graph for files, symbols, imports, confidence-scored calls, routes,
40
+ tests, and packages, including generic call/test signals and mixed-language
41
+ framework routes
42
+ - conservative cleanup candidates for unreferenced files, unused exports, and
43
+ internal-looking unused symbols
41
44
  - memory-code links so project knowledge points at the code it affects
42
45
  - decision intelligence for why-memory coverage, stale/weak packets, and
43
46
  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
@@ -1656,7 +1656,9 @@ function hydrateCodeGraphArtifact(projectDir, artifact, structural) {
1656
1656
  ...index.imports,
1657
1657
  ...(artifact.extra_imports ?? []),
1658
1658
  ].sort((a, b) => a.from_path.localeCompare(b.from_path) || a.line - b.line || a.specifier.localeCompare(b.specifier)),
1659
- calls: artifact.calls ?? [],
1659
+ calls: (artifact.calls ?? [])
1660
+ .map((call) => normalizeCallEdge(call, { confidence: 0.7, resolution: "generic_static_name" }))
1661
+ .filter((call) => Boolean(call)),
1660
1662
  routes: artifact.routes ?? [],
1661
1663
  tests: artifact.tests ?? [],
1662
1664
  packages: artifact.packages ?? [],
@@ -2755,6 +2757,27 @@ function symbolAtLine(symbols, path, line) {
2755
2757
  .filter((symbol) => symbol.path === path && symbol.line <= line && (symbol.end_line ?? symbol.line) >= line)
2756
2758
  .sort((a, b) => (b.line - a.line) || ((a.end_line ?? a.line) - (b.end_line ?? b.line)))[0] ?? null;
2757
2759
  }
2760
+ function normalizeCallConfidence(value, fallback) {
2761
+ const numeric = Number(value);
2762
+ if (!Number.isFinite(numeric))
2763
+ return fallback;
2764
+ return Number(Math.max(0, Math.min(1, numeric)).toFixed(2));
2765
+ }
2766
+ function normalizeCallResolution(value, fallback) {
2767
+ return value === "typescript_ast_name" || value === "generic_static_name" || value === "external_index" ? value : fallback;
2768
+ }
2769
+ function normalizeCallEdge(call, fallback) {
2770
+ if (!isRecord(call) || typeof call.to_symbol !== "string")
2771
+ return null;
2772
+ return {
2773
+ from_symbol: typeof call.from_symbol === "string" ? call.from_symbol : null,
2774
+ to_symbol: call.to_symbol,
2775
+ path: String(call.path ?? ""),
2776
+ line: Math.max(1, Number(call.line ?? 1)),
2777
+ confidence: normalizeCallConfidence(call.confidence, fallback.confidence),
2778
+ resolution: normalizeCallResolution(call.resolution, fallback.resolution),
2779
+ };
2780
+ }
2758
2781
  function extractCalls(path, text, symbols, symbolByName) {
2759
2782
  const sourceFile = sourceFileFor(path, text);
2760
2783
  const calls = [];
@@ -2782,7 +2805,14 @@ function extractCalls(path, text, symbols, symbolByName) {
2782
2805
  break;
2783
2806
  if (target.path === path && target.line === line)
2784
2807
  continue;
2785
- calls.push({ from_symbol: caller?.id ?? null, to_symbol: target.id, path, line });
2808
+ calls.push({
2809
+ from_symbol: caller?.id ?? null,
2810
+ to_symbol: target.id,
2811
+ path,
2812
+ line,
2813
+ confidence: target.path === path ? 0.9 : 0.75,
2814
+ resolution: "typescript_ast_name",
2815
+ });
2786
2816
  }
2787
2817
  ts.forEachChild(node, visit);
2788
2818
  };
@@ -2824,7 +2854,14 @@ function extractGenericCalls(path, text, symbols, symbolByName) {
2824
2854
  for (const target of targets.slice(0, 3)) {
2825
2855
  if (calls.length >= MAX_CODE_GRAPH_CALLS_PER_FILE)
2826
2856
  break;
2827
- calls.push({ from_symbol: caller?.id ?? null, to_symbol: target.id, path, line });
2857
+ calls.push({
2858
+ from_symbol: caller?.id ?? null,
2859
+ to_symbol: target.id,
2860
+ path,
2861
+ line,
2862
+ confidence: target.path === path ? 0.7 : 0.55,
2863
+ resolution: "generic_static_name",
2864
+ });
2828
2865
  }
2829
2866
  }
2830
2867
  }
@@ -3173,9 +3210,8 @@ function parseKageExternalIndex(projectDir, parser, path) {
3173
3210
  : [];
3174
3211
  const calls = Array.isArray(raw.calls)
3175
3212
  ? raw.calls.flatMap((item) => {
3176
- if (!isRecord(item) || typeof item.to_symbol !== "string")
3177
- return [];
3178
- return [{ from_symbol: typeof item.from_symbol === "string" ? item.from_symbol : null, to_symbol: item.to_symbol, path: String(item.path ?? ""), line: Math.max(1, Number(item.line ?? 1)) }];
3213
+ const call = normalizeCallEdge(item, { confidence: 0.85, resolution: "external_index" });
3214
+ return call ? [call] : [];
3179
3215
  })
3180
3216
  : [];
3181
3217
  return { symbols, imports, calls };
@@ -3230,7 +3266,7 @@ function parseScipJsonObject(projectDir, raw) {
3230
3266
  symbols.push(symbol);
3231
3267
  }
3232
3268
  else {
3233
- calls.push({ from_symbol: null, to_symbol: name, path: rel, line });
3269
+ calls.push({ from_symbol: null, to_symbol: name, path: rel, line, confidence: 0.85, resolution: "external_index" });
3234
3270
  }
3235
3271
  }
3236
3272
  }
@@ -4995,7 +5031,7 @@ function queryCodeGraph(projectDir, query, limit = 10, graph) {
4995
5031
  ...imports.map(({ item }, index) => `${index + 1}. ${item.from_path}:${item.line} ${item.kind} ${item.specifier}${item.to_path ? ` -> ${item.to_path}` : ""}`),
4996
5032
  calls.length ? "" : "",
4997
5033
  calls.length ? "## Calls" : "",
4998
- ...calls.map((call, index) => `${index + 1}. ${call.from_symbol ? symbolNameById.get(call.from_symbol) ?? call.from_symbol : call.path} calls ${symbolNameById.get(call.to_symbol) ?? call.to_symbol} at ${call.path}:${call.line}`),
5034
+ ...calls.map((call, index) => `${index + 1}. ${call.from_symbol ? symbolNameById.get(call.from_symbol) ?? call.from_symbol : call.path} calls ${symbolNameById.get(call.to_symbol) ?? call.to_symbol} at ${call.path}:${call.line} (${call.resolution}, confidence ${call.confidence.toFixed(2)})`),
4999
5035
  ];
5000
5036
  return {
5001
5037
  query,
@@ -5501,6 +5537,26 @@ function hasRuntimePathReference(projectDir, graph, target) {
5501
5537
  }
5502
5538
  return false;
5503
5539
  }
5540
+ function cleanupSymbolKind(symbol) {
5541
+ return ["function", "method", "class", "constant"].includes(symbol.kind);
5542
+ }
5543
+ function symbolCleanupCandidate(symbol, kind, reasons, score, coveredByTests, git) {
5544
+ return {
5545
+ path: symbol.path,
5546
+ kind,
5547
+ symbol_id: symbol.id,
5548
+ symbol_name: symbol.name,
5549
+ line: symbol.line,
5550
+ confidence: cleanupConfidence(score),
5551
+ score,
5552
+ reasons,
5553
+ inbound_imports: 0,
5554
+ source_inbound_imports: 0,
5555
+ outbound_imports: 0,
5556
+ covered_by_tests: coveredByTests,
5557
+ last_commit_at: git?.last_commit_at ?? null,
5558
+ };
5559
+ }
5504
5560
  function kageCleanupCandidates(projectDir) {
5505
5561
  const graph = readCurrentCodeGraph(projectDir) ?? buildCodeGraph(projectDir);
5506
5562
  const fileByPath = new Map(graph.files.map((file) => [file.path, file]));
@@ -5525,6 +5581,7 @@ function kageCleanupCandidates(projectDir) {
5525
5581
  warnings.push("Git history is unavailable, so cleanup confidence does not use recency.");
5526
5582
  const candidates = [];
5527
5583
  const skippedRuntimeReferences = [];
5584
+ const wholeFileCandidates = new Set();
5528
5585
  for (const file of graph.files) {
5529
5586
  if (file.kind !== "source")
5530
5587
  continue;
@@ -5577,6 +5634,60 @@ function kageCleanupCandidates(projectDir) {
5577
5634
  covered_by_tests: coveredByTests,
5578
5635
  last_commit_at: git?.last_commit_at ?? null,
5579
5636
  });
5637
+ wholeFileCandidates.add(file.path);
5638
+ }
5639
+ const calledSymbols = new Set(graph.calls.map((call) => call.to_symbol));
5640
+ const routeHandlers = new Set(graph.routes.map((route) => route.handler_symbol).filter((value) => Boolean(value)));
5641
+ const coveredSymbolNames = new Set(graph.tests.map((test) => test.covers_symbol?.toLowerCase()).filter((value) => Boolean(value)));
5642
+ const symbolsByPath = new Map();
5643
+ for (const symbol of graph.symbols.filter(cleanupSymbolKind)) {
5644
+ const list = symbolsByPath.get(symbol.path) ?? [];
5645
+ list.push(symbol);
5646
+ symbolsByPath.set(symbol.path, list);
5647
+ }
5648
+ const importedNamesByPath = new Map();
5649
+ for (const edge of graph.imports) {
5650
+ if (!edge.to_path || !edge.imported.length)
5651
+ continue;
5652
+ const names = importedNamesByPath.get(edge.to_path) ?? new Set();
5653
+ for (const name of edge.imported)
5654
+ names.add(name);
5655
+ importedNamesByPath.set(edge.to_path, names);
5656
+ }
5657
+ for (const file of graph.files) {
5658
+ if (file.kind !== "source" || wholeFileCandidates.has(file.path))
5659
+ continue;
5660
+ if (isEntrypointLike(file.path) || routeFiles.has(file.path))
5661
+ continue;
5662
+ const fileSymbols = symbolsByPath.get(file.path) ?? [];
5663
+ if (!fileSymbols.length)
5664
+ continue;
5665
+ const git = hasGit ? gitFileSignal(projectDir, file.path, graphPaths) : null;
5666
+ const coveredByTests = hasTestCoverage(file.path, graph);
5667
+ const importedNames = importedNamesByPath.get(file.path) ?? new Set();
5668
+ const exportedSymbols = fileSymbols.filter((symbol) => symbol.export);
5669
+ const hasMatchedNamedExport = exportedSymbols.some((symbol) => importedNames.has(symbol.name));
5670
+ for (const symbol of fileSymbols) {
5671
+ const symbolReferenced = calledSymbols.has(symbol.id) || routeHandlers.has(symbol.id) || coveredSymbolNames.has(symbol.name.toLowerCase());
5672
+ if (symbolReferenced)
5673
+ continue;
5674
+ if (symbol.export) {
5675
+ if (!hasMatchedNamedExport || importedNames.has(symbol.name))
5676
+ continue;
5677
+ candidates.push(symbolCleanupCandidate(symbol, "unused_export", [
5678
+ `export "${symbol.name}" is not imported by current named import edges`,
5679
+ "symbol is not a known call target, route handler, or covered test target",
5680
+ "file has at least one other exported symbol imported by name",
5681
+ ], 0.62, coveredByTests, git));
5682
+ }
5683
+ else if (/^_[A-Za-z0-9_]+/.test(symbol.name)) {
5684
+ candidates.push(symbolCleanupCandidate(symbol, "unused_internal_symbol", [
5685
+ `internal-looking symbol "${symbol.name}" has no known call edge`,
5686
+ "symbol is not a route handler or covered test target",
5687
+ "candidate is review input only; dynamic references may exist",
5688
+ ], 0.5, coveredByTests, git));
5689
+ }
5690
+ }
5580
5691
  }
5581
5692
  candidates.sort((a, b) => b.score - a.score || a.path.localeCompare(b.path));
5582
5693
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kage-core/kage-graph-mcp",
3
- "version": "1.1.29",
3
+ "version": "1.1.31",
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": [
package/viewer/app.js CHANGED
@@ -698,19 +698,21 @@
698
698
  seen.add(entity.id);
699
699
  entities.push(entity);
700
700
  };
701
- var addEdge = function (from, to, relation, fact, source) {
701
+ var addEdge = function (from, to, relation, fact, source, options) {
702
702
  if (!from || !to) return;
703
+ options = options || {};
703
704
  edges.push({
704
705
  id: relation + ":" + from + ":" + to + ":" + edges.length,
705
706
  from: from,
706
707
  to: to,
707
708
  relation: relation,
708
709
  fact: fact,
709
- confidence: 1,
710
+ confidence: options.confidence == null ? 1 : Number(options.confidence),
710
711
  evidence: [],
711
712
  commit: graph.repo_state && graph.repo_state.head,
712
713
  source: source || "code_graph",
713
- graph_kind: "code"
714
+ graph_kind: "code",
715
+ resolution: options.resolution
714
716
  });
715
717
  };
716
718
 
@@ -764,7 +766,11 @@
764
766
  });
765
767
 
766
768
  (graph.calls || []).forEach(function (call) {
767
- addEdge(call.from_symbol || "file:" + call.path, call.to_symbol, "calls", call.path + ":" + call.line + " calls target symbol.", "calls");
769
+ var confidence = call.confidence == null ? 0.7 : Number(call.confidence);
770
+ addEdge(call.from_symbol || "file:" + call.path, call.to_symbol, "calls", call.path + ":" + call.line + " calls target symbol.", "calls", {
771
+ confidence: confidence,
772
+ resolution: call.resolution
773
+ });
768
774
  });
769
775
 
770
776
  (graph.routes || []).forEach(function (route) {