@kage-core/kage-graph-mcp 2.0.2 → 2.2.0

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
@@ -24,6 +24,7 @@ Core commands:
24
24
  kage verify --project <dir> check memory citations against code
25
25
  kage setup <agent> --project <dir> --write wire your agent (claude-code, codex, cursor, ...)
26
26
  kage doctor --project <dir> health check
27
+ kage repair --project <dir> fix what doctor finds (indexes, broken packets, wiring)
27
28
  kage viewer --project <dir> local dashboard
28
29
 
29
30
  Run 'kage help --all' for the full command list (lifecycle, CI, benchmarks, daemon, workspace).`;
@@ -37,6 +38,7 @@ Usage:
37
38
  kage init --project <dir> [--with-policy]
38
39
  kage policy --project <dir>
39
40
  kage doctor --project <dir>
41
+ kage repair --project <dir> [--json]
40
42
  kage setup list
41
43
  kage setup <agent> --project <dir> [--write] [--json]
42
44
  kage setup doctor --project <dir> [--json]
@@ -49,7 +51,8 @@ Usage:
49
51
  kage hook install --project <dir> [--json]
50
52
  kage hook status --project <dir> [--json]
51
53
  kage hook uninstall --project <dir> [--json]
52
- kage refresh --project <dir> [--full] [--json]
54
+ kage refresh --project <dir> [--full] [--force] [--json]
55
+ kage merge-packet <ours> <base> <theirs> git merge driver for .agent_memory/packets/*.json
53
56
  kage gc --project <dir> [--dry-run] [--force] [--json]
54
57
  kage compact --project <dir> [--dry-run] [--json]
55
58
  kage verify --project <dir> [--id <packet-id>] [--json]
@@ -83,6 +86,7 @@ Usage:
83
86
  kage workspace --project <workspace-dir> [--json]
84
87
  kage workspace recall "<query>" --project <workspace-dir> [--json]
85
88
  kage audit --project <dir> [--json]
89
+ kage audit-claude-mem [--store <path>] [--project <dir>] [--json]
86
90
  kage inbox --project <dir> [--json]
87
91
  kage quality --project <dir> [--json]
88
92
  kage benchmark --project <dir> [--json]
@@ -104,11 +108,16 @@ Usage:
104
108
  kage graph-registry --project <dir> [--json]
105
109
  kage embeddings build --project <dir> [--model Xenova/all-MiniLM-L6-v2] [--json]
106
110
  kage recall "<query>" --project <dir> [--json] [--explain] [--embeddings] [--max-context-tokens <n>] [--structural-hops <n>]
111
+ kage file-context --project <dir> --path <file> [--json]
107
112
  kage observe --project <dir> --event <json>
108
113
  kage sessions --project <dir> [--json]
109
114
  kage replay --project <dir> [--session <id>] [--limit <n>] [--json]
110
- kage distill --project <dir> --session <id>
111
- kage learn --project <dir> --learning <text> [--title <title>] [--type <type>] [--evidence <text>] [--verified-by <text>] [--tags a,b] [--paths a,b] [--graph-nodes a,b] [--allow-missing-paths]
115
+ kage distill --project <dir> --session <id> [--auto] [--json]
116
+ kage resume --project <dir> [--json]
117
+ kage learn --project <dir> --learning <text> [--personal] [--title <title>] [--type <type>] [--evidence <text>] [--verified-by <text>] [--tags a,b] [--paths a,b] [--graph-nodes a,b] [--discovery-tokens <n>] [--allow-missing-paths]
118
+ kage sync setup --remote <git-url> init ~/.kage/memory as a git repo wired to your private remote
119
+ kage sync [--json] commit + pull --rebase + push personal memory (newest-wins conflicts)
120
+ kage sync --status [--json] ahead/behind/dirty for the personal store (fetch only)
112
121
  kage feedback --project <dir> --packet <packet-id> --kind helpful|wrong|stale
113
122
  kage capture --project <dir> --title <title> --body <body> [--type <type>] [--summary <summary>] [--tags a,b] [--paths a,b] [--stack a,b] [--graph-nodes a,b] [--allow-missing-paths]
114
123
  kage propose --project <dir> --from-diff
@@ -204,6 +213,74 @@ async function main() {
204
213
  console.log(args.includes("--all") ? FULL_USAGE : CORE_USAGE);
205
214
  return;
206
215
  }
216
+ if (command === "merge-packet") {
217
+ // Git merge driver (%A %O %B): runs before any heavy setup — merge drivers
218
+ // must be fast and dependency-free. Exit 0 = merged, 1 = leave conflict.
219
+ const [ours, base, theirs] = [args[1], args[2], args[3]];
220
+ if (!ours || !base || !theirs) {
221
+ console.error("Usage: kage merge-packet <ours> <base> <theirs>");
222
+ console.error(`Enable once per clone: ${kernel_js_1.PACKET_MERGE_DRIVER_CONFIG}`);
223
+ process.exit(1);
224
+ }
225
+ const result = (0, kernel_js_1.mergePacketFiles)(ours, base, theirs);
226
+ console.error(result.detail);
227
+ process.exit(result.ok ? 0 : 1);
228
+ }
229
+ if (command === "sync") {
230
+ // Personal-store sync (docs/CLOUD.md v1): plain git under the hood, no
231
+ // tree-sitter or repo indexes needed, so it runs before the heavy setup.
232
+ if (args[1] === "setup") {
233
+ const remote = takeArg(args, "--remote");
234
+ if (!remote) {
235
+ console.error("Usage: kage sync setup --remote <git-url>");
236
+ process.exit(2);
237
+ }
238
+ const result = (0, kernel_js_1.syncSetup)(remote);
239
+ if (args.includes("--json")) {
240
+ console.log(JSON.stringify(result, null, 2));
241
+ process.exit(result.ok ? 0 : 2);
242
+ }
243
+ if (!result.ok) {
244
+ console.error(`kage sync setup failed:\n${result.errors.map((error) => ` - ${error}`).join("\n")}`);
245
+ process.exit(2);
246
+ }
247
+ console.log(`Personal memory store: ${result.memory_dir}${result.initialized ? " (new git repo)" : ""}`);
248
+ console.log(`Remote: ${result.remote}${result.remote_updated ? " (updated)" : ""}`);
249
+ console.log(`Pushed ${result.branch ?? "HEAD"} to origin. Run \`kage sync\` on any machine to stay in sync.`);
250
+ return;
251
+ }
252
+ if (args.includes("--status")) {
253
+ const result = (0, kernel_js_1.syncStatus)();
254
+ if (args.includes("--json")) {
255
+ console.log(JSON.stringify(result, null, 2));
256
+ process.exit(result.ok ? 0 : 2);
257
+ }
258
+ if (!result.ok) {
259
+ console.error(`kage sync status failed:\n${result.errors.map((error) => ` - ${error}`).join("\n")}`);
260
+ process.exit(2);
261
+ }
262
+ console.log(`Personal memory store: ${result.memory_dir}`);
263
+ console.log(`Remote: ${result.remote} (branch ${result.branch ?? "unknown"})`);
264
+ console.log(`Ahead ${result.ahead}, behind ${result.behind}, ${result.dirty ? "uncommitted local changes" : "clean"}`);
265
+ for (const warning of result.warnings)
266
+ console.log(`Warning: ${warning}`);
267
+ return;
268
+ }
269
+ const result = (0, kernel_js_1.syncPersonal)();
270
+ if (args.includes("--json")) {
271
+ console.log(JSON.stringify(result, null, 2));
272
+ process.exit(result.ok ? 0 : 2);
273
+ }
274
+ if (!result.ok) {
275
+ console.error(`kage sync failed:\n${result.errors.map((error) => ` - ${error}`).join("\n")}`);
276
+ process.exit(2);
277
+ }
278
+ console.log(`kage sync: pushed ${result.pushed}, pulled ${result.pulled}, resolved ${result.resolved}`);
279
+ if (result.conflict_backups.length) {
280
+ console.log(`Conflict losers preserved:\n${result.conflict_backups.map((path) => ` - ${path}`).join("\n")}`);
281
+ }
282
+ return;
283
+ }
207
284
  await (0, kernel_js_1.ensureTreeSitterLanguages)();
