@kage-core/kage-graph-mcp 2.5.6 → 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 +52 -2
- package/dist/daemon.js +1 -1
- package/dist/kernel.js +91 -34
- package/dist/okf.js +384 -0
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -10,12 +10,12 @@ 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:
|
|
16
17
|
kage install [--project <dir>] one-shot: init + index + auto-wire detected agents
|
|
17
18
|
kage scan --project <dir> 60-second truth report on any repo (zero setup)
|
|
18
|
-
kage demo watch the reject/withhold loop run in a sandbox
|
|
19
19
|
kage init --project <dir> create repo memory (.agent_memory only)
|
|
20
20
|
kage index --project <dir> [--full] build/refresh code graph + indexes
|
|
21
21
|
kage recall "<query>" --project <dir> grounded recall from repo memory
|
|
@@ -52,7 +52,7 @@ Usage:
|
|
|
52
52
|
kage hook status --project <dir> [--json]
|
|
53
53
|
kage hook uninstall --project <dir> [--json]
|
|
54
54
|
kage refresh --project <dir> [--full] [--force] [--json]
|
|
55
|
-
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
|
|
56
56
|
kage gc --project <dir> [--dry-run] [--force] [--json]
|
|
57
57
|
kage compact --project <dir> [--dry-run] [--json]
|
|
58
58
|
kage verify --project <dir> [--id <packet-id>] [--json]
|
|
@@ -449,6 +449,52 @@ async function main() {
|
|
|
449
449
|
console.log(" npx -y @kage-core/kage-graph-mcp install");
|
|
450
450
|
return;
|
|
451
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
|
+
}
|
|
452
498
|
if (command === "init") {
|
|
453
499
|
const withPolicy = args.includes("--with-policy");
|
|
454
500
|
const result = (0, kernel_js_1.initProject)(projectArg(args), { policy: withPolicy });
|
|
@@ -1406,6 +1452,10 @@ async function main() {
|
|
|
1406
1452
|
`~${(0, kernel_js_1.formatTokenCount)(summary.all_time.replay_tokens)} all time ` +
|
|
1407
1453
|
`(discovery cost of served memories vs their compressed read cost)`);
|
|
1408
1454
|
}
|
|
1455
|
+
const usdOverridden = Number.isFinite(Number(process.env.KAGE_USD_PER_MTOK)) && Number(process.env.KAGE_USD_PER_MTOK) > 0;
|
|
1456
|
+
console.log(`\nDollars estimated at $${kernel_js_1.VALUE_DOLLARS_PER_MILLION_TOKENS}/1M input tokens ` +
|
|
1457
|
+
`(${usdOverridden ? "via KAGE_USD_PER_MTOK" : "Sonnet-class default — set KAGE_USD_PER_MTOK for your model"}). ` +
|
|
1458
|
+
`Ledger: .agent_memory/reports/value.json`);
|
|
1409
1459
|
return;
|
|
1410
1460
|
}
|
|
1411
1461
|
if (command === "file-context") {
|
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
|
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.PACKET_MERGE_DRIVER_CONFIG = exports.PACKET_MERGE_ATTRIBUTE_LINE = exports.AUTO_DISTILL_SIGNAL_THRESHOLD = exports.AUTO_DISTILL_TAG = exports.SETUP_AGENTS = exports.MEMORY_TYPES = exports.PACKET_SCHEMA_VERSION = void 0;
|
|
36
|
+
exports.PACKET_MERGE_DRIVER_CONFIG = exports.PACKET_MERGE_ATTRIBUTE_LINE = exports.AUTO_DISTILL_SIGNAL_THRESHOLD = exports.AUTO_DISTILL_TAG = exports.VALUE_DOLLARS_PER_MILLION_TOKENS = exports.SETUP_AGENTS = exports.MEMORY_TYPES = exports.PACKET_SCHEMA_VERSION = void 0;
|
|
37
37
|
exports.memoryRoot = memoryRoot;
|
|
38
38
|
exports.packetsDir = packetsDir;
|
|
39
39
|
exports.pendingDir = pendingDir;
|
|
@@ -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");
|
|
@@ -1017,8 +1035,14 @@ function recordRecallAccess(projectDir, results) {
|
|
|
1017
1035
|
// ---------------------------------------------------------------------------
|
|
1018
1036
|
const VALUE_LEDGER_SCHEMA_VERSION = 1;
|
|
1019
1037
|
const VALUE_LEDGER_EVENT_CAP = 5000;
|
|
1020
|
-
//
|
|
1021
|
-
|
|
1038
|
+
// Input price used for the dollar estimate. Default: Sonnet-class ~$3 per 1M input tokens (the
|
|
1039
|
+
// typical coding-agent tier) — deliberately conservative so the savings figure never overstates.
|
|
1040
|
+
// Override with KAGE_USD_PER_MTOK to match your model (e.g. 15 for Opus, 2.5 for GPT-4o, 0.8 for
|
|
1041
|
+
// Haiku). The old default ($15, Opus pricing) overstated savings ~5x for most users.
|
|
1042
|
+
exports.VALUE_DOLLARS_PER_MILLION_TOKENS = (() => {
|
|
1043
|
+
const raw = Number(process.env.KAGE_USD_PER_MTOK);
|
|
1044
|
+
return Number.isFinite(raw) && raw > 0 ? raw : 3;
|
|
1045
|
+
})();
|
|
1022
1046
|
function valueLedgerPath(projectDir) {
|
|
1023
1047
|
return (0, node_path_1.join)(reportsDir(projectDir), "value.json");
|
|
1024
1048
|
}
|
|
@@ -1112,7 +1136,7 @@ function recordValueEvent(projectDir, event) {
|
|
|
1112
1136
|
recordValueEvents(projectDir, [event]);
|
|
1113
1137
|
}
|
|
1114
1138
|
function estimatedTokenDollars(tokensSaved) {
|
|
1115
|
-
return Number(((tokensSaved / 1_000_000) * VALUE_DOLLARS_PER_MILLION_TOKENS).toFixed(2));
|
|
1139
|
+
return Number(((tokensSaved / 1_000_000) * exports.VALUE_DOLLARS_PER_MILLION_TOKENS).toFixed(2));
|
|
1116
1140
|
}
|
|
1117
1141
|
function summarizeValueWindow(events, cutoff) {
|
|
1118
1142
|
const window = { tokens_saved: 0, replay_tokens: 0, stale_withheld: 0, stale_caught: 0, recalls: 0, caller_answers: 0 };
|
|
@@ -2369,9 +2393,33 @@ function walkFiles(root, predicate) {
|
|
|
2369
2393
|
// Tolerant packet read: a single corrupt or merge-conflicted packet (e.g. an
|
|
2370
2394
|
// unresolved `<<<<<<<` from a teammate's git merge) must not take down all of
|
|
2371
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
|
+
}
|
|
2372
2420
|
function tryReadPacket(path) {
|
|
2373
2421
|
try {
|
|
2374
|
-
return
|
|
2422
|
+
return readPacketFromDisk(path);
|
|
2375
2423
|
}
|
|
2376
2424
|
catch (error) {
|
|
2377
2425
|
process.stderr.write(`kage: skipping unreadable memory packet ${path}: ${error.message}\n`);
|
|
@@ -2382,7 +2430,7 @@ function loadPacketsFromDir(dir) {
|
|
|
2382
2430
|
if (!(0, node_fs_1.existsSync)(dir))
|
|
2383
2431
|
return [];
|
|
2384
2432
|
return (0, node_fs_1.readdirSync)(dir)
|
|
2385
|
-
.filter(
|
|
2433
|
+
.filter(isPacketFile)
|
|
2386
2434
|
.sort()
|
|
2387
2435
|
.flatMap((name) => {
|
|
2388
2436
|
const packet = tryReadPacket((0, node_path_1.join)(dir, name));
|
|
@@ -2393,7 +2441,7 @@ function loadPacketEntriesFromDir(dir) {
|
|
|
2393
2441
|
if (!(0, node_fs_1.existsSync)(dir))
|
|
2394
2442
|
return [];
|
|
2395
2443
|
return (0, node_fs_1.readdirSync)(dir)
|
|
2396
|
-
.filter(
|
|
2444
|
+
.filter(isPacketFile)
|
|
2397
2445
|
.sort()
|
|
2398
2446
|
.flatMap((name) => {
|
|
2399
2447
|
const path = (0, node_path_1.join)(dir, name);
|
|
@@ -2416,7 +2464,7 @@ function recallablePendingPackets(projectDir) {
|
|
|
2416
2464
|
function writePacket(projectDir, packet, statusDir) {
|
|
2417
2465
|
const dir = statusDir === "packets" ? packetsDir(projectDir) : pendingDir(projectDir);
|
|
2418
2466
|
const path = (0, node_path_1.join)(dir, packetFileName(packet));
|
|
2419
|
-
|
|
2467
|
+
writePacketToDisk(path, packet);
|
|
2420
2468
|
return path;
|
|
2421
2469
|
}
|
|
2422
2470
|
function memoryAuditPath(projectDir) {
|
|
@@ -2815,7 +2863,7 @@ const NOISE_PATH_PREFIXES = [
|
|
|
2815
2863
|
"elm-stuff/",
|
|
2816
2864
|
];
|
|
2817
2865
|
function isReviewableMemoryPath(filePath) {
|
|
2818
|
-
return /^\.agent_memory\/(?:packets|pending)\/[^/]+\.json$/.test(filePath);
|
|
2866
|
+
return /^\.agent_memory\/(?:packets|pending)\/[^/]+\.(?:json|md)$/.test(filePath);
|
|
2819
2867
|
}
|
|
2820
2868
|
function isNoisePath(filePath) {
|
|
2821
2869
|
if (isReviewableMemoryPath(filePath))
|
|
@@ -6731,7 +6779,7 @@ function readCurrentKnowledgeGraph(projectDir, codeGraph, expectedInputHash) {
|
|
|
6731
6779
|
function graphFastFingerprint(projectDir) {
|
|
6732
6780
|
const packetPaths = (0, node_fs_1.existsSync)(packetsDir(projectDir))
|
|
6733
6781
|
? (0, node_fs_1.readdirSync)(packetsDir(projectDir))
|
|
6734
|
-
.filter(
|
|
6782
|
+
.filter(isPacketFile)
|
|
6735
6783
|
.map((name) => (0, node_path_1.join)(packetsDir(projectDir), name))
|
|
6736
6784
|
: [];
|
|
6737
6785
|
const paths = [
|
|
@@ -7147,7 +7195,7 @@ function compactProject(projectDir, options = {}) {
|
|
|
7147
7195
|
if (hardReason) {
|
|
7148
7196
|
deprecated.push({ id: packet.id, title: packet.title, reason: hardReason });
|
|
7149
7197
|
if (!dryRun)
|
|
7150
|
-
|
|
7198
|
+
writePacketToDisk(path, { ...packet, status: "deprecated", updated_at: nowIso() });
|
|
7151
7199
|
continue;
|
|
7152
7200
|
}
|
|
7153
7201
|
const meaningful = packet.paths.filter((p) => meaningfulMemoryPath(p) && !shouldSkipRepoMemoryPath(p));
|
|
@@ -13939,7 +13987,7 @@ function createPublicCandidate(projectDir, id) {
|
|
|
13939
13987
|
if (!validation.ok)
|
|
13940
13988
|
return { ok: false, errors: validation.errors };
|
|
13941
13989
|
const path = (0, node_path_1.join)(publicCandidatesDir(projectDir), packetFileName(packet));
|
|
13942
|
-
|
|
13990
|
+
writePacketToDisk(path, packet);
|
|
13943
13991
|
return { ok: true, packet, path, errors: [] };
|
|
13944
13992
|
}
|
|
13945
13993
|
function registryRecommendations(projectDir) {
|
|
@@ -15474,7 +15522,7 @@ function distillSession(projectDir, sessionId, options = {}) {
|
|
|
15474
15522
|
result.path = promotedPath;
|
|
15475
15523
|
}
|
|
15476
15524
|
else {
|
|
15477
|
-
|
|
15525
|
+
writePacketToDisk(result.path, result.packet);
|
|
15478
15526
|
}
|
|
15479
15527
|
return result;
|
|
15480
15528
|
};
|
|
@@ -15686,7 +15734,7 @@ function createDiffChangeMemory(projectDir, summary) {
|
|
|
15686
15734
|
const existing = (0, node_fs_1.readdirSync)(packetsDir(projectDir)).filter((name) => name.startsWith(stalePrefix) && name !== stableFileName);
|
|
15687
15735
|
for (const name of existing) {
|
|
15688
15736
|
const stale = (0, node_path_1.join)(packetsDir(projectDir), name);
|
|
15689
|
-
const stalePacket =
|
|
15737
|
+
const stalePacket = readPacketFromDisk(stale);
|
|
15690
15738
|
if (stalePacket?.type === "workflow" && stalePacket?.title === title) {
|
|
15691
15739
|
(0, node_fs_1.unlinkSync)(stale);
|
|
15692
15740
|
}
|
|
@@ -16044,7 +16092,7 @@ function prCheck(projectDir) {
|
|
|
16044
16092
|
.split(/\r?\n/)
|
|
16045
16093
|
.map(parsePorcelainPath)
|
|
16046
16094
|
.map((path) => path.replace(/^.* -> /, ""))
|
|
16047
|
-
.filter((path) => path.startsWith(".agent_memory/packets/") && path
|
|
16095
|
+
.filter((path) => path.startsWith(".agent_memory/packets/") && isPacketFile(path))).sort();
|
|
16048
16096
|
const codeGraphCurrent = graphIsCurrent(projectDir, ".agent_memory/code_graph/graph.json", { head: overlay.head, tree, inputHash: codeInputHash });
|
|
16049
16097
|
const memoryGraphCurrent = graphIsCurrent(projectDir, ".agent_memory/graph/graph.json", { head: overlay.head, tree, inputHash: memoryInputHash });
|
|
16050
16098
|
const sessions = kageSessionCaptureReport(projectDir);
|
|
@@ -16412,8 +16460,8 @@ function recordFeedback(projectDir, id, feedback) {
|
|
|
16412
16460
|
if (!["helpful", "wrong", "stale"].includes(feedback)) {
|
|
16413
16461
|
return { ok: false, errors: [`Invalid feedback: ${feedback}`] };
|
|
16414
16462
|
}
|
|
16415
|
-
for (const path of walkFiles(packetsDir(projectDir),
|
|
16416
|
-
const packet =
|
|
16463
|
+
for (const path of walkFiles(packetsDir(projectDir), isPacketFile)) {
|
|
16464
|
+
const packet = readPacketFromDisk(path);
|
|
16417
16465
|
if (packet.id !== id)
|
|
16418
16466
|
continue;
|
|
16419
16467
|
const quality = (packet.quality ?? {});
|
|
@@ -16434,7 +16482,7 @@ function recordFeedback(projectDir, id, feedback) {
|
|
|
16434
16482
|
stale_reported_at: packet.updated_at,
|
|
16435
16483
|
};
|
|
16436
16484
|
}
|
|
16437
|
-
|
|
16485
|
+
writePacketToDisk(path, packet);
|
|
16438
16486
|
recordMemoryAudit(projectDir, "feedback", [packet], {
|
|
16439
16487
|
feedback,
|
|
16440
16488
|
path: (0, node_path_1.relative)(projectDir, path),
|
|
@@ -16454,9 +16502,9 @@ function validateProject(projectDir) {
|
|
|
16454
16502
|
[pendingDir(projectDir), "pending"],
|
|
16455
16503
|
[publicCandidatesDir(projectDir), "public candidate"],
|
|
16456
16504
|
]) {
|
|
16457
|
-
for (const packetPath of walkFiles(dir,
|
|
16505
|
+
for (const packetPath of walkFiles(dir, isPacketFile)) {
|
|
16458
16506
|
try {
|
|
16459
|
-
const packet =
|
|
16507
|
+
const packet = readPacketFromDisk(packetPath);
|
|
16460
16508
|
const validation = validatePacket(packet, (0, node_path_1.relative)(projectDir, packetPath));
|
|
16461
16509
|
errors.push(...validation.errors);
|
|
16462
16510
|
warnings.push(...validation.warnings);
|
|
@@ -16654,12 +16702,17 @@ function resolveConflictedPacket(content) {
|
|
|
16654
16702
|
for (const side of [sides.ours, sides.theirs]) {
|
|
16655
16703
|
try {
|
|
16656
16704
|
const parsed = JSON.parse(side);
|
|
16657
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
|
|
16705
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
16658
16706
|
candidates.push(parsed);
|
|
16707
|
+
continue;
|
|
16708
|
+
}
|
|
16659
16709
|
}
|
|
16660
16710
|
catch {
|
|
16661
|
-
//
|
|
16711
|
+
// Not JSON — fall through and try the OKF concept format below.
|
|
16662
16712
|
}
|
|
16713
|
+
const okf = (0, okf_js_1.okfConceptToPacket)(side);
|
|
16714
|
+
if (okf)
|
|
16715
|
+
candidates.push(okf);
|
|
16663
16716
|
}
|
|
16664
16717
|
if (!candidates.length)
|
|
16665
16718
|
return null;
|
|
@@ -16672,7 +16725,7 @@ function resolveConflictedPacket(content) {
|
|
|
16672
16725
|
// exit 0 on success, non-zero to leave the conflict). v1 policy is whole-file
|
|
16673
16726
|
// newest-wins by updated_at — packets are single facts, so field-level merges
|
|
16674
16727
|
// buy little over taking the most recently verified side.
|
|
16675
|
-
exports.PACKET_MERGE_ATTRIBUTE_LINE = ".agent_memory/packets/*.
|
|
16728
|
+
exports.PACKET_MERGE_ATTRIBUTE_LINE = ".agent_memory/packets/*.md merge=kage-packet";
|
|
16676
16729
|
exports.PACKET_MERGE_DRIVER_CONFIG = 'git config merge.kage-packet.driver "npx -y @kage-core/kage-graph-mcp merge-packet %A %O %B"';
|
|
16677
16730
|
function mergePacketFiles(oursPath, basePath, theirsPath) {
|
|
16678
16731
|
void basePath; // Reserved for a future field-level three-way merge.
|
|
@@ -16680,6 +16733,10 @@ function mergePacketFiles(oursPath, basePath, theirsPath) {
|
|
|
16680
16733
|
const raw = safeReadText(path);
|
|
16681
16734
|
if (raw === null)
|
|
16682
16735
|
return null;
|
|
16736
|
+
if (path.endsWith(".md")) {
|
|
16737
|
+
const packet = (0, okf_js_1.okfConceptToPacket)(raw);
|
|
16738
|
+
return packet ? { raw, packet } : null;
|
|
16739
|
+
}
|
|
16683
16740
|
try {
|
|
16684
16741
|
const parsed = JSON.parse(raw);
|
|
16685
16742
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
@@ -16729,7 +16786,7 @@ function ensurePacketMergeAttributes(projectDir) {
|
|
|
16729
16786
|
const path = (0, node_path_1.join)(projectDir, ".gitattributes");
|
|
16730
16787
|
const existing = safeReadText(path) ?? "";
|
|
16731
16788
|
const lines = existing.split(/\r?\n/);
|
|
16732
|
-
const pattern = /^\.agent_memory\/packets\/\*\.json\s+merge=/;
|
|
16789
|
+
const pattern = /^\.agent_memory\/packets\/\*\.(?:json|md)\s+merge=/;
|
|
16733
16790
|
const index = lines.findIndex((line) => pattern.test(line.trim()));
|
|
16734
16791
|
if (index !== -1) {
|
|
16735
16792
|
if (lines[index].trim() === exports.PACKET_MERGE_ATTRIBUTE_LINE)
|
|
@@ -16756,7 +16813,7 @@ function repairProject(projectDir, options = {}) {
|
|
|
16756
16813
|
[publicCandidatesDir(projectDir), "public-candidates"],
|
|
16757
16814
|
];
|
|
16758
16815
|
for (const [dir, label] of packetDirs) {
|
|
16759
|
-
for (const path of walkFiles(dir,
|
|
16816
|
+
for (const path of walkFiles(dir, isPacketFile)) {
|
|
16760
16817
|
const target = `${label}/${(0, node_path_1.basename)(path)}`;
|
|
16761
16818
|
let raw;
|
|
16762
16819
|
try {
|
|
@@ -16953,14 +17010,14 @@ function remediationFor(error) {
|
|
|
16953
17010
|
return "npx -y kage-graph-mcp doctor --project .";
|
|
16954
17011
|
}
|
|
16955
17012
|
function approvePending(projectDir, id) {
|
|
16956
|
-
const pendingFiles = walkFiles(pendingDir(projectDir),
|
|
17013
|
+
const pendingFiles = walkFiles(pendingDir(projectDir), isPacketFile);
|
|
16957
17014
|
for (const path of pendingFiles) {
|
|
16958
|
-
const packet =
|
|
17015
|
+
const packet = readPacketFromDisk(path);
|
|
16959
17016
|
if (packet.id === id) {
|
|
16960
17017
|
packet.status = "approved";
|
|
16961
17018
|
packet.updated_at = nowIso();
|
|
16962
17019
|
const target = (0, node_path_1.join)(packetsDir(projectDir), packetFileName(packet));
|
|
16963
|
-
|
|
17020
|
+
writePacketToDisk(target, packet);
|
|
16964
17021
|
(0, node_fs_1.renameSync)(path, `${path}.approved`);
|
|
16965
17022
|
recordMemoryAudit(projectDir, "approve", [packet], {
|
|
16966
17023
|
from: (0, node_path_1.relative)(projectDir, path),
|
|
@@ -16973,9 +17030,9 @@ function approvePending(projectDir, id) {
|
|
|
16973
17030
|
throw new Error(`Pending packet not found: ${id}`);
|
|
16974
17031
|
}
|
|
16975
17032
|
function rejectPending(projectDir, id) {
|
|
16976
|
-
const pendingFiles = walkFiles(pendingDir(projectDir),
|
|
17033
|
+
const pendingFiles = walkFiles(pendingDir(projectDir), isPacketFile);
|
|
16977
17034
|
for (const path of pendingFiles) {
|
|
16978
|
-
const packet =
|
|
17035
|
+
const packet = readPacketFromDisk(path);
|
|
16979
17036
|
if (packet.id === id) {
|
|
16980
17037
|
const target = `${path}.rejected`;
|
|
16981
17038
|
(0, node_fs_1.renameSync)(path, target);
|
|
@@ -17733,7 +17790,7 @@ function syncIdentityArgs(memoryDir) {
|
|
|
17733
17790
|
}
|
|
17734
17791
|
const SYNC_SETUP_HINT = "Run: kage sync setup --remote <git-url>";
|
|
17735
17792
|
function syncPacketFile(file) {
|
|
17736
|
-
return file.startsWith("packets/") && file
|
|
17793
|
+
return file.startsWith("packets/") && isPacketFile(file);
|
|
17737
17794
|
}
|
|
17738
17795
|
function countSyncPacketFiles(namesOutput) {
|
|
17739
17796
|
return namesOutput
|
|
@@ -17816,7 +17873,7 @@ function rebaseOntoUpstream(memoryDir, upstream) {
|
|
|
17816
17873
|
for (const file of conflicted) {
|
|
17817
17874
|
if (!syncPacketFile(file)) {
|
|
17818
17875
|
runSyncGit(memoryDir, ["rebase", "--abort"]);
|
|
17819
|
-
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}.` };
|
|
17820
17877
|
}
|
|
17821
17878
|
const resolution = resolvePacketSyncConflict(memoryDir, file);
|
|
17822
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",
|