@kage-core/kage-graph-mcp 2.5.1 → 2.5.3
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/dist/cli.js +39 -17
- package/dist/kernel.js +76 -7
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1396,12 +1396,25 @@ async function main() {
|
|
|
1396
1396
|
const path = takeArg(args, "--path");
|
|
1397
1397
|
if (!path)
|
|
1398
1398
|
usage();
|
|
1399
|
-
const
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1399
|
+
const project = projectArg(args);
|
|
1400
|
+
const result = (0, kernel_js_1.kageFileContext)(project, path);
|
|
1401
|
+
// Surface any watcher nudge targeting THIS file at the edit/read moment (marks it
|
|
1402
|
+
// surfaced so it shows once) — the highest-value trigger for a nudge. prompt-context
|
|
1403
|
+
// still drains file-less / not-yet-touched nudges on the next prompt.
|
|
1404
|
+
const nudges = (0, kernel_js_1.surfacePendingNudges)(project, { path });
|
|
1405
|
+
if (args.includes("--json")) {
|
|
1406
|
+
console.log(JSON.stringify({ ...result, nudges: nudges.items }, null, 2));
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
const parts = [];
|
|
1410
|
+
if (result.context_block)
|
|
1411
|
+
parts.push(result.context_block);
|
|
1412
|
+
if (nudges.block)
|
|
1413
|
+
parts.push(nudges.block.trimStart());
|
|
1414
|
+
if (parts.length)
|
|
1415
|
+
console.log(parts.join("\n\n"));
|
|
1416
|
+
// No verified packets cite this file and no nudge targets it: print nothing so hooks
|
|
1417
|
+
// can gate on empty output.
|
|
1405
1418
|
return;
|
|
1406
1419
|
}
|
|
1407
1420
|
if (command === "prompt-context") {
|
|
@@ -1411,21 +1424,30 @@ async function main() {
|
|
|
1411
1424
|
const query = takeArg(args, "--query") ?? firstPositional(args);
|
|
1412
1425
|
if (!query)
|
|
1413
1426
|
usage();
|
|
1414
|
-
const
|
|
1427
|
+
const project = projectArg(args);
|
|
1428
|
+
const result = (0, kernel_js_1.recall)(project, query, 5, false, {});
|
|
1429
|
+
// Surface any nudges the kage-watcher wrote (marks them surfaced so each shows once) —
|
|
1430
|
+
// this is what makes recall/nudges felt instead of silently sitting in a file.
|
|
1431
|
+
const nudges = (0, kernel_js_1.surfacePendingNudges)(project);
|
|
1415
1432
|
if (args.includes("--json")) {
|
|
1416
|
-
console.log(JSON.stringify(result, null, 2));
|
|
1433
|
+
console.log(JSON.stringify({ ...result, nudges: nudges.items }, null, 2));
|
|
1417
1434
|
return;
|
|
1418
1435
|
}
|
|
1419
|
-
|
|
1436
|
+
const parts = [];
|
|
1437
|
+
if (result.results.length) {
|
|
1438
|
+
// Lead with felt behavior, not a vanity/gameable token number: tell the agent these are
|
|
1439
|
+
// verified team memories to follow. (The tokens/$ ledger lives in `kage gains`.) Keep
|
|
1440
|
+
// only the trust-relevant stale-withheld signal here.
|
|
1441
|
+
const plural = result.results.length === 1 ? "y" : "ies";
|
|
1442
|
+
const withheld = result.value_receipt?.stale_withheld ?? 0;
|
|
1443
|
+
parts.push(result.context_block);
|
|
1444
|
+
parts.push(`_${result.results.length} verified team memor${plural} above — follow them.${withheld ? ` ${withheld} stale memor${withheld === 1 ? "y" : "ies"} withheld (code changed under them).` : ""}_`);
|
|
1445
|
+
}
|
|
1446
|
+
if (nudges.block)
|
|
1447
|
+
parts.push(nudges.block.trimStart());
|
|
1448
|
+
if (!parts.length)
|
|
1420
1449
|
return;
|
|
1421
|
-
|
|
1422
|
-
// Lead with felt behavior, not a vanity/gameable token number: tell the agent these are
|
|
1423
|
-
// verified team memories to follow. (The tokens/$ ledger lives in `kage gains` for anyone
|
|
1424
|
-
// who wants it.) Keep only the trust-relevant stale-withheld signal here.
|
|
1425
|
-
const plural = result.results.length === 1 ? "y" : "ies";
|
|
1426
|
-
const withheld = result.value_receipt?.stale_withheld ?? 0;
|
|
1427
|
-
out += `\n\n_${result.results.length} verified team memor${plural} above — follow them.${withheld ? ` ${withheld} stale memor${withheld === 1 ? "y" : "ies"} withheld (code changed under them).` : ""}_`;
|
|
1428
|
-
console.log(out);
|
|
1450
|
+
console.log(parts.join("\n\n"));
|
|
1429
1451
|
return;
|
|
1430
1452
|
}
|
|
1431
1453
|
if (command === "memory-access") {
|
package/dist/kernel.js
CHANGED
|
@@ -102,6 +102,7 @@ exports.compactProject = compactProject;
|
|
|
102
102
|
exports.installAgentPolicy = installAgentPolicy;
|
|
103
103
|
exports.createDenseEmbeddingProvider = createDenseEmbeddingProvider;
|
|
104
104
|
exports.buildEmbeddingIndex = buildEmbeddingIndex;
|
|
105
|
+
exports.surfacePendingNudges = surfacePendingNudges;
|
|
105
106
|
exports.recall = recall;
|
|
106
107
|
exports.recallWithEmbeddings = recallWithEmbeddings;
|
|
107
108
|
exports.queryCodeGraph = queryCodeGraph;
|
|
@@ -1202,6 +1203,18 @@ function replayTokensSaved(packets, contextBlock) {
|
|
|
1202
1203
|
return Math.max(0, discovery - estimateTokens(contextBlock));
|
|
1203
1204
|
}
|
|
1204
1205
|
const FILE_CONTEXT_PACKET_CAP = 3;
|
|
1206
|
+
// Normalize a file path to the repo-relative form used for packet citation matching:
|
|
1207
|
+
// absolute paths are made relative to the project root, backslashes/leading "./" are
|
|
1208
|
+
// stripped. Returns "" for an empty/blank input. Shared by file-context citation lookup
|
|
1209
|
+
// and edit-time nudge matching so both compare paths identically.
|
|
1210
|
+
function normalizeRepoPath(projectDir, filePath) {
|
|
1211
|
+
let rel = (filePath ?? "").trim().replace(/\\/g, "/");
|
|
1212
|
+
if (!rel)
|
|
1213
|
+
return "";
|
|
1214
|
+
if ((0, node_path_1.isAbsolute)(rel))
|
|
1215
|
+
rel = (0, node_path_1.relative)((0, node_path_1.resolve)(projectDir), rel).replace(/\\/g, "/");
|
|
1216
|
+
return rel.replace(/^\.\//, "").replace(/^\/+/, "").replace(/\/+$/, "");
|
|
1217
|
+
}
|
|
1205
1218
|
// PreToolUse(Read) injection: verified memory at the moment of relevance. Returns at
|
|
1206
1219
|
// most three currently-verified packets that cite the file an agent is about to read,
|
|
1207
1220
|
// as a compact context block. Reuses the same staleness machinery as recall — a packet
|
|
@@ -1215,12 +1228,7 @@ function kageFileContext(projectDir, filePath) {
|
|
|
1215
1228
|
packets: [],
|
|
1216
1229
|
context_block: "",
|
|
1217
1230
|
};
|
|
1218
|
-
|
|
1219
|
-
return result;
|
|
1220
|
-
let rel = filePath.trim().replace(/\\/g, "/");
|
|
1221
|
-
if ((0, node_path_1.isAbsolute)(rel))
|
|
1222
|
-
rel = (0, node_path_1.relative)((0, node_path_1.resolve)(projectDir), rel).replace(/\\/g, "/");
|
|
1223
|
-
rel = rel.replace(/^\.\//, "").replace(/^\/+/, "");
|
|
1231
|
+
const rel = normalizeRepoPath(projectDir, filePath);
|
|
1224
1232
|
result.path = rel;
|
|
1225
1233
|
// Files outside the project (or an uninitialized project) never produce context.
|
|
1226
1234
|
if (!rel || rel.startsWith("..") || !(0, node_fs_1.existsSync)(memoryRoot(projectDir)))
|
|
@@ -7942,7 +7950,10 @@ function isSerializedDumpTitle(title) {
|
|
|
7942
7950
|
return /^(workflow|runbook)\s*:?\s*[{[]/i.test(t)
|
|
7943
7951
|
|| t.startsWith('{"')
|
|
7944
7952
|
|| /^<(task-notification|div|svg|html)\b/i.test(t)
|
|
7945
|
-
|| /\btool_use_id\b|toolu_[A-Za-z0-9]{10}/.test(title)
|
|
7953
|
+
|| /\btool_use_id\b|toolu_[A-Za-z0-9]{10}/.test(title)
|
|
7954
|
+
// Shell-prompt / terminal paste, e.g. "user@host dir % cmd" or "user@host:path$ cmd" —
|
|
7955
|
+
// a pasted command transcript, not a durable learning.
|
|
7956
|
+
|| /^[\w.+-]+@[\w.+-]+[\s:].*[%$#]\s/.test(t);
|
|
7946
7957
|
}
|
|
7947
7958
|
// Durable-learning size ceiling. A memory packet body is a distilled insight, not a
|
|
7948
7959
|
// document; anything past this is almost certainly a raw transcript, file-content, or
|
|
@@ -7961,6 +7972,64 @@ function isSerializedDumpBody(body) {
|
|
|
7961
7972
|
return isSerializedDumpTitle(t)
|
|
7962
7973
|
|| /<task-notification\b|<tool-use-id\b|"hookSpecificOutput"|"isImage"\s*:|"noOutputExpected"\s*:|"interrupted"\s*:\s*(true|false)/i.test(t.slice(0, 4000));
|
|
7963
7974
|
}
|
|
7975
|
+
// Surface watcher nudges: read the nudge inbox the kage-watcher writes (.agent_memory/
|
|
7976
|
+
// nudges/pending.jsonl), return the unsurfaced ones as a prominent block, and mark them
|
|
7977
|
+
// surfaced so each shows exactly once. This is what makes recall/nudges FELT instead of
|
|
7978
|
+
// sitting silently in a file. Two trigger points share this: prompt-context
|
|
7979
|
+
// (UserPromptSubmit) passes no filter and drains every pending nudge; file-context
|
|
7980
|
+
// (PreToolUse Edit/Read) passes opts.path so only a nudge targeting the file being touched
|
|
7981
|
+
// fires — surfacing the warning at the edit moment, the highest-value trigger. A file-less
|
|
7982
|
+
// nudge has no edit moment, so it waits for the next prompt-context drain.
|
|
7983
|
+
function surfacePendingNudges(projectDir, opts = {}) {
|
|
7984
|
+
const path = (0, node_path_1.join)(projectDir, ".agent_memory", "nudges", "pending.jsonl");
|
|
7985
|
+
if (!(0, node_fs_1.existsSync)(path))
|
|
7986
|
+
return { block: "", items: [], count: 0 };
|
|
7987
|
+
let raw;
|
|
7988
|
+
try {
|
|
7989
|
+
raw = (0, node_fs_1.readFileSync)(path, "utf8");
|
|
7990
|
+
}
|
|
7991
|
+
catch {
|
|
7992
|
+
return { block: "", items: [], count: 0 };
|
|
7993
|
+
}
|
|
7994
|
+
const records = [];
|
|
7995
|
+
for (const line of raw.split("\n")) {
|
|
7996
|
+
const trimmed = line.trim();
|
|
7997
|
+
if (!trimmed)
|
|
7998
|
+
continue;
|
|
7999
|
+
try {
|
|
8000
|
+
const parsed = JSON.parse(trimmed);
|
|
8001
|
+
if (parsed && typeof parsed === "object")
|
|
8002
|
+
records.push(parsed);
|
|
8003
|
+
}
|
|
8004
|
+
catch { /* drop malformed lines */ }
|
|
8005
|
+
}
|
|
8006
|
+
const wantPath = opts.path !== undefined ? normalizeRepoPath(projectDir, opts.path) : undefined;
|
|
8007
|
+
const unsurfaced = records.filter((record) => record.surfaced !== true &&
|
|
8008
|
+
typeof record.message === "string" &&
|
|
8009
|
+
(wantPath === undefined ||
|
|
8010
|
+
(typeof record.file === "string" && normalizeRepoPath(projectDir, record.file) === wantPath)));
|
|
8011
|
+
if (!unsurfaced.length)
|
|
8012
|
+
return { block: "", items: [], count: 0 };
|
|
8013
|
+
const items = unsurfaced.map((record) => ({
|
|
8014
|
+
message: String(record.message),
|
|
8015
|
+
file: typeof record.file === "string" ? record.file : undefined,
|
|
8016
|
+
kind: typeof record.kind === "string" ? record.kind : undefined,
|
|
8017
|
+
}));
|
|
8018
|
+
for (const record of unsurfaced)
|
|
8019
|
+
record.surfaced = true;
|
|
8020
|
+
// Atomic write (tmp + rename): file-context now rewrites this inbox on every edit, so the
|
|
8021
|
+
// window where prompt-context could read a half-written file is wider. rename is atomic, so
|
|
8022
|
+
// a concurrent reader sees either the old or the new file, never a torn one.
|
|
8023
|
+
try {
|
|
8024
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
8025
|
+
(0, node_fs_1.writeFileSync)(tmp, `${records.map((record) => JSON.stringify(record)).join("\n")}\n`, "utf8");
|
|
8026
|
+
(0, node_fs_1.renameSync)(tmp, path);
|
|
8027
|
+
}
|
|
8028
|
+
catch { /* best-effort; recall still works */ }
|
|
8029
|
+
const block = ["", "## ⚠ Kage nudges — act on these before proceeding",
|
|
8030
|
+
...items.map((item) => `- ${item.file ? `${item.file}: ` : ""}${item.message}`)].join("\n");
|
|
8031
|
+
return { block, items, count: items.length };
|
|
8032
|
+
}
|
|
7964
8033
|
// Collapse whitespace and hard-cap a value rendered inline in a context block, so one
|
|
7965
8034
|
// oversized field (e.g. a graph fact whose body is a raw transcript) can never blow up
|
|
7966
8035
|
// the assembled output — the 270k-char overflow that motivated this guard.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kage-core/kage-graph-mcp",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.3",
|
|
4
4
|
"description": "Team memory for coding agents: captures the decisions, runbooks, and bug fixes that get lost, verified against your code and shared via git. MCP server, zero deps, no account.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"files": [
|