@kage-core/kage-graph-mcp 2.5.7 → 3.0.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
@@ -10,6 +10,7 @@ const node_process_1 = require("node:process");
10
10
  const daemon_js_1 = require("./daemon.js");
11
11
  const kernel_js_1 = require("./kernel.js");
12
12
  const graph_registry_js_1 = require("./graph-registry.js");
13
+ const okf_js_1 = require("./okf.js");
13
14
  const CORE_USAGE = `Kage — code-grounded memory for coding agents
14
15
 
15
16
  Core commands:
@@ -51,7 +52,7 @@ Usage:
51
52
  kage hook status --project <dir> [--json]
52
53
  kage hook uninstall --project <dir> [--json]
53
54
  kage refresh --project <dir> [--full] [--force] [--json]
54
- kage merge-packet <ours> <base> <theirs> git merge driver for .agent_memory/packets/*.json
55
+ kage merge-packet <ours> <base> <theirs> git merge driver for .agent_memory/packets/*.md
55
56
  kage gc --project <dir> [--dry-run] [--force] [--json]
56
57
  kage compact --project <dir> [--dry-run] [--json]
57
58
  kage verify --project <dir> [--id <packet-id>] [--json]
@@ -448,6 +449,52 @@ async function main() {
448
449
  console.log(" npx -y @kage-core/kage-graph-mcp install");
449
450
  return;
450
451
  }
452
+ if (command === "okf") {
453
+ // OKF (Open Knowledge Format) is Kage's standard on-disk memory format.
454
+ const sub = args[1] && !args[1].startsWith("--") ? args[1] : "";
455
+ const project = (0, node_path_1.resolve)(projectArg(args));
456
+ if (sub === "migrate" || sub === "export") {
457
+ const result = (0, okf_js_1.migratePacketsToOkf)(project, { includePending: args.includes("--pending") });
458
+ console.log(`Kage memory → OKF: wrote ${result.written} concept(s) to ${result.root}`);
459
+ for (const [type, count] of Object.entries(result.byType).sort((a, b) => b[1] - a[1])) {
460
+ console.log(` ${type}: ${count}`);
461
+ }
462
+ console.log("\nConformant OKF bundle: index.md (progressive disclosure), log.md (history), concept docs.");
463
+ console.log("Trust metadata rides in x-kage-* frontmatter, so any OKF consumer (incl. Google's visualizer) reads it unchanged.");
464
+ return;
465
+ }
466
+ if (sub === "lint") {
467
+ const target = args[2] && !args[2].startsWith("--") ? (0, node_path_1.resolve)(args[2]) : (0, okf_js_1.okfBundleDir)(project);
468
+ const { files, failures } = (0, okf_js_1.lintOkfBundle)(target);
469
+ if (failures.length === 0) {
470
+ console.log(`OKF lint: ${files} concept(s) in ${target} — all conformant.`);
471
+ return;
472
+ }
473
+ console.log(`OKF lint: ${failures.length}/${files} concept(s) non-conformant in ${target}:`);
474
+ for (const failure of failures)
475
+ console.log(` ${failure.path}: ${failure.errors.join("; ")}`);
476
+ process.exit(1);
477
+ }
478
+ if (sub === "import") {
479
+ const dir = args[2] && !args[2].startsWith("--") ? (0, node_path_1.resolve)(args[2]) : (0, okf_js_1.okfBundleDir)(project);
480
+ const packets = (0, okf_js_1.loadOkfConcepts)(dir, { projectDir: project });
481
+ if (args.includes("--json")) {
482
+ console.log(JSON.stringify(packets, null, 2));
483
+ return;
484
+ }
485
+ console.log(`Read ${packets.length} concept(s) from OKF bundle ${dir}`);
486
+ for (const packet of packets.slice(0, 20))
487
+ console.log(` [${packet.type}] ${packet.title}`);
488
+ if (packets.length > 20)
489
+ console.log(` … and ${packets.length - 20} more`);
490
+ return;
491
+ }
492
+ console.log("kage okf — Open Knowledge Format is Kage's standard memory format.");
493
+ console.log(" kage okf migrate [--project <dir>] [--pending] packets → OKF bundle (.agent_memory/okf)");
494
+ console.log(" kage okf lint [<dir|file>] [--project <dir>] check OKF conformance");
495
+ console.log(" kage okf import [<dir>] [--project <dir>] [--json] read an OKF bundle back into packets");
496
+ return;
497
+ }
451
498
  if (command === "init") {
452
499
  const withPolicy = args.includes("--with-policy");
453
500
  const result = (0, kernel_js_1.initProject)(projectArg(args), { policy: withPolicy });
package/dist/daemon.js CHANGED
@@ -202,7 +202,7 @@ function startLiveFeed(projectRoot, options = {}) {
202
202
  try {
203
203
  watchers.push((0, node_fs_1.watch)(packetsDir, (_event, filename) => {
204
204
  const name = String(filename ?? "");
205
- if (!name.endsWith(".json"))
205
+ if (!name.endsWith(".json") && !name.endsWith(".md"))
206
206
  return;
207
207
  debounced(`packet:${name}`, () => onPacketChange(name));
208
208
  }));
package/dist/kernel.js CHANGED
@@ -208,6 +208,7 @@ const node_path_1 = require("node:path");
208
208
  const node_worker_threads_1 = require("node:worker_threads");
209
209
  const ts = __importStar(require("typescript"));
210
210
  const index_js_1 = require("./registry/index.js");
211
+ const okf_js_1 = require("./okf.js");
211
212
  exports.PACKET_SCHEMA_VERSION = 2;
212
213
  exports.MEMORY_TYPES = [
213
214
  "repo_map",
@@ -271,7 +272,11 @@ const AGENTS_POLICY_END = "<!-- END_KAGE_MEMORY_POLICY_V1 -->";
271
272
  const AGENTS_POLICY = `${AGENTS_POLICY_MARKER}
