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

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
@@ -45,8 +45,8 @@ Restart your agent once after setup so MCP tools reload.
45
45
  - decision intelligence for why-memory coverage, stale/weak packets, and
46
46
  important files that still lack linked repo knowledge
47
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
+ route-contract, topic/event contract, and git co-change links when existing
49
+ local evidence exposes them
50
50
  - local git intelligence for risk, reviewers, contributor profiles, co-change
51
51
  warnings, ownership silos, and module health
52
52
  - `AGENTS.md` bootstrap instructions so agents recall context automatically
package/dist/cli.js CHANGED
@@ -855,6 +855,12 @@ async function main() {
855
855
  console.log(`- ${contract.producer_repo} ${contract.topic} -> ${contract.consumer_repo}/${contract.consumer_file}`);
856
856
  }
857
857
  }
858
+ if (result.co_changes.length) {
859
+ console.log("Cross-repo co-changes:");
860
+ for (const link of result.co_changes.slice(0, 10)) {
861
+ console.log(`- ${link.source_repo}/${link.source_file} <-> ${link.target_repo}/${link.target_file} (${link.frequency}x, strength ${link.strength})`);
862
+ }
863
+ }
858
864
  if (result.warnings.length)
859
865
  console.log(`Warnings:\n${result.warnings.map((warning) => ` - ${warning}`).join("\n")}`);
860
866
  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, package dependencies, route contracts, and topic/event contracts 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, topic/event contracts, and cross-repo co-change links 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
@@ -6577,6 +6577,117 @@ function workspaceTopicContracts(workspaceDir, repos) {
6577
6577
  .sort((a, b) => a.topic.localeCompare(b.topic) || a.producer_repo.localeCompare(b.producer_repo) || a.consumer_repo.localeCompare(b.consumer_repo))
6578
6578
  .slice(0, 50);
6579
6579
  }
6580
+ function workspaceGitCommits(repo, repoRoot, limit = 250) {
6581
+ const graph = readCurrentCodeGraph(repoRoot);
6582
+ const graphPaths = new Set(graph?.files.map((file) => file.path) ?? []);
6583
+ if (!graphPaths.size)
6584
+ return [];
6585
+ const raw = readGit(repoRoot, ["log", `-${limit}`, "--format=__KAGE_COMMIT__%x1f%an <%ae>%x1f%ct%x1f%cI", "--name-only", "--no-renames"]) ?? "";
6586
+ const records = [];
6587
+ let current = null;
6588
+ for (const rawLine of raw.split(/\r?\n/)) {
6589
+ const line = rawLine.trim();
6590
+ if (!line)
6591
+ continue;
6592
+ if (line.startsWith("__KAGE_COMMIT__")) {
6593
+ if (current?.files.length)
6594
+ records.push(current);
6595
+ const [, author = "", timestamp = "0", iso = ""] = line.split("\x1f");
6596
+ current = {
6597
+ repo: repo.alias,
6598
+ author: author.trim(),
6599
+ timestamp: Number(timestamp) || 0,
6600
+ iso: iso.trim() || null,
6601
+ files: [],
6602
+ };
6603
+ continue;
6604
+ }
6605
+ if (current && graphPaths.has(line) && !isNoisePath(line))
6606
+ current.files.push(line);
6607
+ }
6608
+ if (current?.files.length)
6609
+ records.push(current);
6610
+ return records;
6611
+ }
6612
+ function workspaceCoChanges(workspaceDir, repos) {
6613
+ const recordsByAuthor = new Map();
6614
+ for (const repo of repos) {
6615
+ const repoRoot = repo.path === "." ? workspaceDir : (0, node_path_1.join)(workspaceDir, repo.path);
6616
+ for (const record of workspaceGitCommits(repo, repoRoot)) {
6617
+ if (!record.author || !record.timestamp || !record.files.length)
6618
+ continue;
6619
+ const list = recordsByAuthor.get(record.author) ?? [];
6620
+ list.push(record);
6621
+ recordsByAuthor.set(record.author, list);
6622
+ }
6623
+ }
6624
+ const windowSeconds = 24 * 60 * 60;
6625
+ const nowSeconds = Math.floor(Date.now() / 1000);
6626
+ const pairs = new Map();
6627
+ const pairKey = (left, leftFile, right, rightFile) => {
6628
+ const a = { repo: left.repo, file: leftFile };
6629
+ const b = { repo: right.repo, file: rightFile };
6630
+ if (`${a.repo}/${a.file}` <= `${b.repo}/${b.file}`)
6631
+ return { key: `${a.repo}\0${a.file}\0${b.repo}\0${b.file}`, source: a, target: b };
6632
+ return { key: `${b.repo}\0${b.file}\0${a.repo}\0${a.file}`, source: b, target: a };
6633
+ };
6634
+ for (const [author, records] of recordsByAuthor.entries()) {
6635
+ const ordered = records.slice().sort((a, b) => a.timestamp - b.timestamp);
6636
+ for (let i = 0; i < ordered.length; i += 1) {
6637
+ const left = ordered[i];
6638
+ for (let j = i + 1; j < ordered.length; j += 1) {
6639
+ const right = ordered[j];
6640
+ const delta = right.timestamp - left.timestamp;
6641
+ if (delta > windowSeconds)
6642
+ break;
6643
+ if (left.repo === right.repo)
6644
+ continue;
6645
+ const ageDays = Math.max(0, (nowSeconds - Math.max(left.timestamp, right.timestamp)) / 86400);
6646
+ const weight = Number((1 / (1 + ageDays / 180)).toFixed(3));
6647
+ for (const leftFile of unique(left.files).slice(0, 20)) {
6648
+ for (const rightFile of unique(right.files).slice(0, 20)) {
6649
+ const { key, source, target } = pairKey(left, leftFile, right, rightFile);
6650
+ const existing = pairs.get(key) ?? {
6651
+ source_repo: source.repo,
6652
+ source_file: source.file,
6653
+ target_repo: target.repo,
6654
+ target_file: target.file,
6655
+ frequency: 0,
6656
+ strength: 0,
6657
+ last_seen_at: null,
6658
+ last_timestamp: 0,
6659
+ authors: new Set(),
6660
+ };
6661
+ existing.frequency += 1;
6662
+ existing.strength = Number((existing.strength + weight).toFixed(3));
6663
+ existing.authors.add(author);
6664
+ const seenAt = Math.max(left.timestamp, right.timestamp);
6665
+ if (seenAt > existing.last_timestamp) {
6666
+ existing.last_timestamp = seenAt;
6667
+ existing.last_seen_at = right.timestamp >= left.timestamp ? right.iso : left.iso;
6668
+ }
6669
+ pairs.set(key, existing);
6670
+ }
6671
+ }
6672
+ }
6673
+ }
6674
+ }
6675
+ return [...pairs.values()]
6676
+ .map((pair) => ({
6677
+ source_repo: pair.source_repo,
6678
+ source_file: pair.source_file,
6679
+ target_repo: pair.target_repo,
6680
+ target_file: pair.target_file,
6681
+ frequency: pair.frequency,
6682
+ strength: Number(pair.strength.toFixed(3)),
6683
+ last_seen_at: pair.last_seen_at,
6684
+ authors: [...pair.authors].sort(),
6685
+ evidence: `${pair.source_repo}/${pair.source_file} and ${pair.target_repo}/${pair.target_file} changed near each other ${pair.frequency} time(s) by ${pair.authors.size} author(s).`,
6686
+ }))
6687
+ .filter((pair) => pair.frequency > 0)
6688
+ .sort((a, b) => b.strength - a.strength || b.frequency - a.frequency || a.source_repo.localeCompare(b.source_repo) || a.source_file.localeCompare(b.source_file))
6689
+ .slice(0, 50);
6690
+ }
6580
6691
  function kageWorkspace(projectDir) {
6581
6692
  const root = (0, node_path_1.resolve)(projectDir);
6582
6693
  const warnings = [];
@@ -6603,6 +6714,7 @@ function kageWorkspace(projectDir) {
6603
6714
  });
