@kage-core/kage-graph-mcp 1.1.30 → 1.1.32

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,15 +36,17 @@ 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
41
42
  - conservative cleanup candidates for unreferenced files, unused exports, and
42
43
  internal-looking unused symbols
43
44
  - memory-code links so project knowledge points at the code it affects
44
45
  - decision intelligence for why-memory coverage, stale/weak packets, and
45
46
  important files that still lack linked repo knowledge
46
- - lightweight workspace recall across sibling repos, including package and
47
- route-contract links when existing code graphs expose them
47
+ - lightweight workspace recall across sibling repos, including package,
48
+ route-contract, and topic/event contract links when existing code graphs
49
+ expose them
48
50
  - local git intelligence for risk, reviewers, contributor profiles, co-change
49
51
  warnings, ownership silos, and module health
50
52
  - `AGENTS.md` bootstrap instructions so agents recall context automatically
package/dist/cli.js CHANGED
@@ -849,6 +849,12 @@ async function main() {
849
849
  console.log(`- ${contract.provider_repo} ${contract.method} ${contract.path} -> ${contract.consumer_repo}/${contract.consumer_file}`);
850
850
  }
851
851
  }
852
+ if (result.topic_contracts.length) {
853
+ console.log("Topic/event contracts:");
854
+ for (const contract of result.topic_contracts.slice(0, 10)) {
855
+ console.log(`- ${contract.producer_repo} ${contract.topic} -> ${contract.consumer_repo}/${contract.consumer_file}`);
856
+ }
857
+ }
852
858
  if (result.warnings.length)
853
859
  console.log(`Warnings:\n${result.warnings.map((warning) => ` - ${warning}`).join("\n")}`);
854
860
  return;
package/dist/index.js CHANGED
@@ -322,7 +322,7 @@ function listTools() {
322
322
  },
