@viloforge/vfkb 0.2.3 → 0.4.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/broadcast.js +134 -0
- package/dist/bundles/vfkb-mcp.mjs +106 -39
- package/dist/bundles/vfkb.mjs +545 -177
- package/dist/cli.js +86 -5
- package/dist/doctor.js +35 -0
- package/dist/engine.js +80 -5
- package/dist/init.js +1 -1
- package/dist/journal.js +207 -0
- package/dist/storage.js +10 -1
- package/package.json +1 -1
package/dist/bundles/vfkb.mjs
CHANGED
|
@@ -9,7 +9,7 @@ var __export = (target, all) => {
|
|
|
9
9
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
10
10
|
|
|
11
11
|
// src/storage.ts
|
|
12
|
-
import { basename, dirname, join as
|
|
12
|
+
import { basename, dirname as dirname2, join as join4, resolve } from "node:path";
|
|
13
13
|
import { homedir } from "node:os";
|
|
14
14
|
import { createHash } from "node:crypto";
|
|
15
15
|
|
|
@@ -207,6 +207,134 @@ function storageBackend() {
|
|
|
207
207
|
return current;
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
+
// src/journal.ts
|
|
211
|
+
import { execFileSync } from "node:child_process";
|
|
212
|
+
import { appendFileSync as appendFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
213
|
+
import { dirname, join as join3, relative, sep } from "node:path";
|
|
214
|
+
var walPath = (brain) => join3(brain, ".journal", "wal.jsonl");
|
|
215
|
+
var suppressedPath = (brain) => join3(brain, ".journal", "suppressed");
|
|
216
|
+
function rewriteWal(wal, keep) {
|
|
217
|
+
const tmp = `${wal}.tmp`;
|
|
218
|
+
writeFileSync2(tmp, keep.length > 0 ? keep.map((l) => l.raw).join("\n") + "\n" : "", "utf8");
|
|
219
|
+
renameSync(tmp, wal);
|
|
220
|
+
}
|
|
221
|
+
var pairOf = (r) => `${String(r.id)}\0${String(r.updated ?? "")}`;
|
|
222
|
+
function parseLines(text) {
|
|
223
|
+
const out = [];
|
|
224
|
+
for (const raw of text.split("\n")) {
|
|
225
|
+
if (raw.trim().length === 0) continue;
|
|
226
|
+
try {
|
|
227
|
+
const rec = JSON.parse(raw);
|
|
228
|
+
if (typeof rec.id !== "string") continue;
|
|
229
|
+
out.push({ raw, pair: pairOf(rec), id: rec.id });
|
|
230
|
+
} catch {
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return out;
|
|
234
|
+
}
|
|
235
|
+
function pairsOfFile(path) {
|
|
236
|
+
if (!existsSync2(path)) return /* @__PURE__ */ new Set();
|
|
237
|
+
return new Set(parseLines(readFileSync3(path, "utf8")).map((l) => l.pair));
|
|
238
|
+
}
|
|
239
|
+
function suppressedPairs(brain) {
|
|
240
|
+
const p = suppressedPath(brain);
|
|
241
|
+
if (!existsSync2(p)) return /* @__PURE__ */ new Set();
|
|
242
|
+
return new Set(
|
|
243
|
+
readFileSync3(p, "utf8").split("\n").filter((l) => l.includes(" ")).map((l) => l.replace(" ", "\0"))
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
function journalAppend(brain, rec) {
|
|
247
|
+
if (process.env.VFKB_NO_JOURNAL) return;
|
|
248
|
+
try {
|
|
249
|
+
mkdirSync3(join3(brain, ".journal"), { recursive: true });
|
|
250
|
+
appendFileSync2(walPath(brain), JSON.stringify(rec) + "\n", "utf8");
|
|
251
|
+
} catch (err) {
|
|
252
|
+
process.stderr.write(
|
|
253
|
+
`vfkb: journal mirror failed (primary append proceeds unprotected): ${err.message}
|
|
254
|
+
`
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
function pairsAtHead(brain) {
|
|
259
|
+
const repoDir = dirname(brain);
|
|
260
|
+
const git2 = (...a) => execFileSync("git", ["-C", repoDir, ...a], {
|
|
261
|
+
encoding: "utf8",
|
|
262
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
263
|
+
}).trim();
|
|
264
|
+
try {
|
|
265
|
+
if (git2("rev-parse", "--is-inside-work-tree") !== "true") return "not-git";
|
|
266
|
+
} catch {
|
|
267
|
+
return "not-git";
|
|
268
|
+
}
|
|
269
|
+
try {
|
|
270
|
+
const top = git2("rev-parse", "--show-toplevel");
|
|
271
|
+
const rel = relative(top, join3(brain, "entries.jsonl")).split(sep).join("/");
|
|
272
|
+
const head = execFileSync("git", ["-C", repoDir, "cat-file", "-p", `HEAD:${rel}`], {
|
|
273
|
+
encoding: "utf8",
|
|
274
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
275
|
+
});
|
|
276
|
+
return new Set(parseLines(head).map((l) => l.pair));
|
|
277
|
+
} catch {
|
|
278
|
+
return "unknown";
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
function recoverFromJournal(brain) {
|
|
282
|
+
if (process.env.VFKB_NO_JOURNAL) return { restored: 0, pruned: 0 };
|
|
283
|
+
const wal = walPath(brain);
|
|
284
|
+
if (!existsSync2(wal)) return { restored: 0, pruned: 0 };
|
|
285
|
+
const walLines = parseLines(readFileSync3(wal, "utf8"));
|
|
286
|
+
if (walLines.length === 0) return { restored: 0, pruned: 0 };
|
|
287
|
+
const entriesPath = join3(brain, "entries.jsonl");
|
|
288
|
+
const present = pairsOfFile(entriesPath);
|
|
289
|
+
const suppressed = suppressedPairs(brain);
|
|
290
|
+
const toRestore = walLines.filter((l) => !present.has(l.pair) && !suppressed.has(l.pair));
|
|
291
|
+
if (toRestore.length > 0) {
|
|
292
|
+
let prefix = "";
|
|
293
|
+
if (existsSync2(entriesPath)) {
|
|
294
|
+
const cur = readFileSync3(entriesPath, "utf8");
|
|
295
|
+
if (cur.length > 0 && !cur.endsWith("\n")) prefix = "\n";
|
|
296
|
+
}
|
|
297
|
+
appendFileSync2(entriesPath, prefix + toRestore.map((l) => l.raw).join("\n") + "\n", "utf8");
|
|
298
|
+
}
|
|
299
|
+
const head = pairsAtHead(brain);
|
|
300
|
+
let keep;
|
|
301
|
+
if (head === "unknown") {
|
|
302
|
+
keep = walLines;
|
|
303
|
+
} else if (head === "not-git") {
|
|
304
|
+
keep = walLines.filter((l) => !suppressed.has(l.pair) && !present.has(l.pair) && !toRestore.includes(l));
|
|
305
|
+
} else {
|
|
306
|
+
keep = walLines.filter((l) => !suppressed.has(l.pair) && !head.has(l.pair));
|
|
307
|
+
}
|
|
308
|
+
const pruned = walLines.length - keep.length;
|
|
309
|
+
if (pruned > 0) {
|
|
310
|
+
rewriteWal(wal, keep);
|
|
311
|
+
}
|
|
312
|
+
return { restored: toRestore.length, pruned };
|
|
313
|
+
}
|
|
314
|
+
function purgeJournal(brain, opts) {
|
|
315
|
+
const wal = walPath(brain);
|
|
316
|
+
if (!existsSync2(wal)) return { purged: 0 };
|
|
317
|
+
const walLines = parseLines(readFileSync3(wal, "utf8"));
|
|
318
|
+
const gone = walLines.filter((l) => opts.all ? true : l.id === opts.id);
|
|
319
|
+
if (gone.length === 0) return { purged: 0 };
|
|
320
|
+
const keep = walLines.filter((l) => !gone.includes(l));
|
|
321
|
+
mkdirSync3(join3(brain, ".journal"), { recursive: true });
|
|
322
|
+
appendFileSync2(suppressedPath(brain), gone.map((l) => l.pair.replace("\0", " ")).join("\n") + "\n", "utf8");
|
|
323
|
+
rewriteWal(wal, keep);
|
|
324
|
+
return { purged: gone.length };
|
|
325
|
+
}
|
|
326
|
+
function journalStatus(brain) {
|
|
327
|
+
const wal = walPath(brain);
|
|
328
|
+
const walLines = existsSync2(wal) ? parseLines(readFileSync3(wal, "utf8")) : [];
|
|
329
|
+
const present = pairsOfFile(join3(brain, "entries.jsonl"));
|
|
330
|
+
const suppressed = suppressedPairs(brain);
|
|
331
|
+
return {
|
|
332
|
+
walLines: walLines.length,
|
|
333
|
+
restorable: walLines.filter((l) => !present.has(l.pair) && !suppressed.has(l.pair)).length,
|
|
334
|
+
suppressedInEntries: [...suppressed].filter((p) => present.has(p)).length
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
210
338
|
// node_modules/zod/v4/classic/external.js
|
|
211
339
|
var external_exports = {};
|
|
212
340
|
__export(external_exports, {
|
|
@@ -14772,7 +14900,7 @@ function isTombstone(r) {
|
|
|
14772
14900
|
return r.deleted === true;
|
|
14773
14901
|
}
|
|
14774
14902
|
function brainDir() {
|
|
14775
|
-
return process.env.VFKB_DATA_DIR || process.env.VFKB_DIR ||
|
|
14903
|
+
return process.env.VFKB_DATA_DIR || process.env.VFKB_DIR || join4(homedir(), ".vfkb");
|
|
14776
14904
|
}
|
|
14777
14905
|
function defaultProject() {
|
|
14778
14906
|
const raw = (() => {
|
|
@@ -14781,7 +14909,7 @@ function defaultProject() {
|
|
|
14781
14909
|
if (explicit) {
|
|
14782
14910
|
const abs = resolve(explicit);
|
|
14783
14911
|
const name = basename(abs);
|
|
14784
|
-
return name.startsWith(".") ? basename(
|
|
14912
|
+
return name.startsWith(".") ? basename(dirname2(abs)) : name;
|
|
14785
14913
|
}
|
|
14786
14914
|
const root = process.env.CLAUDE_PROJECT_DIR;
|
|
14787
14915
|
if (root) return basename(resolve(root));
|
|
@@ -14790,7 +14918,9 @@ function defaultProject() {
|
|
|
14790
14918
|
return raw.replace(/["<>&]/g, "") || "spike";
|
|
14791
14919
|
}
|
|
14792
14920
|
function appendRecord(rec) {
|
|
14793
|
-
storageBackend()
|
|
14921
|
+
const be = storageBackend();
|
|
14922
|
+
if (be.name === "jsonl-fs") journalAppend(be.location(), rec);
|
|
14923
|
+
be.append(rec);
|
|
14794
14924
|
writeMeta();
|
|
14795
14925
|
}
|
|
14796
14926
|
function withExclusive(fn) {
|
|
@@ -14960,21 +15090,21 @@ function assertNoSecrets(text) {
|
|
|
14960
15090
|
}
|
|
14961
15091
|
|
|
14962
15092
|
// src/counters.ts
|
|
14963
|
-
import { appendFileSync as
|
|
14964
|
-
import { join as
|
|
15093
|
+
import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync4, existsSync as existsSync3 } from "node:fs";
|
|
15094
|
+
import { join as join5 } from "node:path";
|
|
14965
15095
|
function signalsFile() {
|
|
14966
|
-
return
|
|
15096
|
+
return join5(brainDir(), ".signals", "counters.jsonl");
|
|
14967
15097
|
}
|
|
14968
15098
|
function recordSignal(entryId, kind, source) {
|
|
14969
15099
|
const sig = { entryId, kind, at: (/* @__PURE__ */ new Date()).toISOString(), source };
|
|
14970
|
-
|
|
14971
|
-
|
|
15100
|
+
mkdirSync4(join5(brainDir(), ".signals"), { recursive: true });
|
|
15101
|
+
appendFileSync3(signalsFile(), JSON.stringify(sig) + "\n", "utf8");
|
|
14972
15102
|
return sig;
|
|
14973
15103
|
}
|
|
14974
15104
|
function readSignals() {
|
|
14975
15105
|
const f = signalsFile();
|
|
14976
|
-
if (!
|
|
14977
|
-
return
|
|
15106
|
+
if (!existsSync3(f)) return [];
|
|
15107
|
+
return readFileSync4(f, "utf8").split("\n").filter((l) => l.trim().length > 0).map((l) => JSON.parse(l));
|
|
14978
15108
|
}
|
|
14979
15109
|
function tally(entryId, signals = readSignals()) {
|
|
14980
15110
|
let helpful = 0;
|
|
@@ -15350,6 +15480,22 @@ function latestHandoff(all = readAll(), today = nowIso().slice(0, 10), supersede
|
|
|
15350
15480
|
}
|
|
15351
15481
|
return latest;
|
|
15352
15482
|
}
|
|
15483
|
+
var CROSS_REPO_PIN_CAP_CHARS = 2e3;
|
|
15484
|
+
function latestCrossRepo(all = readAll(), today = nowIso().slice(0, 10), superseded = supersededIds(all)) {
|
|
15485
|
+
let latest = null;
|
|
15486
|
+
for (const e of all) {
|
|
15487
|
+
if (!e.tags.includes("cross-repo")) continue;
|
|
15488
|
+
if (e.tags.includes("handoff") || e.tags.includes("next")) continue;
|
|
15489
|
+
if (!isInjectable(e, today, superseded)) continue;
|
|
15490
|
+
if (!latest) {
|
|
15491
|
+
latest = e;
|
|
15492
|
+
continue;
|
|
15493
|
+
}
|
|
15494
|
+
const cmp = e.updated.localeCompare(latest.updated) || e.created.localeCompare(latest.created);
|
|
15495
|
+
if (cmp >= 0) latest = e;
|
|
15496
|
+
}
|
|
15497
|
+
return latest;
|
|
15498
|
+
}
|
|
15353
15499
|
function renderContextBundle(project = defaultProject(), budget = SESSION_BUDGET_CHARS) {
|
|
15354
15500
|
const all = readAll();
|
|
15355
15501
|
const today = nowIso().slice(0, 10);
|
|
@@ -15377,25 +15523,57 @@ function renderContextBundle(project = defaultProject(), budget = SESSION_BUDGET
|
|
|
15377
15523
|
body += `## Last handoff
|
|
15378
15524
|
- [${handoff.type} ${trustGlyph(handoff)}] ${text}
|
|
15379
15525
|
|
|
15526
|
+
`;
|
|
15527
|
+
}
|
|
15528
|
+
const crossRepo = latestCrossRepo(all, today, superseded);
|
|
15529
|
+
if (crossRepo && !constitutionalIds.has(crossRepo.id)) {
|
|
15530
|
+
const text = crossRepo.text.length > CROSS_REPO_PIN_CAP_CHARS ? `${crossRepo.text.slice(0, CROSS_REPO_PIN_CAP_CHARS)}\u2026 (truncated \u2014 kb_get ${crossRepo.id} for the rest)` : crossRepo.text;
|
|
15531
|
+
body += `## Cross-repo operations
|
|
15532
|
+
- [${crossRepo.type} ${trustGlyph(crossRepo)}] ${text}
|
|
15533
|
+
|
|
15380
15534
|
`;
|
|
15381
15535
|
}
|
|
15382
15536
|
body += renderContextMap() + "\n\n";
|
|
15537
|
+
const kept = [];
|
|
15538
|
+
let keptLen = 0;
|
|
15383
15539
|
let dropped = 0;
|
|
15540
|
+
const fixedLen = () => header.length + body.length + footer.length;
|
|
15384
15541
|
for (const e of injectable) {
|
|
15385
15542
|
if (constitutionalIds.has(e.id)) continue;
|
|
15386
15543
|
if (handoff && e.id === handoff.id) continue;
|
|
15544
|
+
if (crossRepo && e.id === crossRepo.id) continue;
|
|
15387
15545
|
const line = `- [${e.type} ${trustGlyph(e)}] ${e.text}
|
|
15388
15546
|
`;
|
|
15389
|
-
if (
|
|
15547
|
+
if (fixedLen() + keptLen + line.length > budget) {
|
|
15390
15548
|
dropped++;
|
|
15391
15549
|
continue;
|
|
15392
15550
|
}
|
|
15393
|
-
|
|
15551
|
+
kept.push(line);
|
|
15552
|
+
keptLen += line.length;
|
|
15394
15553
|
}
|
|
15395
15554
|
if (dropped > 0) {
|
|
15396
|
-
const note =
|
|
15555
|
+
const note = () => `(+ ${dropped} lower-ranked entries omitted for the ${budget}-char budget \u2014 kb_search / kb_list pulls them)
|
|
15397
15556
|
`;
|
|
15398
|
-
|
|
15557
|
+
const evicted = [];
|
|
15558
|
+
while (kept.length > 0 && fixedLen() + keptLen + note().length > budget) {
|
|
15559
|
+
const line = kept.pop();
|
|
15560
|
+
keptLen -= line.length;
|
|
15561
|
+
evicted.push(line);
|
|
15562
|
+
dropped++;
|
|
15563
|
+
}
|
|
15564
|
+
if (fixedLen() + keptLen + note().length <= budget) {
|
|
15565
|
+
body += kept.join("") + note();
|
|
15566
|
+
} else {
|
|
15567
|
+
while (evicted.length > 0) {
|
|
15568
|
+
const line = evicted.pop();
|
|
15569
|
+
kept.push(line);
|
|
15570
|
+
keptLen += line.length;
|
|
15571
|
+
dropped--;
|
|
15572
|
+
}
|
|
15573
|
+
body += kept.join("");
|
|
15574
|
+
}
|
|
15575
|
+
} else {
|
|
15576
|
+
body += kept.join("");
|
|
15399
15577
|
}
|
|
15400
15578
|
return header + body + footer;
|
|
15401
15579
|
}
|
|
@@ -15563,15 +15741,15 @@ ${bundle}`;
|
|
|
15563
15741
|
|
|
15564
15742
|
// src/export.ts
|
|
15565
15743
|
import {
|
|
15566
|
-
existsSync as
|
|
15567
|
-
mkdirSync as
|
|
15744
|
+
existsSync as existsSync4,
|
|
15745
|
+
mkdirSync as mkdirSync5,
|
|
15568
15746
|
readdirSync as readdirSync2,
|
|
15569
|
-
readFileSync as
|
|
15747
|
+
readFileSync as readFileSync5,
|
|
15570
15748
|
rmSync,
|
|
15571
15749
|
statSync as statSync2,
|
|
15572
|
-
writeFileSync as
|
|
15750
|
+
writeFileSync as writeFileSync3
|
|
15573
15751
|
} from "node:fs";
|
|
15574
|
-
import { dirname as
|
|
15752
|
+
import { dirname as dirname3, join as join6 } from "node:path";
|
|
15575
15753
|
var GENERATED_MARKER = "generated by vfkb export";
|
|
15576
15754
|
var MARKER_LINE_RE = /^<!-- generated by vfkb export /m;
|
|
15577
15755
|
var EXPORT_BUDGET_CHARS = 4e4;
|
|
@@ -15620,7 +15798,7 @@ function listFiles(dir) {
|
|
|
15620
15798
|
const out = [];
|
|
15621
15799
|
const walk = (d) => {
|
|
15622
15800
|
for (const name of readdirSync2(d)) {
|
|
15623
|
-
const p =
|
|
15801
|
+
const p = join6(d, name);
|
|
15624
15802
|
if (statSync2(p).isDirectory()) walk(p);
|
|
15625
15803
|
else out.push(p);
|
|
15626
15804
|
}
|
|
@@ -15631,14 +15809,14 @@ function listFiles(dir) {
|
|
|
15631
15809
|
function isGenerated(path) {
|
|
15632
15810
|
if (!path.endsWith(".md")) return false;
|
|
15633
15811
|
try {
|
|
15634
|
-
return MARKER_LINE_RE.test(
|
|
15812
|
+
return MARKER_LINE_RE.test(readFileSync5(path, "utf8"));
|
|
15635
15813
|
} catch {
|
|
15636
15814
|
return false;
|
|
15637
15815
|
}
|
|
15638
15816
|
}
|
|
15639
15817
|
function prepareTree(dir) {
|
|
15640
|
-
if (!
|
|
15641
|
-
|
|
15818
|
+
if (!existsSync4(dir)) {
|
|
15819
|
+
mkdirSync5(dir, { recursive: true });
|
|
15642
15820
|
return;
|
|
15643
15821
|
}
|
|
15644
15822
|
const files = listFiles(dir);
|
|
@@ -15653,7 +15831,7 @@ function prepareTree(dir) {
|
|
|
15653
15831
|
const dirs = [];
|
|
15654
15832
|
const collect = (d) => {
|
|
15655
15833
|
for (const name of readdirSync2(d)) {
|
|
15656
|
-
const p =
|
|
15834
|
+
const p = join6(d, name);
|
|
15657
15835
|
if (statSync2(p).isDirectory()) {
|
|
15658
15836
|
collect(p);
|
|
15659
15837
|
dirs.push(p);
|
|
@@ -15669,8 +15847,8 @@ function exportAgentsMd(opts = {}) {
|
|
|
15669
15847
|
const { superseded, asOf, eligible } = projectionInputs();
|
|
15670
15848
|
const project = opts.project ?? defaultProject();
|
|
15671
15849
|
const budget = opts.budget ?? EXPORT_BUDGET_CHARS;
|
|
15672
|
-
const out = opts.out ??
|
|
15673
|
-
if (
|
|
15850
|
+
const out = opts.out ?? join6(process.cwd(), "AGENTS.md");
|
|
15851
|
+
if (existsSync4(out) && !MARKER_LINE_RE.test(readFileSync5(out, "utf8"))) {
|
|
15674
15852
|
throw new Error(
|
|
15675
15853
|
`vfkb export: refusing to overwrite ${out} \u2014 it is not a previous export (no generated marker)`
|
|
15676
15854
|
);
|
|
@@ -15729,8 +15907,8 @@ ${spine.trim()}
|
|
|
15729
15907
|
}
|
|
15730
15908
|
if (dropped > 0) body += `<!-- ${dropped} entries omitted for the ${budget}-char budget -->
|
|
15731
15909
|
`;
|
|
15732
|
-
|
|
15733
|
-
|
|
15910
|
+
mkdirSync5(dirname3(out), { recursive: true });
|
|
15911
|
+
writeFileSync3(out, head + body);
|
|
15734
15912
|
return { path: out };
|
|
15735
15913
|
}
|
|
15736
15914
|
function okfDoc(e, asOf, exportedDirs) {
|
|
@@ -15827,14 +16005,14 @@ function deriveLog(eligibleIds, records, live) {
|
|
|
15827
16005
|
function exportOkf(opts = {}) {
|
|
15828
16006
|
const { superseded, asOf, eligible } = projectionInputs();
|
|
15829
16007
|
const project = opts.project ?? defaultProject();
|
|
15830
|
-
const dir = opts.out ??
|
|
16008
|
+
const dir = opts.out ?? join6(process.cwd(), ".okf");
|
|
15831
16009
|
prepareTree(dir);
|
|
15832
16010
|
const marker = `<!-- ${GENERATED_MARKER} okf; regenerate with \`vfkb export okf\`, do not hand-edit (as-of ${asOf}) -->`;
|
|
15833
16011
|
const files = [];
|
|
15834
16012
|
const write = (rel, content) => {
|
|
15835
|
-
const p =
|
|
15836
|
-
|
|
15837
|
-
|
|
16013
|
+
const p = join6(dir, rel);
|
|
16014
|
+
mkdirSync5(dirname3(p), { recursive: true });
|
|
16015
|
+
writeFileSync3(p, content);
|
|
15838
16016
|
files.push(rel);
|
|
15839
16017
|
};
|
|
15840
16018
|
const exportedDirs = new Map(eligible.map((e) => [e.id, TYPE_DIR[e.type]]));
|
|
@@ -15888,6 +16066,134 @@ function runExport(target, opts = {}) {
|
|
|
15888
16066
|
throw new Error("usage: vfkb export <agents-md|okf> [--out <path>]");
|
|
15889
16067
|
}
|
|
15890
16068
|
|
|
16069
|
+
// src/broadcast.ts
|
|
16070
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
16071
|
+
import { existsSync as existsSync6, readFileSync as readFileSync7 } from "node:fs";
|
|
16072
|
+
import { basename as basename2, join as join8, resolve as resolve2 } from "node:path";
|
|
16073
|
+
|
|
16074
|
+
// src/manifest.ts
|
|
16075
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
|
|
16076
|
+
import { dirname as dirname4, join as join7 } from "node:path";
|
|
16077
|
+
|
|
16078
|
+
// src/version.ts
|
|
16079
|
+
var SCHEMA_VERSION = 1;
|
|
16080
|
+
var ENGINE_VERSION = true ? "0.4.0" : ownPackageVersion();
|
|
16081
|
+
var ENGINE_COMMIT = true ? "63659e5" : "dev";
|
|
16082
|
+
|
|
16083
|
+
// src/manifest.ts
|
|
16084
|
+
function manifestPath(brainDir2) {
|
|
16085
|
+
return join7(brainDir2, "manifest.json");
|
|
16086
|
+
}
|
|
16087
|
+
function currentManifest() {
|
|
16088
|
+
return { schema_version: SCHEMA_VERSION, engine_version: ENGINE_VERSION, engine_commit: ENGINE_COMMIT };
|
|
16089
|
+
}
|
|
16090
|
+
function readManifest(brainDir2) {
|
|
16091
|
+
const p = manifestPath(brainDir2);
|
|
16092
|
+
if (!existsSync5(p)) return void 0;
|
|
16093
|
+
try {
|
|
16094
|
+
return JSON.parse(readFileSync6(p, "utf8"));
|
|
16095
|
+
} catch {
|
|
16096
|
+
return void 0;
|
|
16097
|
+
}
|
|
16098
|
+
}
|
|
16099
|
+
function writeManifest(brainDir2) {
|
|
16100
|
+
const p = manifestPath(brainDir2);
|
|
16101
|
+
const existed = existsSync5(p);
|
|
16102
|
+
const cur = readManifest(brainDir2);
|
|
16103
|
+
const next = currentManifest();
|
|
16104
|
+
if (cur && JSON.stringify(cur) === JSON.stringify(next)) return "skipped";
|
|
16105
|
+
mkdirSync6(dirname4(p), { recursive: true });
|
|
16106
|
+
writeFileSync4(p, JSON.stringify(next, null, 2) + "\n");
|
|
16107
|
+
return existed ? "updated" : "created";
|
|
16108
|
+
}
|
|
16109
|
+
|
|
16110
|
+
// src/broadcast.ts
|
|
16111
|
+
var FORBIDDEN_TAGS = /* @__PURE__ */ new Set(["handoff", "next"]);
|
|
16112
|
+
function targetBrainDir(target) {
|
|
16113
|
+
const abs = resolve2(target);
|
|
16114
|
+
return basename2(abs) === ".vfkb" ? abs : join8(abs, ".vfkb");
|
|
16115
|
+
}
|
|
16116
|
+
function gitPosture(repoDir) {
|
|
16117
|
+
const git2 = (...a) => execFileSync2("git", ["-C", repoDir, ...a], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
16118
|
+
try {
|
|
16119
|
+
const branch = git2("rev-parse", "--abbrev-ref", "HEAD");
|
|
16120
|
+
const parked = branch === "main" || branch === "master";
|
|
16121
|
+
return `uncommitted; target on ${branch}${parked ? " (parked \u2014 rides until a topic-branch brain commit)" : ""}`;
|
|
16122
|
+
} catch {
|
|
16123
|
+
try {
|
|
16124
|
+
if (git2("rev-parse", "--is-inside-work-tree") === "true") {
|
|
16125
|
+
return "uncommitted; target git repository on an unborn branch (no commits yet)";
|
|
16126
|
+
}
|
|
16127
|
+
} catch {
|
|
16128
|
+
}
|
|
16129
|
+
return "uncommitted; target not a git repository";
|
|
16130
|
+
}
|
|
16131
|
+
}
|
|
16132
|
+
function broadcast(text, targets, opts = {}) {
|
|
16133
|
+
const origin = defaultProject();
|
|
16134
|
+
const date5 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
16135
|
+
const extraTags = opts.tags ?? [];
|
|
16136
|
+
for (const t of extraTags) {
|
|
16137
|
+
if (FORBIDDEN_TAGS.has(t)) {
|
|
16138
|
+
throw new Error(
|
|
16139
|
+
`broadcast: tag '${t}' is forbidden on cross-repo records \u2014 it claims the resident's ADR-0049 continuity pin (ADR-0063 \xA71)`
|
|
16140
|
+
);
|
|
16141
|
+
}
|
|
16142
|
+
}
|
|
16143
|
+
const op = (opts.op || "OPERATION").toUpperCase();
|
|
16144
|
+
const MARKER = /^CROSS-REPO \S+ \(\d{4}-\d{2}-\d{2}, from [^)]+\):/;
|
|
16145
|
+
const stamped = MARKER.test(text) ? text : `CROSS-REPO ${op} (${date5}, from ${origin}): ${text}`;
|
|
16146
|
+
const results = [];
|
|
16147
|
+
const written = /* @__PURE__ */ new Set();
|
|
16148
|
+
for (const target of targets) {
|
|
16149
|
+
const brain = targetBrainDir(target);
|
|
16150
|
+
if (written.has(brain)) {
|
|
16151
|
+
results.push({ target, ok: false, reason: "duplicate target (already written in this broadcast)" });
|
|
16152
|
+
continue;
|
|
16153
|
+
}
|
|
16154
|
+
const repoDir = resolve2(brain, "..");
|
|
16155
|
+
const manifestPath2 = join8(brain, "manifest.json");
|
|
16156
|
+
let healed = false;
|
|
16157
|
+
if (!existsSync6(manifestPath2)) {
|
|
16158
|
+
if (!existsSync6(join8(brain, "entries.jsonl"))) {
|
|
16159
|
+
results.push({ target, ok: false, reason: `no brain (no entries.jsonl in ${brain}) \u2014 never bootstrap a wire-less brain (ADR-0063 \xA73)` });
|
|
16160
|
+
continue;
|
|
16161
|
+
}
|
|
16162
|
+
try {
|
|
16163
|
+
writeManifest(brain);
|
|
16164
|
+
} catch (err) {
|
|
16165
|
+
results.push({ target, ok: false, reason: `manifest heal failed: ${err.message}` });
|
|
16166
|
+
continue;
|
|
16167
|
+
}
|
|
16168
|
+
healed = true;
|
|
16169
|
+
}
|
|
16170
|
+
let schema;
|
|
16171
|
+
try {
|
|
16172
|
+
schema = JSON.parse(readFileSync7(manifestPath2, "utf8")).schema_version;
|
|
16173
|
+
} catch {
|
|
16174
|
+
results.push({ target, ok: false, reason: "unreadable manifest.json" });
|
|
16175
|
+
continue;
|
|
16176
|
+
}
|
|
16177
|
+
if (schema !== SCHEMA_VERSION) {
|
|
16178
|
+
results.push({ target, ok: false, reason: `brain schema ${JSON.stringify(schema)} unsupported by engine schema v${SCHEMA_VERSION}` });
|
|
16179
|
+
continue;
|
|
16180
|
+
}
|
|
16181
|
+
const prev = process.env.VFKB_DATA_DIR;
|
|
16182
|
+
try {
|
|
16183
|
+
process.env.VFKB_DATA_DIR = brain;
|
|
16184
|
+
const e = addEntry("fact", stamped, { role: "executor", tags: [.../* @__PURE__ */ new Set(["cross-repo", ...extraTags])] });
|
|
16185
|
+
written.add(brain);
|
|
16186
|
+
results.push({ target, ok: true, id: e.id, posture: gitPosture(repoDir), ...healed ? { healed } : {} });
|
|
16187
|
+
} catch (err) {
|
|
16188
|
+
results.push({ target, ok: false, reason: err.message, ...healed ? { healed } : {} });
|
|
16189
|
+
} finally {
|
|
16190
|
+
if (prev === void 0) delete process.env.VFKB_DATA_DIR;
|
|
16191
|
+
else process.env.VFKB_DATA_DIR = prev;
|
|
16192
|
+
}
|
|
16193
|
+
}
|
|
16194
|
+
return results;
|
|
16195
|
+
}
|
|
16196
|
+
|
|
15891
16197
|
// src/curator.ts
|
|
15892
16198
|
function get(id) {
|
|
15893
16199
|
const e = readAll().find((x) => x.id === id);
|
|
@@ -16086,7 +16392,7 @@ function queryExplained(opts = {}) {
|
|
|
16086
16392
|
}
|
|
16087
16393
|
|
|
16088
16394
|
// src/gating.ts
|
|
16089
|
-
import { resolve as
|
|
16395
|
+
import { resolve as resolve3 } from "node:path";
|
|
16090
16396
|
var WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
16091
16397
|
"write",
|
|
16092
16398
|
"edit",
|
|
@@ -16104,16 +16410,16 @@ function isBrainWrite(toolName, input, brain = brainDir()) {
|
|
|
16104
16410
|
if (!toolName || !WRITE_TOOLS.has(toolName.toLowerCase())) return false;
|
|
16105
16411
|
const p = extractPath(input);
|
|
16106
16412
|
if (!p) return false;
|
|
16107
|
-
const abs =
|
|
16108
|
-
const root =
|
|
16413
|
+
const abs = resolve3(p);
|
|
16414
|
+
const root = resolve3(brain);
|
|
16109
16415
|
return abs === root || abs.startsWith(root + "/");
|
|
16110
16416
|
}
|
|
16111
16417
|
var GATING_REASON = "vfkb: edit the brain via the engine/CLI/MCP, not by writing files directly (keeps the index, freshness, and no-secrets invariants).";
|
|
16112
16418
|
|
|
16113
16419
|
// src/stop-reminder.ts
|
|
16114
|
-
import { execFileSync } from "node:child_process";
|
|
16115
|
-
import { readFileSync as
|
|
16116
|
-
import { join as
|
|
16420
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
16421
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
16422
|
+
import { join as join9, relative as relative2 } from "node:path";
|
|
16117
16423
|
var STOP_REMINDER = 'vfkb decision-capture check: this turn changed code/docs but no `decision` was recorded to the brain. If a load-bearing decision was made, capture it now via `mcp__vfkb__kb_add` (type=decision, why=<rationale>, role=human) \u2014 or `vfkb add decision "\u2026" --why "\u2026" --role human` \u2014 and add an ADR under docs/adr/ for anything architectural. If NO decision was made this turn, just finish normally.';
|
|
16118
16424
|
var HANDOFF_MIN_ENTRIES = 3;
|
|
16119
16425
|
var HANDOFF_REMINDER = 'vfkb handoff check: this session has recorded knowledge but no `handoff`/`next` entry. If you are WRAPPING UP, record a durable handoff now \u2014 `mcp__vfkb__kb_add` (type=fact, tags=handoff,next, role=human) naming what the NEXT session should pick up (a real "next:", not just a summary). If you are still mid-session, ignore this and finish normally \u2014 the SessionEnd floor will leave a fallback if you never do.';
|
|
@@ -16129,11 +16435,11 @@ function decideStop(input, ctx) {
|
|
|
16129
16435
|
function hasUncommittedWork(cwd = process.cwd(), brain = brainDir()) {
|
|
16130
16436
|
let out;
|
|
16131
16437
|
try {
|
|
16132
|
-
out =
|
|
16438
|
+
out = execFileSync3("git", ["status", "--porcelain"], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
16133
16439
|
} catch {
|
|
16134
16440
|
return false;
|
|
16135
16441
|
}
|
|
16136
|
-
const brainRel =
|
|
16442
|
+
const brainRel = relative2(cwd, brain).replace(/\\/g, "/");
|
|
16137
16443
|
return out.split("\n").some((line) => {
|
|
16138
16444
|
const p = line.slice(3).trim();
|
|
16139
16445
|
if (!p) return false;
|
|
@@ -16142,17 +16448,17 @@ function hasUncommittedWork(cwd = process.cwd(), brain = brainDir()) {
|
|
|
16142
16448
|
});
|
|
16143
16449
|
}
|
|
16144
16450
|
function newBrainEntriesSinceHead(brain = brainDir(), cwd = process.cwd()) {
|
|
16145
|
-
const file2 =
|
|
16451
|
+
const file2 = join9(brain, "entries.jsonl");
|
|
16146
16452
|
let current2;
|
|
16147
16453
|
try {
|
|
16148
|
-
current2 =
|
|
16454
|
+
current2 = readFileSync8(file2, "utf8").split("\n").filter(Boolean);
|
|
16149
16455
|
} catch {
|
|
16150
16456
|
return [];
|
|
16151
16457
|
}
|
|
16152
|
-
const rel =
|
|
16458
|
+
const rel = relative2(cwd, file2).replace(/\\/g, "/");
|
|
16153
16459
|
let headCount = 0;
|
|
16154
16460
|
try {
|
|
16155
|
-
const head =
|
|
16461
|
+
const head = execFileSync3("git", ["show", `HEAD:${rel}`], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
16156
16462
|
headCount = head.split("\n").filter(Boolean).length;
|
|
16157
16463
|
} catch {
|
|
16158
16464
|
headCount = 0;
|
|
@@ -16177,14 +16483,14 @@ function gatherStopContext(cwd = process.cwd(), brain = brainDir()) {
|
|
|
16177
16483
|
}
|
|
16178
16484
|
|
|
16179
16485
|
// src/git.ts
|
|
16180
|
-
import { execFileSync as
|
|
16181
|
-
import { existsSync as
|
|
16182
|
-
import { join as
|
|
16486
|
+
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
16487
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
16488
|
+
import { join as join10 } from "node:path";
|
|
16183
16489
|
function git(args, cwd) {
|
|
16184
|
-
return
|
|
16490
|
+
return execFileSync4("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
16185
16491
|
}
|
|
16186
16492
|
function ensureRepo(brain) {
|
|
16187
|
-
if (!
|
|
16493
|
+
if (!existsSync7(join10(brain, ".git"))) {
|
|
16188
16494
|
git(["init", "-q"], brain);
|
|
16189
16495
|
}
|
|
16190
16496
|
}
|
|
@@ -16210,12 +16516,12 @@ function save(message = "vfkb: update", role = "engine", brain = brainDir()) {
|
|
|
16210
16516
|
}
|
|
16211
16517
|
|
|
16212
16518
|
// src/session-end.ts
|
|
16213
|
-
import { execFileSync as
|
|
16214
|
-
import { readFileSync as
|
|
16215
|
-
import { join as
|
|
16216
|
-
var realGit = (args, cwd) =>
|
|
16519
|
+
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
16520
|
+
import { readFileSync as readFileSync9 } from "node:fs";
|
|
16521
|
+
import { join as join11, isAbsolute } from "node:path";
|
|
16522
|
+
var realGit = (args, cwd) => execFileSync5("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
16217
16523
|
function brainEntriesRelPath(dataDir2) {
|
|
16218
|
-
return
|
|
16524
|
+
return join11(dataDir2, "entries.jsonl").replace(/\\/g, "/");
|
|
16219
16525
|
}
|
|
16220
16526
|
function tryGit(git2, args, cwd) {
|
|
16221
16527
|
try {
|
|
@@ -16239,7 +16545,7 @@ function countAdded(git2, cwd, path) {
|
|
|
16239
16545
|
function newEntriesSinceHead(git2, cwd, repoRelEntries, absEntries) {
|
|
16240
16546
|
let lines;
|
|
16241
16547
|
try {
|
|
16242
|
-
lines =
|
|
16548
|
+
lines = readFileSync9(absEntries, "utf8").split("\n").filter(Boolean);
|
|
16243
16549
|
} catch {
|
|
16244
16550
|
return [];
|
|
16245
16551
|
}
|
|
@@ -16316,8 +16622,8 @@ function runSessionEnd(opts = {}) {
|
|
|
16316
16622
|
systemMessage: `vfkb: ${added2} new brain entr${added2 === 1 ? "y" : "ies"} on \`${branch}\` left uncommitted \u2014 branch + commit to preserve continuity (vfkb never auto-commits the default branch).`
|
|
16317
16623
|
};
|
|
16318
16624
|
}
|
|
16319
|
-
const absBrain = isAbsolute(dataDir2) ? dataDir2 :
|
|
16320
|
-
const fresh = newEntriesSinceHead(git2, cwd, entries,
|
|
16625
|
+
const absBrain = isAbsolute(dataDir2) ? dataDir2 : join11(cwd, dataDir2);
|
|
16626
|
+
const fresh = newEntriesSinceHead(git2, cwd, entries, join11(absBrain, "entries.jsonl"));
|
|
16321
16627
|
let autoHandoff = false;
|
|
16322
16628
|
if (fresh.length > 0 && !fresh.some(isHandoff2)) {
|
|
16323
16629
|
try {
|
|
@@ -16337,46 +16643,8 @@ function runSessionEnd(opts = {}) {
|
|
|
16337
16643
|
}
|
|
16338
16644
|
|
|
16339
16645
|
// src/init.ts
|
|
16340
|
-
import { existsSync as
|
|
16341
|
-
import { basename as
|
|
16342
|
-
|
|
16343
|
-
// src/manifest.ts
|
|
16344
|
-
import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "node:fs";
|
|
16345
|
-
import { dirname as dirname3, join as join9 } from "node:path";
|
|
16346
|
-
|
|
16347
|
-
// src/version.ts
|
|
16348
|
-
var SCHEMA_VERSION = 1;
|
|
16349
|
-
var ENGINE_VERSION = true ? "0.2.3" : ownPackageVersion();
|
|
16350
|
-
var ENGINE_COMMIT = true ? "21adfd7" : "dev";
|
|
16351
|
-
|
|
16352
|
-
// src/manifest.ts
|
|
16353
|
-
function manifestPath(brainDir2) {
|
|
16354
|
-
return join9(brainDir2, "manifest.json");
|
|
16355
|
-
}
|
|
16356
|
-
function currentManifest() {
|
|
16357
|
-
return { schema_version: SCHEMA_VERSION, engine_version: ENGINE_VERSION, engine_commit: ENGINE_COMMIT };
|
|
16358
|
-
}
|
|
16359
|
-
function readManifest(brainDir2) {
|
|
16360
|
-
const p = manifestPath(brainDir2);
|
|
16361
|
-
if (!existsSync5(p)) return void 0;
|
|
16362
|
-
try {
|
|
16363
|
-
return JSON.parse(readFileSync7(p, "utf8"));
|
|
16364
|
-
} catch {
|
|
16365
|
-
return void 0;
|
|
16366
|
-
}
|
|
16367
|
-
}
|
|
16368
|
-
function writeManifest(brainDir2) {
|
|
16369
|
-
const p = manifestPath(brainDir2);
|
|
16370
|
-
const existed = existsSync5(p);
|
|
16371
|
-
const cur = readManifest(brainDir2);
|
|
16372
|
-
const next = currentManifest();
|
|
16373
|
-
if (cur && JSON.stringify(cur) === JSON.stringify(next)) return "skipped";
|
|
16374
|
-
mkdirSync5(dirname3(p), { recursive: true });
|
|
16375
|
-
writeFileSync3(p, JSON.stringify(next, null, 2) + "\n");
|
|
16376
|
-
return existed ? "updated" : "created";
|
|
16377
|
-
}
|
|
16378
|
-
|
|
16379
|
-
// src/init.ts
|
|
16646
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "node:fs";
|
|
16647
|
+
import { basename as basename3, join as join12 } from "node:path";
|
|
16380
16648
|
var AGENTS_MARKER = "<!-- vfkb:how-we-track-work -->";
|
|
16381
16649
|
var BOOTSTRAP_REL = ".vfkb/bin/bootstrap.mjs";
|
|
16382
16650
|
function mcpConfig(project) {
|
|
@@ -16461,48 +16729,48 @@ If it is unset, a session-start banner tells you; run \`vfkb doctor\` to check.
|
|
|
16461
16729
|
`;
|
|
16462
16730
|
}
|
|
16463
16731
|
function readJson(path) {
|
|
16464
|
-
if (!
|
|
16732
|
+
if (!existsSync8(path)) return void 0;
|
|
16465
16733
|
try {
|
|
16466
|
-
return JSON.parse(
|
|
16734
|
+
return JSON.parse(readFileSync10(path, "utf8"));
|
|
16467
16735
|
} catch {
|
|
16468
16736
|
return void 0;
|
|
16469
16737
|
}
|
|
16470
16738
|
}
|
|
16471
16739
|
function writeJson(path, value) {
|
|
16472
|
-
|
|
16740
|
+
writeFileSync5(path, JSON.stringify(value, null, 2) + "\n");
|
|
16473
16741
|
}
|
|
16474
16742
|
function eventHasVfkb(arr2) {
|
|
16475
16743
|
return JSON.stringify(arr2 ?? "").includes(BOOTSTRAP_REL);
|
|
16476
16744
|
}
|
|
16477
16745
|
function initProject(root, opts = {}) {
|
|
16478
|
-
const project = opts.project ||
|
|
16746
|
+
const project = opts.project || basename3(root) || "project";
|
|
16479
16747
|
const changes = [];
|
|
16480
|
-
const brainDir2 =
|
|
16481
|
-
const entries =
|
|
16482
|
-
if (!
|
|
16483
|
-
|
|
16484
|
-
|
|
16748
|
+
const brainDir2 = join12(root, ".vfkb");
|
|
16749
|
+
const entries = join12(brainDir2, "entries.jsonl");
|
|
16750
|
+
if (!existsSync8(entries)) {
|
|
16751
|
+
mkdirSync7(brainDir2, { recursive: true });
|
|
16752
|
+
writeFileSync5(entries, "");
|
|
16485
16753
|
changes.push({ path: ".vfkb/entries.jsonl", action: "created" });
|
|
16486
16754
|
} else {
|
|
16487
16755
|
changes.push({ path: ".vfkb/entries.jsonl", action: "skipped" });
|
|
16488
16756
|
}
|
|
16489
16757
|
changes.push({ path: ".vfkb/manifest.json", action: writeManifest(brainDir2) });
|
|
16490
16758
|
{
|
|
16491
|
-
const binDir =
|
|
16492
|
-
const path =
|
|
16493
|
-
const existed =
|
|
16494
|
-
const same = existed &&
|
|
16759
|
+
const binDir = join12(brainDir2, "bin");
|
|
16760
|
+
const path = join12(binDir, "bootstrap.mjs");
|
|
16761
|
+
const existed = existsSync8(path);
|
|
16762
|
+
const same = existed && readFileSync10(path, "utf8") === BOOTSTRAP_SRC;
|
|
16495
16763
|
if (same) {
|
|
16496
16764
|
changes.push({ path: BOOTSTRAP_REL, action: "skipped" });
|
|
16497
16765
|
} else {
|
|
16498
|
-
|
|
16499
|
-
|
|
16766
|
+
mkdirSync7(binDir, { recursive: true });
|
|
16767
|
+
writeFileSync5(path, BOOTSTRAP_SRC);
|
|
16500
16768
|
changes.push({ path: BOOTSTRAP_REL, action: existed ? "updated" : "created" });
|
|
16501
16769
|
}
|
|
16502
16770
|
}
|
|
16503
16771
|
{
|
|
16504
|
-
const path =
|
|
16505
|
-
const existed =
|
|
16772
|
+
const path = join12(root, ".mcp.json");
|
|
16773
|
+
const existed = existsSync8(path);
|
|
16506
16774
|
const cfg = readJson(path) ?? {};
|
|
16507
16775
|
cfg.mcpServers = cfg.mcpServers ?? {};
|
|
16508
16776
|
const desired = mcpConfig(project);
|
|
@@ -16516,9 +16784,9 @@ function initProject(root, opts = {}) {
|
|
|
16516
16784
|
}
|
|
16517
16785
|
}
|
|
16518
16786
|
{
|
|
16519
|
-
const dir =
|
|
16520
|
-
const path =
|
|
16521
|
-
const existed =
|
|
16787
|
+
const dir = join12(root, ".claude");
|
|
16788
|
+
const path = join12(dir, "settings.json");
|
|
16789
|
+
const existed = existsSync8(path);
|
|
16522
16790
|
const cfg = readJson(path) ?? {};
|
|
16523
16791
|
cfg.hooks = cfg.hooks ?? {};
|
|
16524
16792
|
const want = settingsHooks(project);
|
|
@@ -16533,7 +16801,7 @@ function initProject(root, opts = {}) {
|
|
|
16533
16801
|
touched = true;
|
|
16534
16802
|
}
|
|
16535
16803
|
if (touched) {
|
|
16536
|
-
|
|
16804
|
+
mkdirSync7(dir, { recursive: true });
|
|
16537
16805
|
writeJson(path, cfg);
|
|
16538
16806
|
changes.push({ path: ".claude/settings.json", action: existed ? "updated" : "created" });
|
|
16539
16807
|
} else {
|
|
@@ -16541,10 +16809,10 @@ function initProject(root, opts = {}) {
|
|
|
16541
16809
|
}
|
|
16542
16810
|
}
|
|
16543
16811
|
{
|
|
16544
|
-
const path =
|
|
16545
|
-
const lines = [".vfkb/index-meta.json", ".vfkb/.sessions/", ".vfkb/.signals/"];
|
|
16546
|
-
const existed =
|
|
16547
|
-
const cur = existed ?
|
|
16812
|
+
const path = join12(root, ".gitignore");
|
|
16813
|
+
const lines = [".vfkb/index-meta.json", ".vfkb/.sessions/", ".vfkb/.signals/", ".vfkb/.journal/"];
|
|
16814
|
+
const existed = existsSync8(path);
|
|
16815
|
+
const cur = existed ? readFileSync10(path, "utf8") : "";
|
|
16548
16816
|
const missing = lines.filter((l) => !cur.split(/\r?\n/).includes(l));
|
|
16549
16817
|
if (missing.length === 0) {
|
|
16550
16818
|
changes.push({ path: ".gitignore", action: "skipped" });
|
|
@@ -16553,15 +16821,15 @@ function initProject(root, opts = {}) {
|
|
|
16553
16821
|
const block = `${prefix}${cur ? "\n" : ""}# vfkb \u2014 derived/operational (only .vfkb/entries.jsonl is committed)
|
|
16554
16822
|
${missing.join("\n")}
|
|
16555
16823
|
`;
|
|
16556
|
-
|
|
16824
|
+
writeFileSync5(path, cur + block);
|
|
16557
16825
|
changes.push({ path: ".gitignore", action: existed ? "updated" : "created" });
|
|
16558
16826
|
}
|
|
16559
16827
|
}
|
|
16560
16828
|
{
|
|
16561
|
-
const path =
|
|
16829
|
+
const path = join12(root, ".gitattributes");
|
|
16562
16830
|
const line = ".vfkb/entries.jsonl merge=union";
|
|
16563
|
-
const existed =
|
|
16564
|
-
const cur = existed ?
|
|
16831
|
+
const existed = existsSync8(path);
|
|
16832
|
+
const cur = existed ? readFileSync10(path, "utf8") : "";
|
|
16565
16833
|
if (cur.split(/\r?\n/).includes(line)) {
|
|
16566
16834
|
changes.push({ path: ".gitattributes", action: "skipped" });
|
|
16567
16835
|
} else {
|
|
@@ -16569,19 +16837,19 @@ ${missing.join("\n")}
|
|
|
16569
16837
|
const block = `${prefix}${cur ? "\n" : ""}# vfkb \u2014 the append-only brain unions across branches (ADR-0041)
|
|
16570
16838
|
${line}
|
|
16571
16839
|
`;
|
|
16572
|
-
|
|
16840
|
+
writeFileSync5(path, cur + block);
|
|
16573
16841
|
changes.push({ path: ".gitattributes", action: existed ? "updated" : "created" });
|
|
16574
16842
|
}
|
|
16575
16843
|
}
|
|
16576
16844
|
{
|
|
16577
|
-
const path =
|
|
16578
|
-
const existed =
|
|
16579
|
-
const cur = existed ?
|
|
16845
|
+
const path = join12(root, "AGENTS.md");
|
|
16846
|
+
const existed = existsSync8(path);
|
|
16847
|
+
const cur = existed ? readFileSync10(path, "utf8") : "";
|
|
16580
16848
|
if (cur.includes(AGENTS_MARKER)) {
|
|
16581
16849
|
changes.push({ path: "AGENTS.md", action: "skipped" });
|
|
16582
16850
|
} else {
|
|
16583
|
-
const
|
|
16584
|
-
|
|
16851
|
+
const sep2 = cur && !cur.endsWith("\n") ? "\n\n" : cur ? "\n" : "";
|
|
16852
|
+
writeFileSync5(path, cur + sep2 + agentsSnippet(project));
|
|
16585
16853
|
changes.push({ path: "AGENTS.md", action: existed ? "updated" : "created" });
|
|
16586
16854
|
}
|
|
16587
16855
|
}
|
|
@@ -16600,13 +16868,13 @@ function approvalNotice(project) {
|
|
|
16600
16868
|
}
|
|
16601
16869
|
|
|
16602
16870
|
// src/doctor.ts
|
|
16603
|
-
import { execFileSync as
|
|
16604
|
-
import { existsSync as
|
|
16605
|
-
import { join as
|
|
16871
|
+
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
16872
|
+
import { existsSync as existsSync9, readFileSync as readFileSync11, writeFileSync as writeFileSync6, mkdirSync as mkdirSync8 } from "node:fs";
|
|
16873
|
+
import { join as join13, dirname as dirname5 } from "node:path";
|
|
16606
16874
|
function readJson2(path) {
|
|
16607
|
-
if (!
|
|
16875
|
+
if (!existsSync9(path)) return void 0;
|
|
16608
16876
|
try {
|
|
16609
|
-
return JSON.parse(
|
|
16877
|
+
return JSON.parse(readFileSync11(path, "utf8"));
|
|
16610
16878
|
} catch {
|
|
16611
16879
|
return void 0;
|
|
16612
16880
|
}
|
|
@@ -16639,16 +16907,16 @@ function detectPluginWiring(settings, root, pluginsFile) {
|
|
|
16639
16907
|
}
|
|
16640
16908
|
function claudeConfigDir(env) {
|
|
16641
16909
|
if (env.CLAUDE_CONFIG_DIR) return env.CLAUDE_CONFIG_DIR;
|
|
16642
|
-
return env.HOME ?
|
|
16910
|
+
return env.HOME ? join13(env.HOME, ".claude") : void 0;
|
|
16643
16911
|
}
|
|
16644
16912
|
function localHeadSha(cloneDir) {
|
|
16645
|
-
const head = readFileMaybe(
|
|
16913
|
+
const head = readFileMaybe(join13(cloneDir, ".git", "HEAD"));
|
|
16646
16914
|
if (!head) return void 0;
|
|
16647
16915
|
const ref = head.trim().match(/^ref:\s*(\S+)$/)?.[1];
|
|
16648
16916
|
if (!ref) return /^[0-9a-f]{40}$/.test(head.trim()) ? head.trim() : void 0;
|
|
16649
|
-
const loose = readFileMaybe(
|
|
16917
|
+
const loose = readFileMaybe(join13(cloneDir, ".git", ...ref.split("/")));
|
|
16650
16918
|
if (loose) return loose.trim();
|
|
16651
|
-
const packed = readFileMaybe(
|
|
16919
|
+
const packed = readFileMaybe(join13(cloneDir, ".git", "packed-refs"));
|
|
16652
16920
|
for (const line of packed?.split("\n") ?? []) {
|
|
16653
16921
|
const [sha, name] = line.trim().split(/\s+/);
|
|
16654
16922
|
if (name === ref) return sha;
|
|
@@ -16657,12 +16925,12 @@ function localHeadSha(cloneDir) {
|
|
|
16657
16925
|
}
|
|
16658
16926
|
function readFileMaybe(path) {
|
|
16659
16927
|
try {
|
|
16660
|
-
return
|
|
16928
|
+
return readFileSync11(path, "utf8");
|
|
16661
16929
|
} catch {
|
|
16662
16930
|
return void 0;
|
|
16663
16931
|
}
|
|
16664
16932
|
}
|
|
16665
|
-
var realGit2 = (args, cwd) =>
|
|
16933
|
+
var realGit2 = (args, cwd) => execFileSync6("git", args, {
|
|
16666
16934
|
cwd,
|
|
16667
16935
|
encoding: "utf8",
|
|
16668
16936
|
timeout: 5e3,
|
|
@@ -16671,14 +16939,14 @@ var realGit2 = (args, cwd) => execFileSync4("git", args, {
|
|
|
16671
16939
|
});
|
|
16672
16940
|
function checkCurrency(plugin, opts, configDir) {
|
|
16673
16941
|
const marketplace = plugin.key.split("@")[1];
|
|
16674
|
-
const kmFile = opts.knownMarketplacesFile ?? (configDir ?
|
|
16942
|
+
const kmFile = opts.knownMarketplacesFile ?? (configDir ? join13(configDir, "plugins", "known_marketplaces.json") : void 0);
|
|
16675
16943
|
const entry = kmFile ? readJson2(kmFile)?.[marketplace] : void 0;
|
|
16676
16944
|
const skip = (detail) => ({ status: "skip", detail });
|
|
16677
16945
|
if (!entry) return skip(`no marketplace "${marketplace}" in ${kmFile ?? "the plugin registry"} \u2014 cannot check currency`);
|
|
16678
16946
|
if (entry.source?.source !== "github") {
|
|
16679
16947
|
return skip(`marketplace "${marketplace}" is a ${entry.source?.source ?? "unknown"}-source \u2014 no clone to compare`);
|
|
16680
16948
|
}
|
|
16681
|
-
if (!entry.installLocation || !
|
|
16949
|
+
if (!entry.installLocation || !existsSync9(join13(entry.installLocation, ".git"))) {
|
|
16682
16950
|
return skip(`marketplace clone not found at ${entry.installLocation ?? "(unset installLocation)"}`);
|
|
16683
16951
|
}
|
|
16684
16952
|
const clone2 = entry.installLocation;
|
|
@@ -16709,7 +16977,7 @@ var NPM_REGISTRY_URL = "https://registry.npmjs.org/@viloforge%2Fvfkb/latest";
|
|
|
16709
16977
|
var NPM_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
16710
16978
|
var NPM_CACHE_FILE = "npm-currency-cache.json";
|
|
16711
16979
|
function npmCacheFilePath(opts) {
|
|
16712
|
-
return opts.cacheFile ??
|
|
16980
|
+
return opts.cacheFile ?? join13(opts.brainDir, ".signals", NPM_CACHE_FILE);
|
|
16713
16981
|
}
|
|
16714
16982
|
function readNpmCache(path) {
|
|
16715
16983
|
const raw = readJson2(path);
|
|
@@ -16719,8 +16987,8 @@ function readNpmCache(path) {
|
|
|
16719
16987
|
}
|
|
16720
16988
|
function writeNpmCache(path, entry) {
|
|
16721
16989
|
try {
|
|
16722
|
-
|
|
16723
|
-
|
|
16990
|
+
mkdirSync8(dirname5(path), { recursive: true });
|
|
16991
|
+
writeFileSync6(path, JSON.stringify(entry));
|
|
16724
16992
|
} catch {
|
|
16725
16993
|
}
|
|
16726
16994
|
}
|
|
@@ -16802,9 +17070,9 @@ function runDoctor(opts) {
|
|
|
16802
17070
|
const checks = [];
|
|
16803
17071
|
const add = (name, status, detail) => checks.push({ name, status, detail });
|
|
16804
17072
|
add("engine", "ok", `version ${ENGINE_VERSION} \xB7 commit ${ENGINE_COMMIT} \xB7 schema v${SCHEMA_VERSION}`);
|
|
16805
|
-
const settings = readJson2(
|
|
17073
|
+
const settings = readJson2(join13(root, ".claude", "settings.json"));
|
|
16806
17074
|
const configDir = claudeConfigDir(env);
|
|
16807
|
-
const pluginsFile = opts.pluginsFile ?? (configDir ?
|
|
17075
|
+
const pluginsFile = opts.pluginsFile ?? (configDir ? join13(configDir, "plugins", "installed_plugins.json") : void 0);
|
|
16808
17076
|
const plugin = detectPluginWiring(settings, root, pluginsFile);
|
|
16809
17077
|
const mf = readManifest(brainDir2);
|
|
16810
17078
|
if (!mf) {
|
|
@@ -16825,6 +17093,41 @@ function runDoctor(opts) {
|
|
|
16825
17093
|
add("engine drift", "warn", `brain last stamped by engine ${mf.engine_commit}, running ${ENGINE_COMMIT} \u2014 possible dual-clone drift`);
|
|
16826
17094
|
}
|
|
16827
17095
|
}
|
|
17096
|
+
{
|
|
17097
|
+
const js = journalStatus(brainDir2);
|
|
17098
|
+
if (js.suppressedInEntries > 0) {
|
|
17099
|
+
add(
|
|
17100
|
+
"journal",
|
|
17101
|
+
"warn",
|
|
17102
|
+
`${js.suppressedInEntries} suppressed (purged) pair(s) still present in entries.jsonl \u2014 a redaction is half-done: remove the line(s) from entries.jsonl too (ADR-0064 \xA74)`
|
|
17103
|
+
);
|
|
17104
|
+
} else if (js.restorable > 0) {
|
|
17105
|
+
add(
|
|
17106
|
+
"journal",
|
|
17107
|
+
"warn",
|
|
17108
|
+
`${js.restorable} journaled entr${js.restorable === 1 ? "y" : "ies"} missing from entries.jsonl \u2014 the next session-start restores them (or run \`vfkb hook session-start\` after checking why they vanished; recovery runs ONLY there)`
|
|
17109
|
+
);
|
|
17110
|
+
} else {
|
|
17111
|
+
add("journal", "ok", `${js.walLines} line(s) in the uncommitted window, nothing to restore`);
|
|
17112
|
+
}
|
|
17113
|
+
if (existsSync9(join13(brainDir2, ".journal"))) {
|
|
17114
|
+
try {
|
|
17115
|
+
execFileSync6("git", ["-C", root, "check-ignore", "-q", join13(brainDir2, ".journal", "wal.jsonl")], {
|
|
17116
|
+
stdio: "ignore"
|
|
17117
|
+
});
|
|
17118
|
+
} catch {
|
|
17119
|
+
try {
|
|
17120
|
+
execFileSync6("git", ["-C", root, "rev-parse", "--is-inside-work-tree"], { stdio: "ignore" });
|
|
17121
|
+
add(
|
|
17122
|
+
"journal gitignore",
|
|
17123
|
+
"warn",
|
|
17124
|
+
`.vfkb/.journal/ exists but is NOT gitignored \u2014 add '.vfkb/.journal/' to .gitignore before the next brain commit (a committed journal defeats its purpose and the ADR-0064 \xA74 redaction)`
|
|
17125
|
+
);
|
|
17126
|
+
} catch {
|
|
17127
|
+
}
|
|
17128
|
+
}
|
|
17129
|
+
}
|
|
17130
|
+
}
|
|
16828
17131
|
const home = env.VFKB_BUNDLE_DIR || env.VFKB_HOME;
|
|
16829
17132
|
if (!home) {
|
|
16830
17133
|
if (plugin) {
|
|
@@ -16832,7 +17135,7 @@ function runDoctor(opts) {
|
|
|
16832
17135
|
} else {
|
|
16833
17136
|
add("$VFKB_BUNDLE_DIR", "warn", "unset \u2014 set it once per machine to the vfkb bundles dir (so the wiring resolves the engine)");
|
|
16834
17137
|
}
|
|
16835
|
-
} else if (!
|
|
17138
|
+
} else if (!existsSync9(join13(home, "vfkb.mjs")) || !existsSync9(join13(home, "vfkb-mcp.mjs"))) {
|
|
16836
17139
|
add("$VFKB_BUNDLE_DIR", "warn", `set to ${home} but vfkb.mjs / vfkb-mcp.mjs not found there (run \`npm run build:bundles\`)`);
|
|
16837
17140
|
} else {
|
|
16838
17141
|
add("$VFKB_BUNDLE_DIR", "ok", home);
|
|
@@ -16843,7 +17146,7 @@ function runDoctor(opts) {
|
|
|
16843
17146
|
if (env.VFKB_HOME && !env.VFKB_BUNDLE_DIR) {
|
|
16844
17147
|
add("env (deprecated)", "warn", "VFKB_HOME is a deprecated alias \u2014 rename it to VFKB_BUNDLE_DIR");
|
|
16845
17148
|
}
|
|
16846
|
-
const mcp = readJson2(
|
|
17149
|
+
const mcp = readJson2(join13(root, ".mcp.json"));
|
|
16847
17150
|
const mcpProject = mcp?.mcpServers?.vfkb?.env?.VFKB_PROJECT;
|
|
16848
17151
|
const mcpPresent = Boolean(mcp?.mcpServers?.vfkb);
|
|
16849
17152
|
if (plugin && mcpPresent) {
|
|
@@ -16873,7 +17176,7 @@ function runDoctor(opts) {
|
|
|
16873
17176
|
if (!plugin && have.length > 0 && hooksBlob.includes("bootstrap.mjs") && !hooksBlob.includes("CLAUDE_PROJECT_DIR")) {
|
|
16874
17177
|
add("hooks anchor", "warn", "vfkb hooks use a CWD-relative bootstrap path \u2014 they break when the session cd's out of the repo root; re-run `vfkb init` to anchor them to $CLAUDE_PROJECT_DIR (issue #22)");
|
|
16875
17178
|
}
|
|
16876
|
-
if (
|
|
17179
|
+
if (existsSync9(join13(root, ".vfkb", "bin", "bootstrap.mjs"))) {
|
|
16877
17180
|
add("bootstrap", "ok", ".vfkb/bin/bootstrap.mjs present");
|
|
16878
17181
|
} else if (!plugin && (mcpPresent || have.length > 0)) {
|
|
16879
17182
|
add("bootstrap", "warn", "wiring present but .vfkb/bin/bootstrap.mjs is missing \u2014 run `vfkb init`");
|
|
@@ -16913,9 +17216,9 @@ function renderDoctor(report) {
|
|
|
16913
17216
|
}
|
|
16914
17217
|
|
|
16915
17218
|
// src/import.ts
|
|
16916
|
-
import { existsSync as
|
|
17219
|
+
import { existsSync as existsSync10, readdirSync as readdirSync3, readFileSync as readFileSync12, statSync as statSync3 } from "node:fs";
|
|
16917
17220
|
import { homedir as homedir2 } from "node:os";
|
|
16918
|
-
import { basename as
|
|
17221
|
+
import { basename as basename4, extname, join as join14 } from "node:path";
|
|
16919
17222
|
var MYKB_FILES = {
|
|
16920
17223
|
"decisions.jsonl": "decision",
|
|
16921
17224
|
"facts.jsonl": "fact",
|
|
@@ -16940,16 +17243,16 @@ function mykbText(type, e) {
|
|
|
16940
17243
|
return parts.filter(Boolean).join("\n\n");
|
|
16941
17244
|
}
|
|
16942
17245
|
function resolveMykbArea(nameOrDir) {
|
|
16943
|
-
if (
|
|
16944
|
-
return
|
|
17246
|
+
if (existsSync10(nameOrDir) && statSync3(nameOrDir).isDirectory()) return nameOrDir;
|
|
17247
|
+
return join14(homedir2(), ".mykb", "areas", nameOrDir);
|
|
16945
17248
|
}
|
|
16946
17249
|
function fromMykb(areaDir) {
|
|
16947
|
-
if (!
|
|
17250
|
+
if (!existsSync10(areaDir)) throw new Error(`mykb area not found: ${areaDir}`);
|
|
16948
17251
|
const out = [];
|
|
16949
17252
|
for (const [file2, type] of Object.entries(MYKB_FILES)) {
|
|
16950
|
-
const path =
|
|
16951
|
-
if (!
|
|
16952
|
-
for (const line of
|
|
17253
|
+
const path = join14(areaDir, file2);
|
|
17254
|
+
if (!existsSync10(path)) continue;
|
|
17255
|
+
for (const line of readFileSync12(path, "utf8").split(/\r?\n/)) {
|
|
16953
17256
|
const trimmed = line.trim();
|
|
16954
17257
|
if (!trimmed) continue;
|
|
16955
17258
|
let e;
|
|
@@ -16968,24 +17271,24 @@ function fromMykb(areaDir) {
|
|
|
16968
17271
|
}
|
|
16969
17272
|
function mdTitle(path) {
|
|
16970
17273
|
try {
|
|
16971
|
-
const heading =
|
|
17274
|
+
const heading = readFileSync12(path, "utf8").split(/\r?\n/).find((l) => l.startsWith("# "));
|
|
16972
17275
|
if (heading) return heading.replace(/^#\s+/, "").trim();
|
|
16973
17276
|
} catch {
|
|
16974
17277
|
}
|
|
16975
|
-
return
|
|
17278
|
+
return basename4(path, extname(path));
|
|
16976
17279
|
}
|
|
16977
17280
|
function fromAdr(dir = "docs/adr") {
|
|
16978
|
-
if (!
|
|
17281
|
+
if (!existsSync10(dir)) throw new Error(`ADR dir not found: ${dir}`);
|
|
16979
17282
|
const out = [];
|
|
16980
17283
|
for (const file2 of readdirSync3(dir).sort()) {
|
|
16981
17284
|
if (extname(file2) !== ".md" || /readme/i.test(file2)) continue;
|
|
16982
|
-
const rel =
|
|
17285
|
+
const rel = join14(dir, file2);
|
|
16983
17286
|
out.push(stamp("link", `${mdTitle(rel)} \u2192 ${rel}`, ["adr"], false));
|
|
16984
17287
|
}
|
|
16985
17288
|
return out;
|
|
16986
17289
|
}
|
|
16987
17290
|
function fromMarkdown(file2) {
|
|
16988
|
-
if (!
|
|
17291
|
+
if (!existsSync10(file2)) throw new Error(`markdown file not found: ${file2}`);
|
|
16989
17292
|
return [stamp("link", `${mdTitle(file2)} \u2192 ${file2}`, ["doc"], false)];
|
|
16990
17293
|
}
|
|
16991
17294
|
|
|
@@ -17064,26 +17367,26 @@ var ENTRY_TYPES = ["fact", "decision", "gotcha", "pattern", "link"];
|
|
|
17064
17367
|
var DECISION_STATUSES = ["proposed", "accepted", "deprecated", "superseded"];
|
|
17065
17368
|
|
|
17066
17369
|
// src/cli.ts
|
|
17067
|
-
import { readFileSync as
|
|
17370
|
+
import { readFileSync as readFileSync13 } from "node:fs";
|
|
17068
17371
|
import { fileURLToPath } from "node:url";
|
|
17069
|
-
import { dirname as
|
|
17372
|
+
import { dirname as dirname6, join as join15 } from "node:path";
|
|
17070
17373
|
function packageVersion() {
|
|
17071
17374
|
try {
|
|
17072
|
-
const here =
|
|
17073
|
-
const pkg = JSON.parse(
|
|
17375
|
+
const here = dirname6(fileURLToPath(import.meta.url));
|
|
17376
|
+
const pkg = JSON.parse(readFileSync13(join15(here, "..", "package.json"), "utf8"));
|
|
17074
17377
|
return pkg.version || ENGINE_VERSION;
|
|
17075
17378
|
} catch {
|
|
17076
17379
|
return ENGINE_VERSION;
|
|
17077
17380
|
}
|
|
17078
17381
|
}
|
|
17079
17382
|
function readStdin() {
|
|
17080
|
-
return new Promise((
|
|
17383
|
+
return new Promise((resolve4) => {
|
|
17081
17384
|
let data = "";
|
|
17082
|
-
if (process.stdin.isTTY) return
|
|
17385
|
+
if (process.stdin.isTTY) return resolve4("");
|
|
17083
17386
|
process.stdin.setEncoding("utf8");
|
|
17084
17387
|
process.stdin.on("data", (c) => data += c);
|
|
17085
|
-
process.stdin.on("end", () =>
|
|
17086
|
-
setTimeout(() =>
|
|
17388
|
+
process.stdin.on("end", () => resolve4(data));
|
|
17389
|
+
setTimeout(() => resolve4(data), 2e3).unref?.();
|
|
17087
17390
|
});
|
|
17088
17391
|
}
|
|
17089
17392
|
function flag(args, name) {
|
|
@@ -17111,7 +17414,7 @@ ${USAGE}`);
|
|
|
17111
17414
|
throw err;
|
|
17112
17415
|
}
|
|
17113
17416
|
}
|
|
17114
|
-
var USAGE = "usage: vfkb <add|list|search|query|map|context|context init|resume|resume-note|curate|distill|save|export|import|init|doctor|supersede|context-block|context-block-naive|--version|hook session-start|hook pre-tool-use|hook post-tool-use|hook stop|hook session-end>\n";
|
|
17417
|
+
var USAGE = "usage: vfkb <add|broadcast|journal purge|list|search|query|map|context|context init|resume|resume-note|curate|distill|save|export|import|init|doctor|supersede|context-block|context-block-naive|--version|hook session-start|hook pre-tool-use|hook post-tool-use|hook stop|hook session-end>\n";
|
|
17115
17418
|
async function dispatch() {
|
|
17116
17419
|
const [cmd, sub, ...rest] = process.argv.slice(2);
|
|
17117
17420
|
if (cmd === "--help" || cmd === "-h" || cmd === "help") {
|
|
@@ -17160,6 +17463,43 @@ async function dispatch() {
|
|
|
17160
17463
|
}
|
|
17161
17464
|
return;
|
|
17162
17465
|
}
|
|
17466
|
+
if (cmd === "broadcast") {
|
|
17467
|
+
const p = parseArgs("broadcast", argsOf(sub, rest), {
|
|
17468
|
+
to: "value",
|
|
17469
|
+
op: "value",
|
|
17470
|
+
tag: "value"
|
|
17471
|
+
});
|
|
17472
|
+
const text = p.positionals.join(" ").trim();
|
|
17473
|
+
const targets = flagList(p, "to") ?? [];
|
|
17474
|
+
if (!text || targets.length === 0) {
|
|
17475
|
+
throw new UsageError('usage: vfkb broadcast "<text>" --to <dir>[,<dir>\u2026] [--op <name>] [--tag a,b]');
|
|
17476
|
+
}
|
|
17477
|
+
try {
|
|
17478
|
+
const results = broadcast(text, targets, {
|
|
17479
|
+
op: flagValue(p, "op"),
|
|
17480
|
+
tags: flagList(p, "tag")
|
|
17481
|
+
});
|
|
17482
|
+
let failed = 0;
|
|
17483
|
+
for (const r of results) {
|
|
17484
|
+
if (r.ok) {
|
|
17485
|
+
process.stdout.write(`written ${r.target} ${r.id} ${r.posture}${r.healed ? " (manifest healed \u2014 brain was wired but manifest-less, vfkb#193)" : ""}
|
|
17486
|
+
`);
|
|
17487
|
+
} else {
|
|
17488
|
+
failed++;
|
|
17489
|
+
process.stdout.write(`REFUSED ${r.target} ${r.reason}
|
|
17490
|
+
`);
|
|
17491
|
+
}
|
|
17492
|
+
}
|
|
17493
|
+
process.stdout.write(`
|
|
17494
|
+
broadcast: ${results.length - failed}/${results.length} written${failed ? ` \u2014 ${failed} refused (partial broadcast, visible by design)` : ""}
|
|
17495
|
+
`);
|
|
17496
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
17497
|
+
} catch (err) {
|
|
17498
|
+
process.stderr.write(`error: ${err.message}
|
|
17499
|
+
`);
|
|
17500
|
+
process.exit(1);
|
|
17501
|
+
}
|
|
17502
|
+
}
|
|
17163
17503
|
if (cmd === "export") {
|
|
17164
17504
|
const p = parseArgs("export", rest, { out: "value" });
|
|
17165
17505
|
if (p.positionals.length > 0) {
|
|
@@ -17465,7 +17805,18 @@ imported ${results.length} entr${results.length === 1 ? "y" : "ies"} (role=impor
|
|
|
17465
17805
|
const project = defaultProject();
|
|
17466
17806
|
const lim = flag(rest, "limit");
|
|
17467
17807
|
const session = SessionState.load(effectiveSessionId(payloadId));
|
|
17468
|
-
|
|
17808
|
+
let restoreNote = "";
|
|
17809
|
+
try {
|
|
17810
|
+
const rec = withExclusive(() => recoverFromJournal(brainDir()));
|
|
17811
|
+
if (rec.restored > 0) {
|
|
17812
|
+
writeMeta();
|
|
17813
|
+
restoreNote = `\u26A0 vfkb restored ${rec.restored} journaled entr${rec.restored === 1 ? "y" : "ies"} lost from entries.jsonl \u2014 likely a destructive git operation on uncommitted brain state (ADR-0064). Verify with kb_list and commit the brain on your next topic branch.
|
|
17814
|
+
|
|
17815
|
+
`;
|
|
17816
|
+
}
|
|
17817
|
+
} catch {
|
|
17818
|
+
}
|
|
17819
|
+
const additionalContext = restoreNote + (rest.includes("--naive") ? renderNaiveDump(project, void 0, lim ? Number(lim) : void 0) : renderResume(project, session));
|
|
17469
17820
|
session.save();
|
|
17470
17821
|
process.stdout.write(
|
|
17471
17822
|
JSON.stringify({
|
|
@@ -17582,6 +17933,23 @@ imported ${results.length} entr${results.length === 1 ? "y" : "ies"} (role=impor
|
|
|
17582
17933
|
process.stdout.write((r.committed ? "committed: " : "no-op: ") + r.message + "\n");
|
|
17583
17934
|
return;
|
|
17584
17935
|
}
|
|
17936
|
+
if (cmd === "journal") {
|
|
17937
|
+
if (sub !== "purge") {
|
|
17938
|
+
throw new UsageError("usage: vfkb journal purge (--id <id> | --all)");
|
|
17939
|
+
}
|
|
17940
|
+
const p = parseArgs("journal purge", rest, { id: "value", all: "boolean" });
|
|
17941
|
+
const id = flagValue(p, "id");
|
|
17942
|
+
const all = p.flags.has("all");
|
|
17943
|
+
if (!!id === all) {
|
|
17944
|
+
throw new UsageError("usage: vfkb journal purge (--id <id> | --all)");
|
|
17945
|
+
}
|
|
17946
|
+
const r = withExclusive(() => purgeJournal(brainDir(), { id, all }));
|
|
17947
|
+
process.stdout.write(
|
|
17948
|
+
r.purged > 0 ? `purged ${r.purged} journal line(s); pair(s) suppressed \u2014 recovery will never restore them (ADR-0064 \xA74). Remember: a redaction of entries.jsonl is complete only with this purge.
|
|
17949
|
+
` : "no matching journal lines\n"
|
|
17950
|
+
);
|
|
17951
|
+
return;
|
|
17952
|
+
}
|
|
17585
17953
|
process.stderr.write(USAGE);
|
|
17586
17954
|
process.exit(1);
|
|
17587
17955
|
}
|