@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 +48 -1
- package/dist/daemon.js +1 -1
- package/dist/kernel.js +81 -30
- package/dist/okf.js +384 -0
- package/package.json +2 -2
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/*.
|
|
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}.
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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),
|
|
16422
|
-
const packet =
|
|
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
|
-
|
|
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,
|
|
16505
|
+
for (const packetPath of walkFiles(dir, isPacketFile)) {
|
|
16464
16506
|
try {
|
|
16465
|
-
const packet =
|
|
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
|
-
//
|
|
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/*.
|
|
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,
|
|
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),
|
|
17013
|
+
const pendingFiles = walkFiles(pendingDir(projectDir), isPacketFile);
|
|
16963
17014
|
for (const path of pendingFiles) {
|
|
16964
|
-
const packet =
|
|
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
|
-
|
|
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),
|
|
17033
|
+
const pendingFiles = walkFiles(pendingDir(projectDir), isPacketFile);
|
|
16983
17034
|
for (const path of pendingFiles) {
|
|
16984
|
-
const packet =
|
|
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
|
|
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/*.
|
|
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": "
|
|
4
|
-
"description": "
|
|
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",
|