272
273
  # Kage Memory Harness
273
274
 
274
- This repo uses Kage as an automatic memory harness for coding agents.
275
+ This repo uses Kage as an automatic memory harness for coding agents. Memory is
276
+ stored and exchanged in Open Knowledge Format (OKF) — markdown concept files under
277
+ \`.agent_memory/packets/\`, with Kage's verification metadata in OKF-legal \`x-kage-*\`
278
+ frontmatter, readable by any OKF consumer. Use \`kage okf migrate|lint|import\` to
279
+ work with OKF bundles.
275
280
 
276
281
  ## Automatic Recall
277
282
 
@@ -466,10 +471,23 @@ function ensureDir(path) {
466
471
  (0, node_fs_1.mkdirSync)(path, { recursive: true });
467
472
  }
468
473
  function readJson(path) {
474
+ // Packet files are OKF concept docs (.md); every other store artifact is JSON.
475
+ // Routing the dispatch here makes all packet readers (supersede, stale, repair,
476
+ // sync, …) format-aware without touching each call site.
477
+ if (path.endsWith(".md")) {
478
+ const packet = (0, okf_js_1.okfConceptToPacket)((0, node_fs_1.readFileSync)(path, "utf8"));
479
+ if (!packet)
480
+ throw new Error(`not a parseable OKF concept: ${path}`);
481
+ return packet;
482
+ }
469
483
  return JSON.parse((0, node_fs_1.readFileSync)(path, "utf8"));
470
484
  }
471
485
  function writeJson(path, value) {
472
486
  ensureDir((0, node_path_1.dirname)(path));
487
+ if (path.endsWith(".md")) {
488
+ (0, node_fs_1.writeFileSync)(path, (0, okf_js_1.packetToOkfConcept)(value), "utf8");
489
+ return;
490
+ }
473
491
  (0, node_fs_1.writeFileSync)(path, `${JSON.stringify(value, null, 2)}\n`, "utf8");
474
492
  }
475
493
  function slugify(input) {
@@ -483,7 +501,7 @@ function slugify(input) {
483
501
  }
484
502
  function packetFileName(packet) {
485
503
  const idHash = (0, node_crypto_1.createHash)("sha256").update(packet.id).digest("hex").slice(0, 8);
486
- return `${packet.type}-${slugify(packet.title)}-${idHash}.json`;
504
+ return `${packet.type}-${slugify(packet.title)}-${idHash}.md`;
487
505
  }
488
506
  function repoKey(projectDir) {
489
507
  const configPath = (0, node_path_1.join)(projectDir, ".git", "config");
@@ -2375,9 +2393,33 @@ function walkFiles(root, predicate) {
2375
2393
  // Tolerant packet read: a single corrupt or merge-conflicted packet (e.g. an
2376
2394
  // unresolved `<<<<<<<` from a teammate's git merge) must not take down all of
2377
2395
  // recall/verify/compact. Skip the bad file with a warning and keep going.
2396
+ // Packets are stored as OKF concept documents (.md). Legacy .json packets stay
2397
+ // readable so existing stores and test fixtures keep loading during/after the swap.
2398
+ function isPacketFile(name) {
2399
+ return name.endsWith(".md") || name.endsWith(".json");
2400
+ }
2401
+ // Read a packet file from disk, dispatching on format. Throws on an unparseable
2402
+ // file (same contract as the JSON reader it replaces); callers that tolerate bad
2403
+ // files go through tryReadPacket.
2404
+ function readPacketFromDisk(path) {
2405
+ if (path.endsWith(".md")) {
2406
+ const packet = (0, okf_js_1.okfConceptToPacket)((0, node_fs_1.readFileSync)(path, "utf8"));
2407
+ if (!packet)
2408
+ throw new Error(`not a parseable OKF concept: ${path}`);
2409
+ return packet;
2410
+ }
2411
+ return readJson(path);
2412
+ }
2413
+ // Write a packet file, preserving its on-disk format (.md = OKF, legacy .json).
2414
+ function writePacketToDisk(path, packet) {
2415
+ if (path.endsWith(".md"))
2416
+ (0, node_fs_1.writeFileSync)(path, (0, okf_js_1.packetToOkfConcept)(packet), "utf8");
2417
+ else
2418
+ writeJson(path, packet);
2419
+ }
2378
2420
  function tryReadPacket(path) {
2379
2421
  try {
2380
- return readJson(path);
2422
+ return readPacketFromDisk(path);
2381
2423
  }
2382
2424
  catch (error) {
2383
2425
  process.stderr.write(`kage: skipping unreadable memory packet ${path}: ${error.message}\n`);
@@ -2388,7 +2430,7 @@ function loadPacketsFromDir(dir) {
2388
2430
  if (!(0, node_fs_1.existsSync)(dir))
2389
2431
  return [];
2390
2432
  return (0, node_fs_1.readdirSync)(dir)
2391
- .filter((name) => name.endsWith(".json"))
2433
+ .filter(isPacketFile)
2392
2434
  .sort()
2393
2435
  .flatMap((name) => {
2394
2436
  const packet = tryReadPacket((0, node_path_1.join)(dir, name));
@@ -2399,7 +2441,7 @@ function loadPacketEntriesFromDir(dir) {
2399
2441
  if (!(0, node_fs_1.existsSync)(dir))
2400
2442
  return [];
2401
2443
  return (0, node_fs_1.readdirSync)(dir)
2402
- .filter((name) => name.endsWith(".json"))
2444
+ .filter(isPacketFile)
2403
2445
  .sort()
2404
2446
  .flatMap((name) => {
2405
2447
  const path = (0, node_path_1.join)(dir, name);
@@ -2422,7 +2464,7 @@ function recallablePendingPackets(projectDir) {
2422
2464
  function writePacket(projectDir, packet, statusDir) {
2423
2465
  const dir = statusDir === "packets" ? packetsDir(projectDir) : pendingDir(projectDir);
2424
2466
  const path = (0, node_path_1.join)(dir, packetFileName(packet));
2425
- writeJson(path, packet);
2467
+ writePacketToDisk(path, packet);
2426
2468
  return path;
2427
2469
  }
2428
2470
  function memoryAuditPath(projectDir) {
@@ -2821,7 +2863,7 @@ const NOISE_PATH_PREFIXES = [
2821
2863
  "elm-stuff/",
2822
2864
  ];
2823
2865
  function isReviewableMemoryPath(filePath) {
2824
- return /^\.agent_memory\/(?:packets|pending)\/[^/]+\.json$/.test(filePath);
2866
+ return /^\.agent_memory\/(?:packets|pending)\/[^/]+\.(?:json|md)$/.test(filePath);
2825
2867
  }
2826
2868
  function isNoisePath(filePath) {
2827
2869
  if (isReviewableMemoryPath(filePath))
@@ -6737,7 +6779,7 @@ function readCurrentKnowledgeGraph(projectDir, codeGraph, expectedInputHash) {
6737
6779
  function graphFastFingerprint(projectDir) {
6738
6780
  const packetPaths = (0, node_fs_1.existsSync)(packetsDir(projectDir))
6739
6781
  ? (0, node_fs_1.readdirSync)(packetsDir(projectDir))
6740
- .filter((name) => name.endsWith(".json"))
6782
+ .filter(isPacketFile)
6741
6783
  .map((name) => (0, node_path_1.join)(packetsDir(projectDir), name))
6742
6784
  : [];
6743
6785
  const paths = [
@@ -7153,7 +7195,7 @@ function compactProject(projectDir, options = {}) {
7153
7195
  if (hardReason) {
7154
7196
  deprecated.push({ id: packet.id, title: packet.title, reason: hardReason });
7155
7197
  if (!dryRun)
7156
- writeJson(path, { ...packet, status: "deprecated", updated_at: nowIso() });
7198
+ writePacketToDisk(path, { ...packet, status: "deprecated", updated_at: nowIso() });
7157
7199
  continue;
7158
7200
  }
7159
7201
  const meaningful = packet.paths.filter((p) => meaningfulMemoryPath(p) && !shouldSkipRepoMemoryPath(p));
@@ -13945,7 +13987,7 @@ function createPublicCandidate(projectDir, id) {
13945
13987
  if (!validation.ok)
13946
13988
  return { ok: false, errors: validation.errors };
13947
13989
  const path = (0, node_path_1.join)(publicCandidatesDir(projectDir), packetFileName(packet));
13948
- writeJson(path, packet);
13990
+ writePacketToDisk(path, packet);
13949
13991
  return { ok: true, packet, path, errors: [] };
13950
13992
  }
13951
13993
  function registryRecommendations(projectDir) {
@@ -15480,7 +15522,7 @@ function distillSession(projectDir, sessionId, options = {}) {
15480
15522
  result.path = promotedPath;
15481
15523
  }
15482
15524
  else {
15483
- writeJson(result.path, result.packet);
15525
+ writePacketToDisk(result.path, result.packet);
15484
15526
  }
15485
15527
  return result;
15486
15528
  };
@@ -15692,7 +15734,7 @@ function createDiffChangeMemory(projectDir, summary) {
15692
15734
  const existing = (0, node_fs_1.readdirSync)(packetsDir(projectDir)).filter((name) => name.startsWith(stalePrefix) && name !== stableFileName);
15693
15735
  for (const name of existing) {
15694
15736
  const stale = (0, node_path_1.join)(packetsDir(projectDir), name);
15695
- const stalePacket = readJson(stale);
15737
+ const stalePacket = readPacketFromDisk(stale);
15696
15738
  if (stalePacket?.type === "workflow" && stalePacket?.title === title) {
15697
15739
  (0, node_fs_1.unlinkSync)(stale);
15698
15740
  }
@@ -16050,7 +16092,7 @@ function prCheck(projectDir) {
16050
16092
  .split(/\r?\n/)
16051
16093
  .map(parsePorcelainPath)
16052
16094
  .map((path) => path.replace(/^.* -> /, ""))
16053
- .filter((path) => path.startsWith(".agent_memory/packets/") && path.endsWith(".json"))).sort();
16095
+ .filter((path) => path.startsWith(".agent_memory/packets/") && isPacketFile(path))).sort();
16054
16096
  const codeGraphCurrent = graphIsCurrent(projectDir, ".agent_memory/code_graph/graph.json", { head: overlay.head, tree, inputHash: codeInputHash });
16055
16097
  const memoryGraphCurrent = graphIsCurrent(projectDir, ".agent_memory/graph/graph.json", { head: overlay.head, tree, inputHash: memoryInputHash });
16056
16098
  const sessions = kageSessionCaptureReport(projectDir);
@@ -16418,8 +16460,8 @@ function recordFeedback(projectDir, id, feedback) {
16418
16460
  if (!["helpful", "wrong", "stale"].includes(feedback)) {
16419
16461
  return { ok: false, errors: [`Invalid feedback: ${feedback}`] };
16420
16462
  }
16421
- for (const path of walkFiles(packetsDir(projectDir), (candidate) => candidate.endsWith(".json"))) {
16422
- const packet = readJson(path);
16463
+ for (const path of walkFiles(packetsDir(projectDir), isPacketFile)) {
16464
+ const packet = readPacketFromDisk(path);
16423
16465
  if (packet.id !== id)
16424
16466
  continue;
16425
16467
  const quality = (packet.quality ?? {});
@@ -16440,7 +16482,7 @@ function recordFeedback(projectDir, id, feedback) {
16440
16482
  stale_reported_at: packet.updated_at,
16441
16483
  };
16442
16484
  }
16443
- writeJson(path, packet);
16485
+ writePacketToDisk(path, packet);
16444
16486
  recordMemoryAudit(projectDir, "feedback", [packet], {
16445
16487
  feedback,
16446
16488
  path: (0, node_path_1.relative)(projectDir, path),
@@ -16460,9 +16502,9 @@ function validateProject(projectDir) {
16460
16502
  [pendingDir(projectDir), "pending"],
16461
16503
  [publicCandidatesDir(projectDir), "public candidate"],
16462
16504
  ]) {
16463
- for (const packetPath of walkFiles(dir, (path) => path.endsWith(".json"))) {
16505
+ for (const packetPath of walkFiles(dir, isPacketFile)) {
16464
16506
  try {
16465
- const packet = readJson(packetPath);
16507
+ const packet = readPacketFromDisk(packetPath);
16466
16508
  const validation = validatePacket(packet, (0, node_path_1.relative)(projectDir, packetPath));
16467
16509
  errors.push(...validation.errors);
16468
16510
  warnings.push(...validation.warnings);
@@ -16660,12 +16702,17 @@ function resolveConflictedPacket(content) {
16660
16702
  for (const side of [sides.ours, sides.theirs]) {
16661
16703
  try {
16662
16704
  const parsed = JSON.parse(side);
16663
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
16705
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
16664
16706
  candidates.push(parsed);
16707
+ continue;
16708
+ }
16665
16709
  }
16666
16710
  catch {
16667
- // This side does not parse; the other side may still win.
16711
+ // Not JSON fall through and try the OKF concept format below.
16668
16712
  }
16713
+ const okf = (0, okf_js_1.okfConceptToPacket)(side);
16714
+ if (okf)
16715
+ candidates.push(okf);
16669
16716
  }
16670
16717
  if (!candidates.length)
16671
16718
  return null;
@@ -16678,7 +16725,7 @@ function resolveConflictedPacket(content) {
16678
16725
  // exit 0 on success, non-zero to leave the conflict). v1 policy is whole-file
16679
16726
  // newest-wins by updated_at — packets are single facts, so field-level merges
16680
16727
  // buy little over taking the most recently verified side.
16681
- exports.PACKET_MERGE_ATTRIBUTE_LINE = ".agent_memory/packets/*.json merge=kage-packet";
16728
+ exports.PACKET_MERGE_ATTRIBUTE_LINE = ".agent_memory/packets/*.md merge=kage-packet";
16682
16729
  exports.PACKET_MERGE_DRIVER_CONFIG = 'git config merge.kage-packet.driver "npx -y @kage-core/kage-graph-mcp merge-packet %A %O %B"';
16683
16730
  function mergePacketFiles(oursPath, basePath, theirsPath) {
16684
16731
  void basePath; // Reserved for a future field-level three-way merge.
@@ -16686,6 +16733,10 @@ function mergePacketFiles(oursPath, basePath, theirsPath) {
16686
16733
  const raw = safeReadText(path);
16687
16734
  if (raw === null)
16688
16735
  return null;
16736
+ if (path.endsWith(".md")) {
16737
+ const packet = (0, okf_js_1.okfConceptToPacket)(raw);
16738
+ return packet ? { raw, packet } : null;
16739
+ }
16689
16740
  try {
16690
16741
  const parsed = JSON.parse(raw);
16691
16742
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
@@ -16735,7 +16786,7 @@ function ensurePacketMergeAttributes(projectDir) {
16735
16786
  const path = (0, node_path_1.join)(projectDir, ".gitattributes");
16736
16787
  const existing = safeReadText(path) ?? "";
16737
16788
  const lines = existing.split(/\r?\n/);
16738
- const pattern = /^\.agent_memory\/packets\/\*\.json\s+merge=/;
16789
+ const pattern = /^\.agent_memory\/packets\/\*\.(?:json|md)\s+merge=/;
16739
16790
  const index = lines.findIndex((line) => pattern.test(line.trim()));
16740
16791
  if (index !== -1) {
16741
16792
  if (lines[index].trim() === exports.PACKET_MERGE_ATTRIBUTE_LINE)
@@ -16762,7 +16813,7 @@ function repairProject(projectDir, options = {}) {
16762
16813
  [publicCandidatesDir(projectDir), "public-candidates"],
16763
16814
  ];
16764
16815
  for (const [dir, label] of packetDirs) {
16765
- for (const path of walkFiles(dir, (candidate) => candidate.endsWith(".json"))) {
16816
+ for (const path of walkFiles(dir, isPacketFile)) {
16766
16817
  const target = `${label}/${(0, node_path_1.basename)(path)}`;
16767
16818
  let raw;
16768
16819
  try {
@@ -16959,14 +17010,14 @@ function remediationFor(error) {
16959
17010
  return "npx -y kage-graph-mcp doctor --project .";
16960
17011
  }
16961
17012
  function approvePending(projectDir, id) {
16962
- const pendingFiles = walkFiles(pendingDir(projectDir), (path) => path.endsWith(".json"));
17013
+ const pendingFiles = walkFiles(pendingDir(projectDir), isPacketFile);
16963
17014
  for (const path of pendingFiles) {
16964
- const packet = readJson(path);
17015
+ const packet = readPacketFromDisk(path);
16965
17016
  if (packet.id === id) {
16966
17017
  packet.status = "approved";
16967
17018
  packet.updated_at = nowIso();
16968
17019
  const target = (0, node_path_1.join)(packetsDir(projectDir), packetFileName(packet));
16969
- writeJson(target, packet);
17020
+ writePacketToDisk(target, packet);
16970
17021
  (0, node_fs_1.renameSync)(path, `${path}.approved`);
16971
17022
  recordMemoryAudit(projectDir, "approve", [packet], {
16972
17023
  from: (0, node_path_1.relative)(projectDir, path),
@@ -16979,9 +17030,9 @@ function approvePending(projectDir, id) {
16979
17030
  throw new Error(`Pending packet not found: ${id}`);
16980
17031
  }
16981
17032
  function rejectPending(projectDir, id) {
16982
- const pendingFiles = walkFiles(pendingDir(projectDir), (path) => path.endsWith(".json"));
17033
+ const pendingFiles = walkFiles(pendingDir(projectDir), isPacketFile);
16983
17034
  for (const path of pendingFiles) {
16984
- const packet = readJson(path);
17035
+ const packet = readPacketFromDisk(path);
16985
17036
  if (packet.id === id) {
16986
17037
  const target = `${path}.rejected`;
16987
17038
  (0, node_fs_1.renameSync)(path, target);
@@ -17739,7 +17790,7 @@ function syncIdentityArgs(memoryDir) {
17739
17790
  }
17740
17791
  const SYNC_SETUP_HINT = "Run: kage sync setup --remote <git-url>";
17741
17792
  function syncPacketFile(file) {
17742
- return file.startsWith("packets/") && file.endsWith(".json");
17793
+ return file.startsWith("packets/") && isPacketFile(file);
17743
17794
  }
17744
17795
  function countSyncPacketFiles(namesOutput) {
17745
17796
  return namesOutput
@@ -17822,7 +17873,7 @@ function rebaseOntoUpstream(memoryDir, upstream) {
17822
17873
  for (const file of conflicted) {
17823
17874
  if (!syncPacketFile(file)) {
17824
17875
  runSyncGit(memoryDir, ["rebase", "--abort"]);
17825
- return { ok: false, resolved, conflictBackups, error: `kage sync only auto-resolves packets/*.json conflicts; ${file} needs manual resolution in ${memoryDir}.` };
17876
+ return { ok: false, resolved, conflictBackups, error: `kage sync only auto-resolves packets/*.md conflicts; ${file} needs manual resolution in ${memoryDir}.` };
17826
17877
  }
17827
17878
  const resolution = resolvePacketSyncConflict(memoryDir, file);
17828
17879
  if (!resolution.ok) {
package/dist/okf.js ADDED
@@ -0,0 +1,384 @@
1
+ "use strict";
2
+ // Kage <-> Google Open Knowledge Format (OKF) adapter.
3
+ //
4
+ // OKF (github.com/GoogleCloudPlatform/knowledge-catalog/okf) is Kage's standard
5
+ // on-disk interchange format for memory. A Kage memory packet is rendered as an
6
+ // OKF concept document: a Markdown file with YAML frontmatter whose only required
7
+ // field is `type`, plus the recommended `title` / `description` / `resource` /
8
+ // `tags` / `timestamp`. Kage's trust metadata (status, scope, confidence, the
9
+ // content-hash anchors, the lineage) rides along as OKF-legal custom `x-kage-*`
10
+ // frontmatter keys — OKF mandates that consumers preserve unknown producer fields,
11
+ // so a Kage bundle stays fully OKF-conformant and renders in any OKF consumer.
12
+ //
13
+ // Round-trip is lossless: every Kage-authored concept embeds the exact packet as a
14
+ // fenced ```json kage-state block in the body, so packet -> concept -> packet is an
15
+ // identity. Concepts WITHOUT that block (hand-authored or produced by another tool)
16
+ // are imported best-effort from their frontmatter + body, which is what lets Kage
17
+ // consume any third-party OKF bundle.
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.okfType = okfType;
20
+ exports.kageType = kageType;
21
+ exports.packetToOkfConcept = packetToOkfConcept;
22
+ exports.okfConceptToPacket = okfConceptToPacket;
23
+ exports.lintOkfConcept = lintOkfConcept;
24
+ exports.okfConceptFileName = okfConceptFileName;
25
+ exports.okfBundleDir = okfBundleDir;
26
+ exports.migratePacketsToOkf = migratePacketsToOkf;
27
+ exports.loadOkfConcepts = loadOkfConcepts;
28
+ exports.lintOkfBundle = lintOkfBundle;
29
+ const node_crypto_1 = require("node:crypto");
30
+ const node_fs_1 = require("node:fs");
31
+ const node_path_1 = require("node:path");
32
+ const kernel_js_1 = require("./kernel.js");
33
+ // ---- type vocabulary mapping (Kage snake_case <-> OKF display string) ----
34
+ const TYPE_DISPLAY = {
35
+ repo_map: "Repo Map",
36
+ runbook: "Runbook",
37
+ bug_fix: "Bug Fix",
38
+ decision: "Decision",
39
+ rationale: "Rationale",
40
+ convention: "Convention",
41
+ workflow: "Workflow",
42
+ gotcha: "Gotcha",
43
+ reference: "Reference",
44
+ policy: "Policy",
45
+ issue_context: "Issue Context",
46
+ code_explanation: "Code Explanation",
47
+ negative_result: "Negative Result",
48
+ constraint: "Constraint",
49
+ };
50
+ const DISPLAY_TO_TYPE = Object.fromEntries(Object.entries(TYPE_DISPLAY).map(([k, v]) => [v.toLowerCase(), k]));
51
+ function okfType(type) {
52
+ return TYPE_DISPLAY[type] ?? "Reference";
53
+ }
54
+ function kageType(display) {
55
+ const key = String(display ?? "").trim().toLowerCase();
56
+ if (DISPLAY_TO_TYPE[key])
57
+ return DISPLAY_TO_TYPE[key];
58
+ if (kernel_js_1.MEMORY_TYPES.includes(key))
59
+ return key;
60
+ return "reference";
61
+ }
62
+ // ---- the lossless machine-state block ----
63
+ const STATE_FENCE_OPEN = "```json kage-state";
64
+ const STATE_FENCE_CLOSE = "```";
65
+ function extractKageState(content) {
66
+ const open = content.indexOf(STATE_FENCE_OPEN);
67
+ if (open === -1)
68
+ return null;
69
+ const start = open + STATE_FENCE_OPEN.length;
70
+ const close = content.indexOf("\n```", start);
71
+ if (close === -1)
72
+ return null;
73
+ try {
74
+ const obj = JSON.parse(content.slice(start, close).trim());
75
+ if (obj && obj.schema_version === kernel_js_1.PACKET_SCHEMA_VERSION && obj.id && obj.title)
76
+ return obj;
77
+ }
78
+ catch {
79
+ // fall through to best-effort import
80
+ }
81
+ return null;
82
+ }
83
+ // ---- small local helpers (kept self-contained; no reliance on kernel internals) ----
84
+ function yamlScalar(value) {
85
+ // JSON-encoded strings are valid YAML flow scalars and dodge every quoting/colon
86
+ // edge case, so we never hand-roll YAML escaping.
87
+ return JSON.stringify(value ?? "");
88
+ }
89
+ function yamlList(items) {
90
+ return `[${items.map((item) => JSON.stringify(item)).join(", ")}]`;
91
+ }
92
+ function firstHeading(body) {
93
+ const match = body.match(/^#\s+(.+)$/m);
94
+ return match ? match[1].trim() : null;
95
+ }
96
+ function stripFirstHeading(body) {
97
+ return body.replace(/^#\s+.+\n?/, "").trim();
98
+ }
99
+ function summarize(body) {
100
+ const text = body.replace(/\s+/g, " ").trim();
101
+ return text.length > 280 ? `${text.slice(0, 277).trimEnd()}…` : text;
102
+ }
103
+ function oneLine(value) {
104
+ const text = (value ?? "").replace(/\s+/g, " ").trim();
105
+ return text.length > 120 ? `${text.slice(0, 117)}…` : text;
106
+ }
107
+ function asStringArray(value) {
108
+ if (Array.isArray(value))
109
+ return value.map(String);
110
+ if (typeof value === "string" && value.trim()) {
111
+ return value.replace(/^\[|\]$/g, "").split(",").map((s) => s.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
112
+ }
113
+ return [];
114
+ }
115
+ function asNumber(value, fallback) {
116
+ const n = typeof value === "number" ? value : Number.parseFloat(String(value));
117
+ return Number.isFinite(n) ? n : fallback;
118
+ }
119
+ function ensureDir(path) {
120
+ if (!(0, node_fs_1.existsSync)(path))
121
+ (0, node_fs_1.mkdirSync)(path, { recursive: true });
122
+ }
123
+ function citationText(ref) {
124
+ const kind = ref.kind ? String(ref.kind) : "source";
125
+ const where = ref.path ? ` ${String(ref.path)}` : ref.resource ? ` ${String(ref.resource)}` : "";
126
+ const at = ref.captured_at ? ` (${String(ref.captured_at)})` : "";
127
+ return `${kind}${where}${at}`.trim();
128
+ }
129
+ function okfVerifiedStatus(packet) {
130
+ if (packet.status === "superseded")
131
+ return "superseded";
132
+ if (packet.status === "deprecated")
133
+ return "deprecated";
134
+ const freshness = packet.freshness;
135
+ return freshness && freshness.last_verified_at ? "verified" : "unverified";
136
+ }
137
+ // ---- packet -> OKF concept document ----
138
+ function packetToOkfConcept(packet) {
139
+ const fm = [];
140
+ fm.push(`type: ${yamlScalar(okfType(packet.type))}`);
141
+ fm.push(`title: ${yamlScalar(packet.title)}`);
142
+ fm.push(`description: ${yamlScalar(packet.summary ?? "")}`);
143
+ const resource = packet.paths?.[0];
144
+ if (resource)
145
+ fm.push(`resource: ${yamlScalar(resource)}`);
146
+ if (packet.tags?.length)
147
+ fm.push(`tags: ${yamlList(packet.tags)}`);
148
+ const freshness = packet.freshness;
149
+ const timestamp = (freshness && freshness.last_verified_at) || packet.updated_at;
150
+ if (timestamp)
151
+ fm.push(`timestamp: ${yamlScalar(String(timestamp))}`);
152
+ // Kage trust extension (OKF-legal arbitrary producer fields).
153
+ fm.push(`x-kage-id: ${yamlScalar(packet.id)}`);
154
+ fm.push(`x-kage-type: ${yamlScalar(packet.type)}`);
155
+ fm.push(`x-kage-status: ${yamlScalar(packet.status)}`);
156
+ fm.push(`x-kage-scope: ${yamlScalar(packet.scope)}`);
157
+ fm.push(`x-kage-visibility: ${yamlScalar(packet.visibility)}`);
158
+ fm.push(`x-kage-confidence: ${packet.confidence}`);
159
+ fm.push(`x-kage-verified: ${yamlScalar(okfVerifiedStatus(packet))}`);
160
+ if (packet.paths?.length)
161
+ fm.push(`x-kage-paths: ${yamlList(packet.paths)}`);
162
+ if (packet.stack?.length)
163
+ fm.push(`x-kage-stack: ${yamlList(packet.stack)}`);
164
+ const body = [];
165
+ body.push(`# ${packet.title}`, "");
166
+ if (packet.summary?.trim() && packet.summary.trim() !== packet.body?.trim())
167
+ body.push(`> ${oneLine(packet.summary)}`, "");
168
+ if (packet.body?.trim())
169
+ body.push(packet.body.trim(), "");
170
+ const ctx = packet.context;
171
+ if (ctx) {
172
+ if (ctx.why?.trim())
173
+ body.push("## Why", "", ctx.why.trim(), "");
174
+ if (ctx.trigger?.trim())
175
+ body.push("## Trigger", "", ctx.trigger.trim(), "");
176
+ if (ctx.action?.trim())
177
+ body.push("## Action", "", ctx.action.trim(), "");
178
+ if (ctx.verification?.trim())
179
+ body.push("## Verification", "", ctx.verification.trim(), "");
180
+ if (ctx.risk_if_forgotten?.trim())
181
+ body.push("## Risk if forgotten", "", ctx.risk_if_forgotten.trim(), "");
182
+ if (ctx.stale_when?.trim())
183
+ body.push("## Stale when", "", ctx.stale_when.trim(), "");
184
+ if (ctx.rejected_alternatives?.length) {
185
+ body.push("## Rejected alternatives", "", ...ctx.rejected_alternatives.map((alt) => `- ${alt}`), "");
186
+ }
187
+ }
188
+ if (packet.source_refs?.length) {
189
+ body.push("# Citations", "");
190
+ packet.source_refs.forEach((ref, i) => body.push(`[${i + 1}] ${citationText(ref)}`));
191
+ body.push("");
192
+ }
193
+ body.push("## Kage state", "", "Machine state for lossless round-trip; OKF consumers can ignore it.", "", STATE_FENCE_OPEN, JSON.stringify(packet), STATE_FENCE_CLOSE, "");
194
+ return `---\n${fm.join("\n")}\n---\n\n${body.join("\n")}\n`;
195
+ }
196
+ // ---- OKF concept document -> packet ----
197
+ function okfConceptToPacket(content, opts = {}) {
198
+ // A file carrying git conflict markers is not a valid concept — surface it as
199
+ // unparseable so validation flags it and the repair / merge-driver path (which
200
+ // parses each side separately) resolves it, rather than silently picking a side.
201
+ if (/^(?:<{7} |={7}\s*$|>{7} )/m.test(content))
202
+ return null;
203
+ // Lossless path: a Kage-authored concept carries its exact packet.
204
+ const state = extractKageState(content);
205
+ if (state)
206
+ return state;
207
+ // Best-effort import of a foreign / hand-authored OKF concept.
208
+ const { frontmatter, body } = (0, kernel_js_1.parseFrontmatter)(content);
209
+ const fm = frontmatter;
210
+ if (!String(fm.type ?? "").trim() && !firstHeading(body))
211
+ return null;
212
+ const projectDir = opts.projectDir ?? ".";
213
+ const base = opts.sourcePath ? (0, node_path_1.basename)(opts.sourcePath, ".md") : "concept";
214
+ const type = kageType(fm.type);
215
+ const title = String(fm.title ?? firstHeading(body) ?? base);
216
+ const cleanBody = stripFirstHeading(body) || body.trim();
217
+ const paths = fm["x-kage-paths"]
218
+ ? asStringArray(fm["x-kage-paths"])
219
+ : fm.resource
220
+ ? [String(fm.resource)]
221
+ : [];
222
+ const timestamp = fm.timestamp ? String(fm.timestamp) : new Date().toISOString();
223
+ return {
224
+ schema_version: kernel_js_1.PACKET_SCHEMA_VERSION,
225
+ id: fm["x-kage-id"] ? String(fm["x-kage-id"]) : (0, kernel_js_1.makePacketId)(projectDir, type, title, base),
226
+ title,
227
+ summary: String(fm.description ?? summarize(cleanBody)),
228
+ body: cleanBody,
229
+ type,
230
+ scope: (String(fm["x-kage-scope"] ?? "repo")),
231
+ visibility: (String(fm["x-kage-visibility"] ?? "team")),
232
+ sensitivity: "internal",
233
+ status: (String(fm["x-kage-status"] ?? "approved")),
234
+ confidence: asNumber(fm["x-kage-confidence"], 0.7),
235
+ tags: asStringArray(fm.tags),
236
+ paths,
237
+ stack: asStringArray(fm["x-kage-stack"]),
238
+ source_refs: [
239
+ { kind: "okf_import", path: opts.sourcePath ? (0, node_path_1.relative)(projectDir, opts.sourcePath) : undefined, resource: fm.resource },
240
+ ],
241
+ freshness: { ttl_days: 365, last_verified_at: fm.timestamp ?? null, verification: "okf_import" },
242
+ edges: [],
243
+ quality: { reviewer: null, votes_up: 0, votes_down: 0, uses_30d: 0, reports_stale: 0 },
244
+ created_at: timestamp,
245
+ updated_at: timestamp,
246
+ };
247
+ }
248
+ // ---- conformance lint ----
249
+ function lintOkfConcept(content) {
250
+ const errors = [];
251
+ if (!content.startsWith("---"))
252
+ errors.push("missing YAML frontmatter");
253
+ const { frontmatter } = (0, kernel_js_1.parseFrontmatter)(content);
254
+ if (!String(frontmatter.type ?? "").trim()) {
255
+ errors.push("frontmatter missing required non-empty `type`");
256
+ }
257
+ return { ok: errors.length === 0, errors };
258
+ }
259
+ // ---- filenames / bundle layout ----
260
+ function okfConceptFileName(packet) {
261
+ const idHash = (0, node_crypto_1.createHash)("sha256").update(packet.id).digest("hex").slice(0, 8);
262
+ return `${(0, kernel_js_1.slugify)(packet.title) || "concept"}-${idHash}.md`;
263
+ }
264
+ function okfBundleDir(projectDir) {
265
+ return (0, node_path_1.join)((0, kernel_js_1.memoryRoot)(projectDir), "okf");
266
+ }
267
+ function titleCaseDir(dir) {
268
+ return dir.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
269
+ }
270
+ function buildLog(entries) {
271
+ const byDate = new Map();
272
+ for (const entry of entries) {
273
+ const date = entry.date || "unknown";
274
+ if (!byDate.has(date))
275
+ byDate.set(date, []);
276
+ byDate.get(date).push(entry.line);
277
+ }
278
+ const out = ["# Log", ""];
279
+ for (const date of [...byDate.keys()].sort().reverse()) {
280
+ out.push(`## ${date}`, "", ...byDate.get(date).map((line) => `- ${line}`), "");
281
+ }
282
+ return `${out.join("\n")}\n`;
283
+ }
284
+ function migratePacketsToOkf(projectDir, opts = {}) {
285
+ const packets = [
286
+ ...(0, kernel_js_1.loadApprovedPackets)(projectDir),
287
+ ...(opts.includePending ? (0, kernel_js_1.loadPendingPackets)(projectDir) : []),
288
+ ];
289
+ const root = okfBundleDir(projectDir);
290
+ ensureDir(root);
291
+ const byType = {};
292
+ const indexByType = {};
293
+ const logEntries = [];
294
+ for (const packet of packets) {
295
+ const typeDir = (0, kernel_js_1.slugify)(okfType(packet.type));
296
+ const dir = (0, node_path_1.join)(root, typeDir);
297
+ ensureDir(dir);
298
+ const file = okfConceptFileName(packet);
299
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(dir, file), packetToOkfConcept(packet), "utf8");
300
+ byType[packet.type] = (byType[packet.type] ?? 0) + 1;
301
+ (indexByType[typeDir] ??= []).push(`- [${packet.title}](/${typeDir}/${file}) — ${oneLine(packet.summary)}`);
302
+ logEntries.push({
303
+ date: (packet.updated_at || packet.created_at || "").slice(0, 10),
304
+ line: `**Update** ${packet.title} (${okfType(packet.type)})`,
305
+ });
306
+ }
307
+ for (const [typeDir, lines] of Object.entries(indexByType)) {
308
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(root, typeDir, "index.md"), `# ${titleCaseDir(typeDir)}\n\n${lines.sort().join("\n")}\n`, "utf8");
309
+ }
310
+ const rootIndex = [
311
+ "# Kage knowledge bundle",
312
+ "",
313
+ `${packets.length} concepts in Open Knowledge Format (OKF) v0.1, with the Kage trust extension (\`x-kage-*\`).`,
314
+ "",
315
+ ];
316
+ for (const typeDir of Object.keys(indexByType).sort()) {
317
+ rootIndex.push(`- [${titleCaseDir(typeDir)}](/${typeDir}/index.md) (${indexByType[typeDir].length})`);
318
+ }
319
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(root, "index.md"), `${rootIndex.join("\n")}\n`, "utf8");
320
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(root, "log.md"), buildLog(logEntries), "utf8");
321
+ return { root, written: packets.length, byType };
322
+ }
323
+ // ---- bundle reader (consume any OKF bundle, ours or a third party's) ----
324
+ function walkMarkdown(root) {
325
+ const out = [];
326
+ let entries = [];
327
+ try {
328
+ entries = (0, node_fs_1.readdirSync)(root);
329
+ }
330
+ catch {
331
+ return out;
332
+ }
333
+ for (const name of entries) {
334
+ const path = (0, node_path_1.join)(root, name);
335
+ let stats;
336
+ try {
337
+ stats = (0, node_fs_1.statSync)(path);
338
+ }
339
+ catch {
340
+ continue;
341
+ }
342
+ if (stats.isDirectory())
343
+ out.push(...walkMarkdown(path));
344
+ else if (name.endsWith(".md"))
345
+ out.push(path);
346
+ }
347
+ return out.sort();
348
+ }
349
+ function loadOkfConcepts(dir, opts = {}) {
350
+ if (!(0, node_fs_1.existsSync)(dir))
351
+ return [];
352
+ const out = [];
353
+ for (const path of walkMarkdown(dir)) {
354
+ const name = (0, node_path_1.basename)(path);
355
+ if (name === "index.md" || name === "log.md")
356
+ continue;
357
+ try {
358
+ const packet = okfConceptToPacket((0, node_fs_1.readFileSync)(path, "utf8"), {
359
+ projectDir: opts.projectDir ?? dir,
360
+ sourcePath: path,
361
+ });
362
+ if (packet)
363
+ out.push(packet);
364
+ }
365
+ catch {
366
+ // a single malformed concept must not break the whole bundle read
367
+ }
368
+ }
369
+ return out;
370
+ }
371
+ function lintOkfBundle(dir) {
372
+ const failures = [];
373
+ let files = 0;
374
+ for (const path of walkMarkdown(dir)) {
375
+ const name = (0, node_path_1.basename)(path);
376
+ if (name === "index.md" || name === "log.md")
377
+ continue;
378
+ files += 1;
379
+ const result = lintOkfConcept((0, node_fs_1.readFileSync)(path, "utf8"));
380
+ if (!result.ok)
381
+ failures.push({ path, errors: result.errors });
382
+ }
383
+ return { files, failures };
384
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kage-core/kage-graph-mcp",
3
- "version": "2.5.7",
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, no account, no API key.",
3
+ "version": "3.0.0",
4
+ "description": "Verified memory for coding agents, stored as a Google Open Knowledge Format (OKF) bundle and checked against your code. The verification layer OKF leaves out. MCP server, no account, no API key.",
5
5
  "main": "dist/index.js",
6
6
  "files": [
7
7
  "dist/**/*.js",