@kage-core/kage-graph-mcp 2.5.2 → 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 CHANGED
@@ -1396,12 +1396,25 @@ async function main() {
1396
1396
  const path = takeArg(args, "--path");
1397
1397
  if (!path)
1398
1398
  usage();
1399
- const result = (0, kernel_js_1.kageFileContext)(projectArg(args), path);
1400
- if (args.includes("--json"))
1401
- console.log(JSON.stringify(result, null, 2));
1402
- else if (result.context_block)
1403
- console.log(result.context_block);
1404
- // No verified packets cite this file: print nothing so hooks can gate on empty output.
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") {
package/dist/kernel.js CHANGED
@@ -1203,6 +1203,18 @@ function replayTokensSaved(packets, contextBlock) {
1203
1203
  return Math.max(0, discovery - estimateTokens(contextBlock));
1204
1204
  }
1205
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
+ }
1206
1218
  // PreToolUse(Read) injection: verified memory at the moment of relevance. Returns at
1207
1219
  // most three currently-verified packets that cite the file an agent is about to read,
1208
1220
  // as a compact context block. Reuses the same staleness machinery as recall — a packet
@@ -1216,12 +1228,7 @@ function kageFileContext(projectDir, filePath) {
1216
1228
  packets: [],
1217
1229
  context_block: "",
1218
1230
  };
1219
- if (!filePath || !filePath.trim())
1220
- return result;
1221
- let rel = filePath.trim().replace(/\\/g, "/");
1222
- if ((0, node_path_1.isAbsolute)(rel))
1223
- rel = (0, node_path_1.relative)((0, node_path_1.resolve)(projectDir), rel).replace(/\\/g, "/");
1224
- rel = rel.replace(/^\.\//, "").replace(/^\/+/, "");
1231
+ const rel = normalizeRepoPath(projectDir, filePath);
1225
1232
  result.path = rel;
1226
1233
  // Files outside the project (or an uninitialized project) never produce context.
1227
1234
  if (!rel || rel.startsWith("..") || !(0, node_fs_1.existsSync)(memoryRoot(projectDir)))
@@ -7965,11 +7972,15 @@ function isSerializedDumpBody(body) {
7965
7972
  return isSerializedDumpTitle(t)
7966
7973
  || /<task-notification\b|<tool-use-id\b|"hookSpecificOutput"|"isImage"\s*:|"noOutputExpected"\s*:|"interrupted"\s*:\s*(true|false)/i.test(t.slice(0, 4000));
7967
7974
  }
7968
- // Surface watcher nudges: read the nudge inbox the kage-watcher writes, return the
7969
- // unsurfaced ones as a prominent block, and mark them surfaced so each shows exactly once.
7970
- // This is what makes recall/nudges FELT the UserPromptSubmit hook injects this block, so
7971
- // the agent sees "act on this" memory instead of it sitting silently in a file.
7972
- function surfacePendingNudges(projectDir) {
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 = {}) {
7973
7984
  const path = (0, node_path_1.join)(projectDir, ".agent_memory", "nudges", "pending.jsonl");
7974
7985
  if (!(0, node_fs_1.existsSync)(path))
7975
7986
  return { block: "", items: [], count: 0 };
@@ -7992,7 +8003,11 @@ function surfacePendingNudges(projectDir) {
7992
8003
  }
7993
8004
  catch { /* drop malformed lines */ }
7994
8005
  }
7995
- const unsurfaced = records.filter((record) => record.surfaced !== true && typeof record.message === "string");
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)));
7996
8011
  if (!unsurfaced.length)
7997
8012
  return { block: "", items: [], count: 0 };
7998
8013
  const items = unsurfaced.map((record) => ({
@@ -8002,8 +8017,13 @@ function surfacePendingNudges(projectDir) {
8002
8017
  }));
8003
8018
  for (const record of unsurfaced)
8004
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.
8005
8023
  try {
8006
- (0, node_fs_1.writeFileSync)(path, `${records.map((record) => JSON.stringify(record)).join("\n")}\n`, "utf8");
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);
8007
8027
  }
8008
8028
  catch { /* best-effort; recall still works */ }
8009
8029
  const block = ["", "## ⚠ Kage nudges — act on these before proceeding",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kage-core/kage-graph-mcp",
3
- "version": "2.5.2",
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": [