@kage-core/kage-graph-mcp 1.1.31 → 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 +3 -2
- package/dist/cli.js +12 -0
- package/dist/index.js +1 -1
- package/dist/kernel.js +206 -1
- package/package.json +1 -1
- package/viewer/app.js +5 -1
package/README.md
CHANGED
|
@@ -44,8 +44,9 @@ Restart your agent once after setup so MCP tools reload.
|
|
|
44
44
|
- memory-code links so project knowledge points at the code it affects
|
|
45
45
|
- decision intelligence for why-memory coverage, stale/weak packets, and
|
|
46
46
|
important files that still lack linked repo knowledge
|
|
47
|
-
- lightweight workspace recall across sibling repos, including package
|
|
48
|
-
route-contract
|
|
47
|
+
- lightweight workspace recall across sibling repos, including package,
|
|
48
|
+
route-contract, topic/event contract, and git co-change links when existing
|
|
49
|
+
local evidence exposes them
|
|
49
50
|
- local git intelligence for risk, reviewers, contributor profiles, co-change
|
|
50
51
|
warnings, ownership silos, and module health
|
|
51
52
|
- `AGENTS.md` bootstrap instructions so agents recall context automatically
|
package/dist/cli.js
CHANGED
|
@@ -849,6 +849,18 @@ 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
|
+
}
|
|
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
|
+
}
|
|
852
864
|
if (result.warnings.length)
|
|
853
865
|
console.log(`Warnings:\n${result.warnings.map((warning) => ` - ${warning}`).join("\n")}`);
|
|
854
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,
|
|
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
|
@@ -6487,6 +6487,207 @@ function workspaceRouteContracts(workspaceDir, repos) {
|
|
|
6487
6487
|
.sort((a, b) => a.provider_repo.localeCompare(b.provider_repo) || a.path.localeCompare(b.path) || a.consumer_repo.localeCompare(b.consumer_repo))
|
|
6488
6488
|
.slice(0, 50);
|
|
6489
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
|
+
}
|
|
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
|
+
}
|
|
6490
6691
|
function kageWorkspace(projectDir) {
|
|
6491
6692
|
const root = (0, node_path_1.resolve)(projectDir);
|
|
6492
6693
|
const warnings = [];
|
|
@@ -6512,6 +6713,8 @@ function kageWorkspace(projectDir) {
|
|
|
6512
6713
|
return { ...rest, dependencies_on_workspace_repos: deps };
|
|
6513
6714
|
});
|
|
6514
6715
|
const routeContracts = workspaceRouteContracts(root, repos);
|
|
6716
|
+
const topicContracts = workspaceTopicContracts(root, repos);
|
|
6717
|
+
const coChanges = workspaceCoChanges(root, repos);
|
|
6515
6718
|
if (repos.length && repos.every((repo) => !repo.indexed))
|
|
6516
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.");
|
|
6517
6720
|
return {
|
|
@@ -6521,8 +6724,10 @@ function kageWorkspace(projectDir) {
|
|
|
6521
6724
|
repos,
|
|
6522
6725
|
package_dependencies: packageDependencies.sort((a, b) => a.from.localeCompare(b.from) || a.to.localeCompare(b.to)),
|
|
6523
6726
|
route_contracts: routeContracts,
|
|
6727
|
+
topic_contracts: topicContracts,
|
|
6728
|
+
co_changes: coChanges,
|
|
6524
6729
|
warnings,
|
|
6525
|
-
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).`,
|
|
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).`,
|
|
6526
6731
|
};
|
|
6527
6732
|
}
|
|
6528
6733
|
function kageWorkspaceRecall(projectDir, query, limit = 8) {
|
package/package.json
CHANGED
package/viewer/app.js
CHANGED
|
@@ -2619,6 +2619,8 @@
|
|
|
2619
2619
|
var repos = Array.isArray(workspace.repos) ? workspace.repos : [];
|
|
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
|
+
var topics = Array.isArray(workspace.topic_contracts) ? workspace.topic_contracts : [];
|
|
2623
|
+
var coChanges = Array.isArray(workspace.co_changes) ? workspace.co_changes : [];
|
|
2622
2624
|
cards.push({
|
|
2623
2625
|
title: "Workspace",
|
|
2624
2626
|
kicker: "multi-repo memory",
|
|
@@ -2627,7 +2629,9 @@
|
|
|
2627
2629
|
["Repos", String(repos.length)],
|
|
2628
2630
|
["Indexed", String(repos.filter(function (repo) { return repo.indexed; }).length)],
|
|
2629
2631
|
["Package deps", String(deps.length)],
|
|
2630
|
-
["Route links", String(contracts.length)]
|
|
2632
|
+
["Route links", String(contracts.length)],
|
|
2633
|
+
["Topic links", String(topics.length)],
|
|
2634
|
+
["Co-changes", String(coChanges.length)]
|
|
2631
2635
|
].concat(repos.slice(0, 2).map(function (repo) { return [repo.alias, repo.approved_packets + " packets, " + repo.code_files + " files"]; }))
|
|
2632
2636
|
});
|
|
2633
2637
|
}
|