323
323
  {
324
324
  name: "kage_workspace",
325
- description: "Summarize a local multi-repo workspace: discovered git repos, Kage memory coverage, code graph counts, and package dependencies between repos. Use when a task spans multiple sibling repos.",
325
+ description: "Summarize a local multi-repo workspace: discovered git repos, Kage memory coverage, code graph counts, package dependencies, route contracts, and topic/event contracts between repos. Use when a task spans multiple sibling repos.",
326
326
  inputSchema: {
327
327
  type: "object",
328
328
  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,
@@ -6451,6 +6487,96 @@ function workspaceRouteContracts(workspaceDir, repos) {
6451
6487
  .sort((a, b) => a.provider_repo.localeCompare(b.provider_repo) || a.path.localeCompare(b.path) || a.consumer_repo.localeCompare(b.consumer_repo))
6452
6488
  .slice(0, 50);
6453
6489
  }
6490
+ const TOPIC_PRODUCER_METHODS = "publish|produce|send|emit|enqueue|dispatch";
6491
+ const TOPIC_CONSUMER_METHODS = "subscribe|consume|listen|handle";
6492
+ function likelyWorkspaceTopic(value) {
6493
+ const topic = value.trim();
6494
+ if (topic.length < 3 || topic.length > 120)
6495
+ return false;
6496
+ if (topic.startsWith("/") || /^https?:\/\//i.test(topic))
6497
+ return false;
6498
+ if (/\s/.test(topic))
6499
+ return false;
6500
+ if (/[.:-]/.test(topic))
6501
+ return true;
6502
+ if (/_/.test(topic) && /^[a-z0-9_]+$/i.test(topic))
6503
+ return true;
6504
+ return /\b(created|updated|deleted|requested|completed|failed|received|changed)$/i.test(topic);
6505
+ }
6506
+ function extractWorkspaceTopicMentions(repo, file, text) {
6507
+ const mentions = [];
6508
+ const patterns = [
6509
+ { role: "producer", regex: new RegExp(`\\b(?:${TOPIC_PRODUCER_METHODS})\\s*\\(\\s*["'\`]([^"'\`]+)["'\`]`, "gi") },
6510
+ { role: "producer", regex: new RegExp(`\\.(?:${TOPIC_PRODUCER_METHODS})\\s*\\(\\s*["'\`]([^"'\`]+)["'\`]`, "gi") },
6511
+ { role: "consumer", regex: new RegExp(`\\b(?:${TOPIC_CONSUMER_METHODS})\\s*\\(\\s*["'\`]([^"'\`]+)["'\`]`, "gi") },
6512
+ { role: "consumer", regex: new RegExp(`\\.(?:${TOPIC_CONSUMER_METHODS})\\s*\\(\\s*["'\`]([^"'\`]+)["'\`]`, "gi") },
6513
+ ];
6514
+ for (const pattern of patterns) {
6515
+ for (const match of text.matchAll(pattern.regex)) {
6516
+ const topic = match[1]?.trim();
6517
+ if (!topic || !likelyWorkspaceTopic(topic))
6518
+ continue;
6519
+ mentions.push({
6520
+ repo,
6521
+ file,
6522
+ topic,
6523
+ role: pattern.role,
6524
+ evidence: `${file} ${pattern.role === "producer" ? "publishes" : "subscribes to"} ${topic}`,
6525
+ });
6526
+ }
6527
+ }
6528
+ return mentions;
6529
+ }
6530
+ function workspaceTopicContracts(workspaceDir, repos) {
6531
+ const mentions = [];
6532
+ for (const repo of repos) {
6533
+ const repoRoot = repo.path === "." ? workspaceDir : (0, node_path_1.join)(workspaceDir, repo.path);
6534
+ const graph = readCurrentCodeGraph(repoRoot);
6535
+ if (!graph)
6536
+ continue;
6537
+ for (const file of graph.files) {
6538
+ if (!["source", "config", "manifest"].includes(file.kind))
6539
+ continue;
6540
+ if (file.size_bytes > MAX_CODE_FILE_BYTES)
6541
+ continue;
6542
+ const absolutePath = (0, node_path_1.join)(repoRoot, file.path);
6543
+ if (!(0, node_fs_1.existsSync)(absolutePath))
6544
+ continue;
6545
+ try {
6546
+ mentions.push(...extractWorkspaceTopicMentions(repo.alias, file.path, (0, node_fs_1.readFileSync)(absolutePath, "utf8")));
6547
+ }
6548
+ catch {
6549
+ continue;
6550
+ }
6551
+ }
6552
+ }
6553
+ const producers = mentions.filter((mention) => mention.role === "producer");
6554
+ const consumers = mentions.filter((mention) => mention.role === "consumer");
6555
+ const contracts = [];
6556
+ const seen = new Set();
6557
+ for (const producer of producers) {
6558
+ for (const consumer of consumers) {
6559
+ if (producer.topic !== consumer.topic || producer.repo === consumer.repo)
6560
+ continue;
6561
+ const key = `${producer.repo}\0${producer.file}\0${consumer.repo}\0${consumer.file}\0${producer.topic}`;
6562
+ if (seen.has(key))
6563
+ continue;
6564
+ seen.add(key);
6565
+ contracts.push({
6566
+ topic: producer.topic,
6567
+ producer_repo: producer.repo,
6568
+ producer_file: producer.file,
6569
+ consumer_repo: consumer.repo,
6570
+ consumer_file: consumer.file,
6571
+ confidence: /[.:/-]/.test(producer.topic) ? "high" : "medium",
6572
+ evidence: `${producer.evidence}; ${consumer.evidence}`,
6573
+ });
6574
+ }
6575
+ }
6576
+ return contracts
6577
+ .sort((a, b) => a.topic.localeCompare(b.topic) || a.producer_repo.localeCompare(b.producer_repo) || a.consumer_repo.localeCompare(b.consumer_repo))
6578
+ .slice(0, 50);
6579
+ }
6454
6580
  function kageWorkspace(projectDir) {
6455
6581
  const root = (0, node_path_1.resolve)(projectDir);
6456
6582
  const warnings = [];
@@ -6476,6 +6602,7 @@ function kageWorkspace(projectDir) {
6476
6602
  return { ...rest, dependencies_on_workspace_repos: deps };
6477
6603
  });
6478
6604
  const routeContracts = workspaceRouteContracts(root, repos);
6605
+ const topicContracts = workspaceTopicContracts(root, repos);
6479
6606
  if (repos.length && repos.every((repo) => !repo.indexed))
6480
6607
  warnings.push("Workspace repos were found, but none has .agent_memory yet. Run kage init or kage refresh in each repo you want searchable.");
6481
6608
  return {
@@ -6485,8 +6612,9 @@ function kageWorkspace(projectDir) {
6485
6612
  repos,
6486
6613
  package_dependencies: packageDependencies.sort((a, b) => a.from.localeCompare(b.from) || a.to.localeCompare(b.to)),
6487
6614
  route_contracts: routeContracts,
6615
+ topic_contracts: topicContracts,
6488
6616
  warnings,
6489
- summary: `${repos.length} repo(s), ${repos.filter((repo) => repo.indexed).length} with Kage memory, ${packageDependencies.length} workspace package dependenc${packageDependencies.length === 1 ? "y" : "ies"}, ${routeContracts.length} route contract link(s).`,
6617
+ summary: `${repos.length} repo(s), ${repos.filter((repo) => repo.indexed).length} with Kage memory, ${packageDependencies.length} workspace package dependenc${packageDependencies.length === 1 ? "y" : "ies"}, ${routeContracts.length} route contract link(s), ${topicContracts.length} topic contract link(s).`,
6490
6618
  };
6491
6619
  }
6492
6620
  function kageWorkspaceRecall(projectDir, query, limit = 8) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kage-core/kage-graph-mcp",
3
- "version": "1.1.30",
3
+ "version": "1.1.32",
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) {
@@ -2613,6 +2619,7 @@
2613
2619
  var repos = Array.isArray(workspace.repos) ? workspace.repos : [];
2614
2620
  var deps = Array.isArray(workspace.package_dependencies) ? workspace.package_dependencies : [];
2615
2621
  var contracts = Array.isArray(workspace.route_contracts) ? workspace.route_contracts : [];
2622
+ var topics = Array.isArray(workspace.topic_contracts) ? workspace.topic_contracts : [];
2616
2623
  cards.push({
2617
2624
  title: "Workspace",
2618
2625
  kicker: "multi-repo memory",
@@ -2621,7 +2628,8 @@
2621
2628
  ["Repos", String(repos.length)],
2622
2629
  ["Indexed", String(repos.filter(function (repo) { return repo.indexed; }).length)],
2623
2630
  ["Package deps", String(deps.length)],
2624
- ["Route links", String(contracts.length)]
2631
+ ["Route links", String(contracts.length)],
2632
+ ["Topic links", String(topics.length)]
2625
2633
  ].concat(repos.slice(0, 2).map(function (repo) { return [repo.alias, repo.approved_packets + " packets, " + repo.code_files + " files"]; }))
2626
2634
  });
2627
2635
  }