6604
6715
  const routeContracts = workspaceRouteContracts(root, repos);
6605
6716
  const topicContracts = workspaceTopicContracts(root, repos);
6717
+ const coChanges = workspaceCoChanges(root, repos);
6606
6718
  if (repos.length && repos.every((repo) => !repo.indexed))
6607
6719
  warnings.push("Workspace repos were found, but none has .agent_memory yet. Run kage init or kage refresh in each repo you want searchable.");
6608
6720
  return {
@@ -6613,8 +6725,9 @@ function kageWorkspace(projectDir) {
6613
6725
  package_dependencies: packageDependencies.sort((a, b) => a.from.localeCompare(b.from) || a.to.localeCompare(b.to)),
6614
6726
  route_contracts: routeContracts,
6615
6727
  topic_contracts: topicContracts,
6728
+ co_changes: coChanges,
6616
6729
  warnings,
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).`,
6730
+ 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), ${coChanges.length} cross-repo co-change link(s).`,
6618
6731
  };
6619
6732
  }
6620
6733
  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.32",
3
+ "version": "1.1.33",
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
@@ -2620,6 +2620,7 @@
2620
2620
  var deps = Array.isArray(workspace.package_dependencies) ? workspace.package_dependencies : [];
2621
2621
  var contracts = Array.isArray(workspace.route_contracts) ? workspace.route_contracts : [];
2622
2622
  var topics = Array.isArray(workspace.topic_contracts) ? workspace.topic_contracts : [];
2623
+ var coChanges = Array.isArray(workspace.co_changes) ? workspace.co_changes : [];
2623
2624
  cards.push({
2624
2625
  title: "Workspace",
2625
2626
  kicker: "multi-repo memory",
@@ -2629,7 +2630,8 @@
2629
2630
  ["Indexed", String(repos.filter(function (repo) { return repo.indexed; }).length)],
2630
2631
  ["Package deps", String(deps.length)],
2631
2632
  ["Route links", String(contracts.length)],
2632
- ["Topic links", String(topics.length)]
2633
+ ["Topic links", String(topics.length)],
2634
+ ["Co-changes", String(coChanges.length)]
2633
2635
  ].concat(repos.slice(0, 2).map(function (repo) { return [repo.alias, repo.approved_packets + " packets, " + repo.code_files + " files"]; }))
2634
2636
  });
2635
2637
  }