@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 +6 -4
- package/dist/cli.js +6 -0
- package/dist/index.js +1 -1
- package/dist/kernel.js +137 -9
- package/package.json +1 -1
- package/viewer/app.js +13 -5
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,
|
|
40
|
-
including generic call/test signals and mixed-language
|
|
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
|
|
47
|
-
route-contract links when existing code graphs
|
|
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,
|
|
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({
|
|
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({
|
|
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
|
-
|
|
3177
|
-
|
|
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
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
|
-
|
|
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
|
}
|