208
285
  if (command === "index") {
209
286
  const result = (0, kernel_js_1.indexProject)(projectArg(args));
@@ -287,6 +364,7 @@ async function main() {
287
364
  console.log(`Initialized Kage memory for ${result.index.projectDir}`);
288
365
  console.log("\nCreated:");
289
366
  console.log(" .agent_memory/ memory packets + indexes (only directory Kage owns)");
367
+ console.log(` .gitattributes kage-packet merge driver for packet JSON${result.gitAttributes.changed ? "" : " (already current)"}`);
290
368
  if (result.policyInstalled) {
291
369
  console.log(" AGENTS.md, CLAUDE.md agent policy (requested via --with-policy)");
292
370
  console.log(" .claude/settings.json allowed kage tools (requested via --with-policy)");
@@ -302,6 +380,8 @@ async function main() {
302
380
  console.log("\nTip — version control:");
303
381
  console.log(" commit .agent_memory/packets/ (your team's reviewed memory)");
304
382
  console.log(" ignore .agent_memory/indexes/ .agent_memory/reports/ (regenerated)");
383
+ console.log("\nEnable the packet merge driver once per clone:");
384
+ console.log(` ${kernel_js_1.PACKET_MERGE_DRIVER_CONFIG}`);
305
385
  if (!result.policyInstalled) {
306
386
  console.log("\nNot written (opt-in): agent policy files. Add them with `kage policy --project .`");
307
387
  console.log("or rerun `kage init --with-policy` when you're ready to commit them.");
@@ -375,6 +455,8 @@ async function main() {
375
455
  console.log(" kage scan --project . 60-second Truth Report on this repo");
376
456
  console.log(" kage viewer --project . local dashboard (gains, packets, graph)");
377
457
  console.log("\nVersion control: commit .agent_memory/packets/, ignore .agent_memory/indexes/ and reports/.");
458
+ console.log("Enable the packet merge driver once per clone:");
459
+ console.log(` ${kernel_js_1.PACKET_MERGE_DRIVER_CONFIG}`);
378
460
  if (!init.validation.ok)
379
461
  process.exit(2);
380
462
  return;
@@ -408,7 +490,36 @@ async function main() {
408
490
  console.log(`Warnings:\n${result.validation.warnings.map((warning) => ` - ${warning}`).join("\n")}`);
409
491
  console.log("\nRecall smoke test:\n");
410
492
  console.log(result.sampleRecall);
411
- if (!result.validation.ok)
493
+ if (!result.validation.ok) {
494
+ console.log("\nSomething broken? kage repair --project .");
495
+ process.exit(2);
496
+ }
497
+ return;
498
+ }
499
+ if (command === "repair") {
500
+ const result = (0, kernel_js_1.repairProject)(projectArg(args));
501
+ if (args.includes("--json")) {
502
+ console.log(JSON.stringify(result, null, 2));
503
+ if (!result.ok || !result.validation.ok)
504
+ process.exit(2);
505
+ return;
506
+ }
507
+ console.log(`Kage repair — ${result.project_dir}\n`);
508
+ const areaLabel = { packets: "Memory", indexes: "Indexes", locks: "Locks", agents: "Agents" };
509
+ for (const action of result.actions) {
510
+ const mark = action.status === "fixed" ? "✓" : action.status === "failed" ? "✗" : "•";
511
+ console.log(` ${(areaLabel[action.area] ?? action.area).padEnd(12)}${mark} ${action.target} — ${action.detail}`);
512
+ }
513
+ console.log(`\n${result.fixed} fixed, ${result.skipped} already healthy, ${result.failed} failed`);
514
+ if (result.removed_packets.length) {
515
+ console.log(`\nRemoved ${result.removed_packets.length} unrecoverable packet(s) — backups kept in .agent_memory/backup/:`);
516
+ for (const removed of result.removed_packets)
517
+ console.log(` ${removed}`);
518
+ }
519
+ console.log(result.validation.ok ? "Validation: passed" : "Validation: still failing");
520
+ if (result.validation.errors.length)
521
+ console.log(`Errors:\n${result.validation.errors.map((error) => ` - ${error}`).join("\n")}`);
522
+ if (!result.ok || !result.validation.ok)
412
523
  process.exit(2);
413
524
  return;
414
525
  }
@@ -666,7 +777,7 @@ async function main() {
666
777
  return;
667
778
  }
668
779
  if (command === "refresh") {
669
- const result = (0, kernel_js_1.refreshProject)(projectArg(args), { full: args.includes("--full") });
780
+ const result = (0, kernel_js_1.refreshProject)(projectArg(args), { full: args.includes("--full"), force: args.includes("--force") });
670
781
  if (args.includes("--json")) {
671
782
  console.log(JSON.stringify(result, null, 2));
672
783
  if (!result.ok)
@@ -674,6 +785,8 @@ async function main() {
674
785
  return;
675
786
  }
676
787
  console.log(`Refreshed ${result.project_dir}`);
788
+ if (result.quiet_refresh)
789
+ console.log("Quiet refresh (non-default branch): packet metadata not rewritten on disk; use --force to persist.");
677
790
  console.log(`Packets indexed: ${result.index.packets}`);
678
791
  console.log(`Packet metadata updated: ${result.updated_packets}`);
679
792
  console.log(`Code graph: ${result.code_graph.files} files, ${result.code_graph.symbols} symbols, ${result.code_graph.imports} imports, ${result.code_graph.calls} calls`);
@@ -1155,6 +1268,23 @@ async function main() {
1155
1268
  `${window.caller_answers} caller ${plural(window.caller_answers, "answer", "answers")}`;
1156
1269
  console.log(windowLine("Today: ", summary.today));
1157
1270
  console.log(windowLine("All time:", summary.all_time));
1271
+ if (summary.all_time.replay_tokens > 0) {
1272
+ console.log(`Knowledge replay value: ~${(0, kernel_js_1.formatTokenCount)(week.replay_tokens)} tokens this week · ` +
1273
+ `~${(0, kernel_js_1.formatTokenCount)(summary.all_time.replay_tokens)} all time ` +
1274
+ `(discovery cost of served memories vs their compressed read cost)`);
1275
+ }
1276
+ return;
1277
+ }
1278
+ if (command === "file-context") {
1279
+ const path = takeArg(args, "--path");
1280
+ if (!path)
1281
+ usage();
1282
+ const result = (0, kernel_js_1.kageFileContext)(projectArg(args), path);
1283
+ if (args.includes("--json"))
1284
+ console.log(JSON.stringify(result, null, 2));
1285
+ else if (result.context_block)
1286
+ console.log(result.context_block);
1287
+ // No verified packets cite this file: print nothing so hooks can gate on empty output.
1158
1288
  return;
1159
1289
  }
1160
1290
  if (command === "memory-access") {
@@ -1502,6 +1632,21 @@ async function main() {
1502
1632
  console.log(`Warnings:\n${result.warnings.map((warning) => ` - ${warning}`).join("\n")}`);
1503
1633
  return;
1504
1634
  }
1635
+ if (command === "audit-claude-mem") {
1636
+ const projectDir = projectArg(args);
1637
+ const storePath = takeArg(args, "--store") ?? (0, kernel_js_1.defaultClaudeMemStorePath)();
1638
+ const result = (0, kernel_js_1.auditClaudeMemStore)(projectDir, { storePath });
1639
+ if (!result.ok) {
1640
+ console.error(result.error);
1641
+ process.exit(2);
1642
+ }
1643
+ if (args.includes("--json")) {
1644
+ console.log(JSON.stringify(result.report, null, 2));
1645
+ return;
1646
+ }
1647
+ console.log((0, kernel_js_1.renderClaudeMemAuditReceipt)(result.report));
1648
+ return;
1649
+ }
1505
1650
  if (command === "audit") {
1506
1651
  const result = (0, kernel_js_1.auditProject)(projectArg(args));
1507
1652
  if (args.includes("--json")) {
@@ -1699,7 +1844,8 @@ async function main() {
1699
1844
  const learning = takeArg(args, "--learning");
1700
1845
  if (!learning)
1701
1846
  usage();
1702
- const result = (0, kernel_js_1.learn)({
1847
+ const personal = args.includes("--personal");
1848
+ const result = (personal ? kernel_js_1.learnPersonal : kernel_js_1.learn)({
1703
1849
  projectDir: projectArg(args),
1704
1850
  learning,
1705
1851
  title: takeArg(args, "--title"),
@@ -1712,15 +1858,18 @@ async function main() {
1712
1858
  graphNodes: listArg(takeArg(args, "--graph-nodes")),
1713
1859
  allowMissingPaths: args.includes("--allow-missing-paths"),
1714
1860
  strictCitations: true,
1861
+ discoveryTokens: args.includes("--discovery-tokens") ? numberArg(args, "--discovery-tokens", 0) : undefined,
1715
1862
  });
1716
1863
  if (!result.ok) {
1717
1864
  console.error(`Learning capture blocked:\n${result.errors.map((error) => ` - ${error}`).join("\n")}`);
1718
1865
  process.exit(2);
1719
1866
  }
1720
- console.log(`Captured session learning: ${result.path}`);
1867
+ console.log(`Captured ${personal ? "personal" : "session"} learning: ${result.path}`);
1721
1868
  if (result.warnings?.length)
1722
1869
  console.log(`Warnings:\n${result.warnings.map((warning) => ` - ${warning}`).join("\n")}`);
1723
- console.log("Repo-local memory is written immediately. Promotion to org/global still requires explicit review.");
1870
+ console.log(personal
1871
+ ? "Personal memory is recalled with lower trust and never enters repo review flows. Sync it across machines with `kage sync`."
1872
+ : "Repo-local memory is written immediately. Promotion to org/global still requires explicit review.");
1724
1873
  return;
1725
1874
  }
1726
1875
  if (command === "propose") {
@@ -1899,9 +2048,18 @@ async function main() {
1899
2048
  const sessionId = takeArg(args, "--session");
1900
2049
  if (!sessionId)
1901
2050
  usage();
1902
- const result = (0, kernel_js_1.distillSession)(projectArg(args), sessionId);
2051
+ const project = projectArg(args);
2052
+ const auto = args.includes("--auto");
2053
+ const result = (0, kernel_js_1.distillSession)(project, sessionId, { auto });
1903
2054
  if (args.includes("--json"))
1904
2055
  console.log(JSON.stringify(result, null, 2));
2056
+ else if (auto) {
2057
+ // Auto mode is quiet: no output for empty or already-captured sessions; one line otherwise.
2058
+ const drafted = result.candidates.filter((candidate) => candidate.ok).length;
2059
+ if (!result.skipped_reason && drafted > 0) {
2060
+ console.log(`Auto-distilled ${drafted} pending draft${drafted === 1 ? "" : "s"} from session ${sessionId}. Review with: kage review --project ${project}`);
2061
+ }
2062
+ }
1905
2063
  else {
1906
2064
  console.log(`Distilled session: ${sessionId}`);
1907
2065
  console.log(`Observations: ${result.observations}`);
@@ -1909,10 +2067,21 @@ async function main() {
1909
2067
  if (result.errors.length)
1910
2068
  console.log(`Errors:\n${result.errors.map((error) => ` - ${error}`).join("\n")}`);
1911
2069
  }
1912
- if (!result.ok)
2070
+ if (!result.ok && !auto)
1913
2071
  process.exit(2);
1914
2072
  return;
1915
2073
  }
2074
+ if (command === "resume") {
2075
+ const result = (0, kernel_js_1.kageResume)(projectArg(args));
2076
+ if (args.includes("--json")) {
2077
+ console.log(JSON.stringify(result, null, 2));
2078
+ return;
2079
+ }
2080
+ // Prints nothing when there is no prior session data, so hooks can append output verbatim.
2081
+ if (result.has_content && result.context_block)
2082
+ console.log(result.context_block);
2083
+ return;
2084
+ }
1916
2085
  if (command === "feedback") {
1917
2086
  const id = takeArg(args, "--packet");
1918
2087
  const kind = takeArg(args, "--kind");
@@ -2004,7 +2173,10 @@ async function main() {
2004
2173
  }
2005
2174
  usage();
2006
2175
  }
2176
+ // Remediation-first failure: lead with the message, follow with exactly ONE
2177
+ // copy-pasteable next command. Exit code stays 1, same as before.
2007
2178
  main().catch((error) => {
2008
- console.error(error);
2179
+ console.error(error instanceof Error ? error.message : String(error));
2180
+ console.error(`\nTry:\n ${(0, kernel_js_1.remediationFor)(error)}`);
2009
2181
  process.exit(1);
2010
2182
  });
package/dist/daemon.js CHANGED
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.viewerStaticHeaders = viewerStaticHeaders;
4
4
  exports.viewerRedirectLocation = viewerRedirectLocation;
5
5
  exports.viewerReportPaths = viewerReportPaths;
6
+ exports.startLiveFeed = startLiveFeed;
6
7
  exports.viewerUrl = viewerUrl;
7
8
  exports.viewerBenchmarkReport = viewerBenchmarkReport;
8
9
  exports.daemonContextReport = daemonContextReport;
@@ -105,6 +106,152 @@ function viewerReportPaths(projectRoot) {
105
106
  value: (0, node_path_1.join)(reportsDir, "value.json"),
106
107
  };
107
108
  }
109
+ const LIVE_FEED_HEARTBEAT_MS = 25_000;
110
+ const LIVE_FEED_DEBOUNCE_MS = 100;
111
+ function readPacketTitle(filePath) {
112
+ try {
113
+ const parsed = JSON.parse((0, node_fs_1.readFileSync)(filePath, "utf8"));
114
+ return typeof parsed.title === "string" && parsed.title ? parsed.title : undefined;
115
+ }
116
+ catch {
117
+ return undefined;
118
+ }
119
+ }
120
+ // Streams memory/value activity to viewer clients over SSE (GET /kage/events).
121
+ // The engine already writes packets and the value ledger to .agent_memory/, so a
122
+ // filesystem watch is the lightest possible event source — no queue, no new deps.
123
+ function startLiveFeed(projectRoot, options = {}) {
124
+ const heartbeatMs = options.heartbeatMs ?? LIVE_FEED_HEARTBEAT_MS;
125
+ const debounceMs = options.debounceMs ?? LIVE_FEED_DEBOUNCE_MS;
126
+ const packetsDir = (0, node_path_1.join)(projectRoot, ".agent_memory", "packets");
127
+ const reportsDir = (0, node_path_1.join)(projectRoot, ".agent_memory", "reports");
128
+ const valuePath = (0, node_path_1.join)(reportsDir, "value.json");
129
+ const clients = new Set();
130
+ const watchers = [];
131
+ const timers = new Map();
132
+ const knownPackets = new Set();
133
+ try {
134
+ (0, node_fs_1.mkdirSync)(packetsDir, { recursive: true });
135
+ for (const name of (0, node_fs_1.readdirSync)(packetsDir))
136
+ knownPackets.add(name);
137
+ }
138
+ catch {
139
+ // packets dir unavailable: packet events simply won't fire
140
+ }
141
+ function readValueEvents() {
142
+ try {
143
+ const parsed = JSON.parse((0, node_fs_1.readFileSync)(valuePath, "utf8"));
144
+ return Array.isArray(parsed.events) ? parsed.events : [];
145
+ }
146
+ catch {
147
+ return [];
148
+ }
149
+ }
150
+ let seenValueEvents = readValueEvents().length;
151
+ function broadcast(event) {
152
+ const payload = `data: ${JSON.stringify(event)}\n\n`;
153
+ for (const res of clients)
154
+ res.write(payload);
155
+ }
156
+ function onPacketChange(name) {
157
+ const filePath = (0, node_path_1.join)(packetsDir, name);
158
+ if (!(0, node_fs_1.existsSync)(filePath)) {
159
+ knownPackets.delete(name);
160
+ return;
161
+ }
162
+ const isNew = !knownPackets.has(name);
163
+ knownPackets.add(name);
164
+ broadcast({
165
+ type: isNew ? "packet_written" : "packet_updated",
166
+ title: readPacketTitle(filePath) ?? name.replace(/\.json$/, ""),
167
+ path: (0, node_path_1.join)(".agent_memory", "packets", name),
168
+ ts: new Date().toISOString(),
169
+ });
170
+ }
171
+ function onValueChange() {
172
+ const events = readValueEvents();
173
+ if (events.length < seenValueEvents)
174
+ seenValueEvents = 0; // ledger trimmed or rewritten
175
+ for (const event of events.slice(seenValueEvents)) {
176
+ broadcast({
177
+ type: "value_event",
178
+ title: typeof event.packet_title === "string" ? event.packet_title : undefined,
179
+ path: (0, node_path_1.join)(".agent_memory", "reports", "value.json"),
180
+ event,
181
+ ts: typeof event.at === "string" ? event.at : new Date().toISOString(),
182
+ });
183
+ }
184
+ seenValueEvents = events.length;
185
+ }
186
+ // fs.watch fires bursts of duplicate events for one logical write; collapse
187
+ // them per file with a short debounce before reading and broadcasting.
188
+ function debounced(key, run) {
189
+ const existing = timers.get(key);
190
+ if (existing)
191
+ clearTimeout(existing);
192
+ timers.set(key, setTimeout(() => {
193
+ timers.delete(key);
194
+ try {
195
+ run();
196
+ }
197
+ catch {
198
+ // keep the feed alive even if a read races a write
199
+ }
200
+ }, debounceMs));
201
+ }
202
+ try {
203
+ watchers.push((0, node_fs_1.watch)(packetsDir, (_event, filename) => {
204
+ const name = String(filename ?? "");
205
+ if (!name.endsWith(".json"))
206
+ return;
207
+ debounced(`packet:${name}`, () => onPacketChange(name));
208
+ }));
209
+ }
210
+ catch {
211
+ // packets dir missing: no packet events
212
+ }
213
+ try {
214
+ (0, node_fs_1.mkdirSync)(reportsDir, { recursive: true });
215
+ watchers.push((0, node_fs_1.watch)(reportsDir, (_event, filename) => {
216
+ if (String(filename ?? "") !== "value.json")
217
+ return;
218
+ debounced("value", onValueChange);
219
+ }));
220
+ }
221
+ catch {
222
+ // reports dir missing: no value events
223
+ }
224
+ const heartbeat = setInterval(() => {
225
+ for (const res of clients)
226
+ res.write(`: heartbeat ${Date.now()}\n\n`);
227
+ }, heartbeatMs);
228
+ heartbeat.unref();
229
+ function handleRequest(req, res) {
230
+ res.writeHead(200, {
231
+ "content-type": "text/event-stream; charset=utf-8",
232
+ "cache-control": "no-cache, no-transform",
233
+ connection: "keep-alive",
234
+ "x-accel-buffering": "no",
235
+ });
236
+ res.write(": connected\n\n");
237
+ clients.add(res);
238
+ req.on("close", () => {
239
+ clients.delete(res);
240
+ });
241
+ }
242
+ function close() {
243
+ clearInterval(heartbeat);
244
+ for (const timer of timers.values())
245
+ clearTimeout(timer);
246
+ timers.clear();
247
+ for (const watcher of watchers)
248
+ watcher.close();
249
+ for (const res of clients)
250
+ res.end();
251
+ clients.clear();
252
+ }
253
+ return { handleRequest, broadcast, clientCount: () => clients.size, close };
254
+ }
108
255
  function viewerUrl(host, port, projectRoot) {
109
256
  const query = Object.entries(viewerReportPaths(projectRoot))
110
257
  .map(([name, path]) => `${name}=${encodeURIComponent(path)}`)
@@ -633,8 +780,13 @@ async function startViewer(projectDir, options = {}) {
633
780
  // non-fatal: viewer will show 404 for reports if generation fails
634
781
  }
635
782
  const url = viewerUrl(host, port, projectRoot);
783
+ const liveFeed = startLiveFeed(projectRoot);
636
784
  const server = (0, node_http_1.createServer)((req, res) => {
637
785
  const requestUrl = new URL(req.url ?? "/", `http://${host}:${port}`);
786
+ if (req.method === "GET" && requestUrl.pathname === "/kage/events") {
787
+ liveFeed.handleRequest(req, res);
788
+ return;
789
+ }
638
790
  let filePath = null;
639
791
  const redirectLocation = viewerRedirectLocation(requestUrl.pathname, requestUrl.search, new URL(url).search);
640
792
  if (redirectLocation) {
@@ -673,6 +825,7 @@ async function startViewer(projectDir, options = {}) {
673
825
  await new Promise((resolveListen) => server.listen(port, host, resolveListen));
674
826
  console.log(`Kage viewer listening on ${url}`);
675
827
  process.on("SIGTERM", () => {
828
+ liveFeed.close();
676
829
  server.close(() => process.exit(0));
677
830
  });
678
831
  return { ok: true, project_dir: projectRoot, host, port, url };
package/dist/index.js CHANGED
@@ -75,6 +75,15 @@ function riskContextBlock(result) {
75
75
  return `\n## Risk Signals\n${lines.join("\n")}`;
76
76
  }
77
77
  const server = new index_js_1.Server({ name: "kage-graph", version: "2.0.0" }, { capabilities: { tools: {} } });
78
+ // Workflow pseudo-tool: the description itself is the documentation, so agents
79
+ // absorb the loop just by listing tools. The handler returns the same text.
80
+ const KAGE_WORKFLOW_TEXT = "Kage memory workflow (this tool performs no action; it returns this loop). " +
81
+ "1) Start every task with kage_context (project_dir + the task as query): it validates memory, recalls relevant packets, and queries the code and knowledge graphs in one call. " +
82
+ "2) Do the work, preferring repo memory over public context. " +
83
+ "3) Capture reusable learnings with kage_learn — bug causes and verified fixes, conventions, decisions, gotchas, run/test/build commands. Wrap anything that must never leave the repo in <private>...</private> tags; private spans are stripped before sharing. " +
84
+ "4) After meaningful file changes, call kage_refresh so indexes, graphs, and stale-memory checks stay current. " +
85
+ "5) Before finishing a branch, call kage_pr_summarize then kage_pr_check. " +
86
+ "Recall receipts show estimated tokens saved versus rediscovery; report memory quality with kage_feedback (helpful/wrong/stale).";
78
87
  function listTools() {
79
88
  return [
80
89
  {
@@ -457,15 +466,25 @@ function listTools() {
457
466
  },
458
467
  {
459
468
  name: "kage_refresh",
460
- description: "Rebuild repo indexes, code graph, memory graph, metrics, and stale-memory metadata. Agents should run this after meaningful file/content changes before PR checks; push-only or same-tree commits do not need another refresh.",
469
+ description: "Rebuild repo indexes, code graph, memory graph, metrics, and stale-memory metadata. Agents should run this after meaningful file/content changes before PR checks; push-only or same-tree commits do not need another refresh. On non-default git branches metadata-only packet rewrites are skipped (quiet refresh) to avoid merge conflicts; pass force to persist them anyway.",
461
470
  inputSchema: {
462
471
  type: "object",
463
472
  properties: {
464
473
  project_dir: { type: "string" },
474
+ force: { type: "boolean", description: "Persist packet metadata rewrites even on a non-default branch" },
465
475
  },
466
476
  required: ["project_dir"],
467
477
  },
468
478
  },
479
+ {
480
+ name: "kage_workflow",
481
+ description: KAGE_WORKFLOW_TEXT,
482
+ inputSchema: {
483
+ type: "object",
484
+ properties: {},
485
+ required: [],
486
+ },
487
+ },
469
488
  {
470
489
  name: "kage_pr_summarize",
471
490
  description: "Create a PR/branch memory summary from local git diff metadata and write repo-local change memory. Use when a branch is ready to hand off.",
@@ -685,6 +704,7 @@ function listTools() {
685
704
  stack: { type: "array", items: { type: "string" } },
686
705
  graph_nodes: { type: "array", items: { type: "string" } },
687
706
  allow_missing_paths: { type: "boolean" },
707
+ discovery_tokens: { type: "number", description: "Approximate token cost of producing this knowledge (exploration + reasoning). Stored on the packet so recall receipts can report replay value; a conservative per-type default is estimated when omitted." },
688
708
  },
689
709
  required: ["project_dir", "learning"],
690
710
  },
@@ -1243,12 +1263,17 @@ async function callTool(name, args) {
1243
1263
  };
1244
1264
  }
1245
1265
  if (name === "kage_refresh") {
1246
- const result = (0, kernel_js_1.refreshProject)(String(args?.project_dir ?? ""), { full: Boolean(args?.full) });
1266
+ const result = (0, kernel_js_1.refreshProject)(String(args?.project_dir ?? ""), { full: Boolean(args?.full), force: Boolean(args?.force) });
1247
1267
  return {
1248
1268
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
1249
1269
  isError: !result.ok,
1250
1270
  };
1251
1271
  }
1272
+ if (name === "kage_workflow") {
1273
+ return {
1274
+ content: [{ type: "text", text: KAGE_WORKFLOW_TEXT }],
1275
+ };
1276
+ }
1252
1277
  if (name === "kage_pr_summarize") {
1253
1278
  const result = (0, kernel_js_1.prSummarize)(String(args?.project_dir ?? ""));
1254
1279
  return {
@@ -1349,6 +1374,7 @@ async function callTool(name, args) {
1349
1374
  graphNodes: arrayArg(args?.graph_nodes),
1350
1375
  allowMissingPaths: Boolean(args?.allow_missing_paths),
1351
1376
  strictCitations: true,
1377
+ discoveryTokens: args?.discovery_tokens === undefined ? undefined : Number(args.discovery_tokens),
1352
1378
  });
1353
1379
  const learnWarnings = result.warnings?.length ? `\nWarnings:\n${result.warnings.map((warning) => `- ${warning}`).join("\n")}` : "";
1354
1380
  return {