@kage-core/kage-graph-mcp 1.1.32 → 1.1.34
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 +6 -4
- package/dist/cli.js +6 -0
- package/dist/index.js +1 -1
- package/dist/kernel.js +114 -1
- package/package.json +1 -1
- package/viewer/app.js +55 -1
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,
|
|
49
|
-
|
|
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
|
|
@@ -165,8 +165,10 @@ kage viewer --project .
|
|
|
165
165
|
|
|
166
166
|
The local viewer loads graph artifacts plus `.agent_memory/reports/*.json` and
|
|
167
167
|
shows a repo-intelligence cockpit for memory-code links, decision memory, risk,
|
|
168
|
-
contributors, module health, graph insights, workspace coverage,
|
|
169
|
-
benchmark proof.
|
|
168
|
+
contributors, module health, graph insights, workspace coverage, workspace
|
|
169
|
+
link maps, quality, and benchmark proof. Workspace Map rows expose package
|
|
170
|
+
dependencies, route contracts, topic/event links, and cross-repo co-change
|
|
171
|
+
pairs from local workspace reports.
|
|
170
172
|
|
|
171
173
|
Hosted demo:
|
|
172
174
|
|
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,
|
|
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
package/viewer/app.js
CHANGED
|
@@ -2321,6 +2321,7 @@
|
|
|
2321
2321
|
var decisions = reports.decisions;
|
|
2322
2322
|
var health = reports.moduleHealth;
|
|
2323
2323
|
var insights = reports.graphInsights;
|
|
2324
|
+
var workspace = reports.workspace;
|
|
2324
2325
|
|
|
2325
2326
|
if (contributors || risk) {
|
|
2326
2327
|
var profiles = contributors && Array.isArray(contributors.contributors) ? contributors.contributors : [];
|
|
@@ -2449,6 +2450,57 @@
|
|
|
2449
2450
|
});
|
|
2450
2451
|
}
|
|
2451
2452
|
|
|
2453
|
+
if (workspace) {
|
|
2454
|
+
var deps = Array.isArray(workspace.package_dependencies) ? workspace.package_dependencies : [];
|
|
2455
|
+
var routeContracts = Array.isArray(workspace.route_contracts) ? workspace.route_contracts : [];
|
|
2456
|
+
var topicContracts = Array.isArray(workspace.topic_contracts) ? workspace.topic_contracts : [];
|
|
2457
|
+
var coChanges = Array.isArray(workspace.co_changes) ? workspace.co_changes : [];
|
|
2458
|
+
var workspaceRows = deps.slice(0, 6).map(function (dep) {
|
|
2459
|
+
return {
|
|
2460
|
+
label: dep.from + " -> " + dep.to,
|
|
2461
|
+
value: dep.package_name || "package",
|
|
2462
|
+
meta: "workspace package dependency",
|
|
2463
|
+
score: 72,
|
|
2464
|
+
status: "ok",
|
|
2465
|
+
};
|
|
2466
|
+
}).concat(routeContracts.slice(0, 6).map(function (contract) {
|
|
2467
|
+
return {
|
|
2468
|
+
label: contract.provider_repo + " -> " + contract.consumer_repo,
|
|
2469
|
+
value: [contract.method, contract.path].filter(Boolean).join(" "),
|
|
2470
|
+
meta: [contract.provider_file, contract.consumer_file].filter(Boolean).join(" -> "),
|
|
2471
|
+
score: contract.confidence === "high" ? 92 : 76,
|
|
2472
|
+
status: "ok",
|
|
2473
|
+
};
|
|
2474
|
+
})).concat(topicContracts.slice(0, 6).map(function (contract) {
|
|
2475
|
+
return {
|
|
2476
|
+
label: contract.producer_repo + " -> " + contract.consumer_repo,
|
|
2477
|
+
value: contract.topic,
|
|
2478
|
+
meta: [contract.producer_file, contract.consumer_file].filter(Boolean).join(" -> "),
|
|
2479
|
+
score: contract.confidence === "high" ? 88 : 72,
|
|
2480
|
+
status: "ok",
|
|
2481
|
+
};
|
|
2482
|
+
})).concat(coChanges.slice(0, 8).map(function (link) {
|
|
2483
|
+
var score = Math.min(100, Number(link.strength || 0) * 22 + Number(link.frequency || 0) * 12);
|
|
2484
|
+
return {
|
|
2485
|
+
label: link.source_repo + " <-> " + link.target_repo,
|
|
2486
|
+
value: (link.frequency || 0) + "x co-change",
|
|
2487
|
+
meta: [link.source_file, link.target_file].filter(Boolean).join(" <-> "),
|
|
2488
|
+
score: Math.max(20, score),
|
|
2489
|
+
status: "warn",
|
|
2490
|
+
};
|
|
2491
|
+
}));
|
|
2492
|
+
if (workspaceRows.length) {
|
|
2493
|
+
sections.push({
|
|
2494
|
+
title: "Workspace Map",
|
|
2495
|
+
kicker: "deps / contracts / co-changes",
|
|
2496
|
+
stat: workspaceRows.length + " links",
|
|
2497
|
+
summary: "Workspace links show how sibling repos relate through package dependencies, source-evidence contracts, topic/event links, and local git co-change history.",
|
|
2498
|
+
rows: workspaceRows,
|
|
2499
|
+
limit: 14,
|
|
2500
|
+
});
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2452
2504
|
if (risk) {
|
|
2453
2505
|
var targets = Array.isArray(risk.targets) ? risk.targets : Object.keys(risk.targets || {}).map(function (key) { return risk.targets[key]; });
|
|
2454
2506
|
var hotspots = Array.isArray(risk.global_hotspots) ? risk.global_hotspots : [];
|
|
@@ -2620,6 +2672,7 @@
|
|
|
2620
2672
|
var deps = Array.isArray(workspace.package_dependencies) ? workspace.package_dependencies : [];
|
|
2621
2673
|
var contracts = Array.isArray(workspace.route_contracts) ? workspace.route_contracts : [];
|
|
2622
2674
|
var topics = Array.isArray(workspace.topic_contracts) ? workspace.topic_contracts : [];
|
|
2675
|
+
var coChanges = Array.isArray(workspace.co_changes) ? workspace.co_changes : [];
|
|
2623
2676
|
cards.push({
|
|
2624
2677
|
title: "Workspace",
|
|
2625
2678
|
kicker: "multi-repo memory",
|
|
@@ -2629,7 +2682,8 @@
|
|
|
2629
2682
|
["Indexed", String(repos.filter(function (repo) { return repo.indexed; }).length)],
|
|
2630
2683
|
["Package deps", String(deps.length)],
|
|
2631
2684
|
["Route links", String(contracts.length)],
|
|
2632
|
-
["Topic links", String(topics.length)]
|
|
2685
|
+
["Topic links", String(topics.length)],
|
|
2686
|
+
["Co-changes", String(coChanges.length)]
|
|
2633
2687
|
].concat(repos.slice(0, 2).map(function (repo) { return [repo.alias, repo.approved_packets + " packets, " + repo.code_files + " files"]; }))
|
|
2634
2688
|
});
|
|
2635
2689
|
}
|