@viloforge/vfkb 0.2.3 → 0.4.1
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 +595 -183
- package/dist/cli.js +86 -5
- package/dist/doctor.js +138 -10
- 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.1" : ownPackageVersion();
|
|
16081
|
+
var ENGINE_COMMIT = true ? "66ee821" : "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, realpathSync } from "node:fs";
|
|
16873
|
+
import { join as join13, dirname as dirname5, relative as relative3, resolve as resolve4, isAbsolute as isAbsolute2 } 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
|
}
|
|
@@ -16797,21 +17065,64 @@ async function checkNpmCurrency(opts) {
|
|
|
16797
17065
|
clearTimeout(timer);
|
|
16798
17066
|
}
|
|
16799
17067
|
}
|
|
17068
|
+
var HOOK_SUBCOMMANDS = ["session-start", "pre-tool-use", "post-tool-use", "stop", "session-end"];
|
|
17069
|
+
function isEngineWiring(hookJson) {
|
|
17070
|
+
const text = hookJson.replace(/\\[tnr]/g, " ").replace(/\\(.)/g, "$1");
|
|
17071
|
+
if (/bootstrap\.mjs/.test(text)) return true;
|
|
17072
|
+
return new RegExp(`\\bhook["'\\s]+(${HOOK_SUBCOMMANDS.join("|")})\\b`).test(text);
|
|
17073
|
+
}
|
|
17074
|
+
function engineDrift(manifestCommit, runningCommit) {
|
|
17075
|
+
if (!manifestCommit || manifestCommit === "dev") return false;
|
|
17076
|
+
if (!runningCommit || runningCommit === "dev") return false;
|
|
17077
|
+
return manifestCommit !== runningCommit;
|
|
17078
|
+
}
|
|
17079
|
+
function repoToplevel(root) {
|
|
17080
|
+
try {
|
|
17081
|
+
return execFileSync6("git", ["-C", root, "rev-parse", "--show-toplevel"], {
|
|
17082
|
+
encoding: "utf8",
|
|
17083
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
17084
|
+
}).trim();
|
|
17085
|
+
} catch {
|
|
17086
|
+
return void 0;
|
|
17087
|
+
}
|
|
17088
|
+
}
|
|
17089
|
+
function isUnder(parent, child) {
|
|
17090
|
+
if (!parent) return false;
|
|
17091
|
+
const real = (p) => {
|
|
17092
|
+
try {
|
|
17093
|
+
return realpathSync(p);
|
|
17094
|
+
} catch {
|
|
17095
|
+
return resolve4(p);
|
|
17096
|
+
}
|
|
17097
|
+
};
|
|
17098
|
+
const rel = relative3(real(parent), real(child));
|
|
17099
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
|
|
17100
|
+
}
|
|
16800
17101
|
function runDoctor(opts) {
|
|
16801
17102
|
const { root, brainDir: brainDir2, env } = opts;
|
|
17103
|
+
const runningCommit = opts.engineCommit ?? ENGINE_COMMIT;
|
|
16802
17104
|
const checks = [];
|
|
16803
17105
|
const add = (name, status, detail) => checks.push({ name, status, detail });
|
|
16804
|
-
add("engine", "ok", `version ${ENGINE_VERSION} \xB7 commit ${
|
|
16805
|
-
const settings = readJson2(
|
|
17106
|
+
add("engine", "ok", `version ${ENGINE_VERSION} \xB7 commit ${runningCommit} \xB7 schema v${SCHEMA_VERSION}`);
|
|
17107
|
+
const settings = readJson2(join13(root, ".claude", "settings.json"));
|
|
16806
17108
|
const configDir = claudeConfigDir(env);
|
|
16807
|
-
const pluginsFile = opts.pluginsFile ?? (configDir ?
|
|
17109
|
+
const pluginsFile = opts.pluginsFile ?? (configDir ? join13(configDir, "plugins", "installed_plugins.json") : void 0);
|
|
16808
17110
|
const plugin = detectPluginWiring(settings, root, pluginsFile);
|
|
16809
17111
|
const mf = readManifest(brainDir2);
|
|
16810
17112
|
if (!mf) {
|
|
16811
17113
|
add(
|
|
16812
17114
|
"brain manifest",
|
|
16813
17115
|
"warn",
|
|
16814
|
-
|
|
17116
|
+
// #188 — the ordinary write path (addEntry → appendRecord → backend.append)
|
|
17117
|
+
// never touches manifest.json. `writeManifest` has exactly two callers:
|
|
17118
|
+
// `vfkb init` (init.ts) and the broadcast heal, which fires only when the
|
|
17119
|
+
// manifest is ABSENT (broadcast.ts). Saying "on the next write" claimed
|
|
17120
|
+
// behaviour the code does not have.
|
|
17121
|
+
// On a PLUGIN-wired repo the message must not prescribe `vfkb init` — that
|
|
17122
|
+
// advice is what scaffolds double wiring (issue #77), and the invariant is
|
|
17123
|
+
// asserted in doctor.test.ts. A plugin-born brain simply has no manifest
|
|
17124
|
+
// until a cross-repo broadcast heals it (vfkb#193).
|
|
17125
|
+
plugin ? `no manifest.json in ${brainDir2} \u2014 plugin-born brains have none until a cross-repo broadcast heals it (vfkb#193); the ordinary write path never creates one` : `no manifest.json in ${brainDir2} \u2014 run \`vfkb init\` to stamp it (the ordinary write path never creates one)`
|
|
16815
17126
|
);
|
|
16816
17127
|
} else if (typeof mf.schema_version !== "number") {
|
|
16817
17128
|
add("brain manifest", "warn", "manifest has no numeric schema_version");
|
|
@@ -16820,9 +17131,45 @@ function runDoctor(opts) {
|
|
|
16820
17131
|
} else if (mf.schema_version < SCHEMA_VERSION) {
|
|
16821
17132
|
add("brain\u2194engine compat", "warn", `brain schema v${mf.schema_version} is older than engine v${SCHEMA_VERSION} \u2014 migration may be needed`);
|
|
16822
17133
|
} else {
|
|
16823
|
-
|
|
16824
|
-
|
|
16825
|
-
|
|
17134
|
+
const sentinel = mf.engine_commit === "dev" ? ` \xB7 stamped by a build that did not know its own commit ("dev") \u2014 provenance unknown (#212)` : "";
|
|
17135
|
+
add("brain\u2194engine compat", "ok", `schema v${mf.schema_version} matches${sentinel}`);
|
|
17136
|
+
if (engineDrift(mf.engine_commit, runningCommit)) {
|
|
17137
|
+
add("engine drift", "warn", `brain last stamped by engine ${mf.engine_commit}, running ${runningCommit} \u2014 possible dual-clone drift`);
|
|
17138
|
+
}
|
|
17139
|
+
}
|
|
17140
|
+
{
|
|
17141
|
+
const js = journalStatus(brainDir2);
|
|
17142
|
+
if (js.suppressedInEntries > 0) {
|
|
17143
|
+
add(
|
|
17144
|
+
"journal",
|
|
17145
|
+
"warn",
|
|
17146
|
+
`${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)`
|
|
17147
|
+
);
|
|
17148
|
+
} else if (js.restorable > 0) {
|
|
17149
|
+
add(
|
|
17150
|
+
"journal",
|
|
17151
|
+
"warn",
|
|
17152
|
+
`${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)`
|
|
17153
|
+
);
|
|
17154
|
+
} else {
|
|
17155
|
+
add("journal", "ok", `${js.walLines} line(s) in the uncommitted window, nothing to restore`);
|
|
17156
|
+
}
|
|
17157
|
+
if (existsSync9(join13(brainDir2, ".journal")) && isUnder(repoToplevel(root), join13(brainDir2, ".journal"))) {
|
|
17158
|
+
try {
|
|
17159
|
+
execFileSync6("git", ["-C", root, "check-ignore", "-q", join13(brainDir2, ".journal", "wal.jsonl")], {
|
|
17160
|
+
stdio: "ignore"
|
|
17161
|
+
});
|
|
17162
|
+
} catch {
|
|
17163
|
+
try {
|
|
17164
|
+
execFileSync6("git", ["-C", root, "rev-parse", "--is-inside-work-tree"], { stdio: "ignore" });
|
|
17165
|
+
add(
|
|
17166
|
+
"journal gitignore",
|
|
17167
|
+
"warn",
|
|
17168
|
+
`.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)`
|
|
17169
|
+
);
|
|
17170
|
+
} catch {
|
|
17171
|
+
}
|
|
17172
|
+
}
|
|
16826
17173
|
}
|
|
16827
17174
|
}
|
|
16828
17175
|
const home = env.VFKB_BUNDLE_DIR || env.VFKB_HOME;
|
|
@@ -16832,7 +17179,7 @@ function runDoctor(opts) {
|
|
|
16832
17179
|
} else {
|
|
16833
17180
|
add("$VFKB_BUNDLE_DIR", "warn", "unset \u2014 set it once per machine to the vfkb bundles dir (so the wiring resolves the engine)");
|
|
16834
17181
|
}
|
|
16835
|
-
} else if (!
|
|
17182
|
+
} else if (!existsSync9(join13(home, "vfkb.mjs")) || !existsSync9(join13(home, "vfkb-mcp.mjs"))) {
|
|
16836
17183
|
add("$VFKB_BUNDLE_DIR", "warn", `set to ${home} but vfkb.mjs / vfkb-mcp.mjs not found there (run \`npm run build:bundles\`)`);
|
|
16837
17184
|
} else {
|
|
16838
17185
|
add("$VFKB_BUNDLE_DIR", "ok", home);
|
|
@@ -16843,7 +17190,7 @@ function runDoctor(opts) {
|
|
|
16843
17190
|
if (env.VFKB_HOME && !env.VFKB_BUNDLE_DIR) {
|
|
16844
17191
|
add("env (deprecated)", "warn", "VFKB_HOME is a deprecated alias \u2014 rename it to VFKB_BUNDLE_DIR");
|
|
16845
17192
|
}
|
|
16846
|
-
const mcp = readJson2(
|
|
17193
|
+
const mcp = readJson2(join13(root, ".mcp.json"));
|
|
16847
17194
|
const mcpProject = mcp?.mcpServers?.vfkb?.env?.VFKB_PROJECT;
|
|
16848
17195
|
const mcpPresent = Boolean(mcp?.mcpServers?.vfkb);
|
|
16849
17196
|
if (plugin && mcpPresent) {
|
|
@@ -16857,7 +17204,7 @@ function runDoctor(opts) {
|
|
|
16857
17204
|
}
|
|
16858
17205
|
const hooks = settings?.hooks ?? {};
|
|
16859
17206
|
const expected = ["SessionStart", "PreToolUse", "Stop", "SessionEnd"];
|
|
16860
|
-
const have = expected.filter((e) => JSON.stringify(hooks[e] ?? "")
|
|
17207
|
+
const have = expected.filter((e) => isEngineWiring(JSON.stringify(hooks[e] ?? "")));
|
|
16861
17208
|
if (plugin && have.length > 0) {
|
|
16862
17209
|
add(".claude/settings.json", "warn", `vfkb hooks present ALONGSIDE the plugin (double wiring) \u2014 remove them; the plugin's hooks are primary (ADR-0045)`);
|
|
16863
17210
|
} else if (plugin) {
|
|
@@ -16873,7 +17220,7 @@ function runDoctor(opts) {
|
|
|
16873
17220
|
if (!plugin && have.length > 0 && hooksBlob.includes("bootstrap.mjs") && !hooksBlob.includes("CLAUDE_PROJECT_DIR")) {
|
|
16874
17221
|
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
17222
|
}
|
|
16876
|
-
if (
|
|
17223
|
+
if (existsSync9(join13(root, ".vfkb", "bin", "bootstrap.mjs"))) {
|
|
16877
17224
|
add("bootstrap", "ok", ".vfkb/bin/bootstrap.mjs present");
|
|
16878
17225
|
} else if (!plugin && (mcpPresent || have.length > 0)) {
|
|
16879
17226
|
add("bootstrap", "warn", "wiring present but .vfkb/bin/bootstrap.mjs is missing \u2014 run `vfkb init`");
|
|
@@ -16913,9 +17260,9 @@ function renderDoctor(report) {
|
|
|
16913
17260
|
}
|
|
16914
17261
|
|
|
16915
17262
|
// src/import.ts
|
|
16916
|
-
import { existsSync as
|
|
17263
|
+
import { existsSync as existsSync10, readdirSync as readdirSync3, readFileSync as readFileSync12, statSync as statSync3 } from "node:fs";
|
|
16917
17264
|
import { homedir as homedir2 } from "node:os";
|
|
16918
|
-
import { basename as
|
|
17265
|
+
import { basename as basename4, extname, join as join14 } from "node:path";
|
|
16919
17266
|
var MYKB_FILES = {
|
|
16920
17267
|
"decisions.jsonl": "decision",
|
|
16921
17268
|
"facts.jsonl": "fact",
|
|
@@ -16940,16 +17287,16 @@ function mykbText(type, e) {
|
|
|
16940
17287
|
return parts.filter(Boolean).join("\n\n");
|
|
16941
17288
|
}
|
|
16942
17289
|
function resolveMykbArea(nameOrDir) {
|
|
16943
|
-
if (
|
|
16944
|
-
return
|
|
17290
|
+
if (existsSync10(nameOrDir) && statSync3(nameOrDir).isDirectory()) return nameOrDir;
|
|
17291
|
+
return join14(homedir2(), ".mykb", "areas", nameOrDir);
|
|
16945
17292
|
}
|
|
16946
17293
|
function fromMykb(areaDir) {
|
|
16947
|
-
if (!
|
|
17294
|
+
if (!existsSync10(areaDir)) throw new Error(`mykb area not found: ${areaDir}`);
|
|
16948
17295
|
const out = [];
|
|
16949
17296
|
for (const [file2, type] of Object.entries(MYKB_FILES)) {
|
|
16950
|
-
const path =
|
|
16951
|
-
if (!
|
|
16952
|
-
for (const line of
|
|
17297
|
+
const path = join14(areaDir, file2);
|
|
17298
|
+
if (!existsSync10(path)) continue;
|
|
17299
|
+
for (const line of readFileSync12(path, "utf8").split(/\r?\n/)) {
|
|
16953
17300
|
const trimmed = line.trim();
|
|
16954
17301
|
if (!trimmed) continue;
|
|
16955
17302
|
let e;
|
|
@@ -16968,24 +17315,24 @@ function fromMykb(areaDir) {
|
|
|
16968
17315
|
}
|
|
16969
17316
|
function mdTitle(path) {
|
|
16970
17317
|
try {
|
|
16971
|
-
const heading =
|
|
17318
|
+
const heading = readFileSync12(path, "utf8").split(/\r?\n/).find((l) => l.startsWith("# "));
|
|
16972
17319
|
if (heading) return heading.replace(/^#\s+/, "").trim();
|
|
16973
17320
|
} catch {
|
|
16974
17321
|
}
|
|
16975
|
-
return
|
|
17322
|
+
return basename4(path, extname(path));
|
|
16976
17323
|
}
|
|
16977
17324
|
function fromAdr(dir = "docs/adr") {
|
|
16978
|
-
if (!
|
|
17325
|
+
if (!existsSync10(dir)) throw new Error(`ADR dir not found: ${dir}`);
|
|
16979
17326
|
const out = [];
|
|
16980
17327
|
for (const file2 of readdirSync3(dir).sort()) {
|
|
16981
17328
|
if (extname(file2) !== ".md" || /readme/i.test(file2)) continue;
|
|
16982
|
-
const rel =
|
|
17329
|
+
const rel = join14(dir, file2);
|
|
16983
17330
|
out.push(stamp("link", `${mdTitle(rel)} \u2192 ${rel}`, ["adr"], false));
|
|
16984
17331
|
}
|
|
16985
17332
|
return out;
|
|
16986
17333
|
}
|
|
16987
17334
|
function fromMarkdown(file2) {
|
|
16988
|
-
if (!
|
|
17335
|
+
if (!existsSync10(file2)) throw new Error(`markdown file not found: ${file2}`);
|
|
16989
17336
|
return [stamp("link", `${mdTitle(file2)} \u2192 ${file2}`, ["doc"], false)];
|
|
16990
17337
|
}
|
|
16991
17338
|
|
|
@@ -17064,26 +17411,26 @@ var ENTRY_TYPES = ["fact", "decision", "gotcha", "pattern", "link"];
|
|
|
17064
17411
|
var DECISION_STATUSES = ["proposed", "accepted", "deprecated", "superseded"];
|
|
17065
17412
|
|
|
17066
17413
|
// src/cli.ts
|
|
17067
|
-
import { readFileSync as
|
|
17414
|
+
import { readFileSync as readFileSync13 } from "node:fs";
|
|
17068
17415
|
import { fileURLToPath } from "node:url";
|
|
17069
|
-
import { dirname as
|
|
17416
|
+
import { dirname as dirname6, join as join15 } from "node:path";
|
|
17070
17417
|
function packageVersion() {
|
|
17071
17418
|
try {
|
|
17072
|
-
const here =
|
|
17073
|
-
const pkg = JSON.parse(
|
|
17419
|
+
const here = dirname6(fileURLToPath(import.meta.url));
|
|
17420
|
+
const pkg = JSON.parse(readFileSync13(join15(here, "..", "package.json"), "utf8"));
|
|
17074
17421
|
return pkg.version || ENGINE_VERSION;
|
|
17075
17422
|
} catch {
|
|
17076
17423
|
return ENGINE_VERSION;
|
|
17077
17424
|
}
|
|
17078
17425
|
}
|
|
17079
17426
|
function readStdin() {
|
|
17080
|
-
return new Promise((
|
|
17427
|
+
return new Promise((resolve5) => {
|
|
17081
17428
|
let data = "";
|
|
17082
|
-
if (process.stdin.isTTY) return
|
|
17429
|
+
if (process.stdin.isTTY) return resolve5("");
|
|
17083
17430
|
process.stdin.setEncoding("utf8");
|
|
17084
17431
|
process.stdin.on("data", (c) => data += c);
|
|
17085
|
-
process.stdin.on("end", () =>
|
|
17086
|
-
setTimeout(() =>
|
|
17432
|
+
process.stdin.on("end", () => resolve5(data));
|
|
17433
|
+
setTimeout(() => resolve5(data), 2e3).unref?.();
|
|
17087
17434
|
});
|
|
17088
17435
|
}
|
|
17089
17436
|
function flag(args, name) {
|
|
@@ -17111,7 +17458,7 @@ ${USAGE}`);
|
|
|
17111
17458
|
throw err;
|
|
17112
17459
|
}
|
|
17113
17460
|
}
|
|
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";
|
|
17461
|
+
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
17462
|
async function dispatch() {
|
|
17116
17463
|
const [cmd, sub, ...rest] = process.argv.slice(2);
|
|
17117
17464
|
if (cmd === "--help" || cmd === "-h" || cmd === "help") {
|
|
@@ -17160,6 +17507,43 @@ async function dispatch() {
|
|
|
17160
17507
|
}
|
|
17161
17508
|
return;
|
|
17162
17509
|
}
|
|
17510
|
+
if (cmd === "broadcast") {
|
|
17511
|
+
const p = parseArgs("broadcast", argsOf(sub, rest), {
|
|
17512
|
+
to: "value",
|
|
17513
|
+
op: "value",
|
|
17514
|
+
tag: "value"
|
|
17515
|
+
});
|
|
17516
|
+
const text = p.positionals.join(" ").trim();
|
|
17517
|
+
const targets = flagList(p, "to") ?? [];
|
|
17518
|
+
if (!text || targets.length === 0) {
|
|
17519
|
+
throw new UsageError('usage: vfkb broadcast "<text>" --to <dir>[,<dir>\u2026] [--op <name>] [--tag a,b]');
|
|
17520
|
+
}
|
|
17521
|
+
try {
|
|
17522
|
+
const results = broadcast(text, targets, {
|
|
17523
|
+
op: flagValue(p, "op"),
|
|
17524
|
+
tags: flagList(p, "tag")
|
|
17525
|
+
});
|
|
17526
|
+
let failed = 0;
|
|
17527
|
+
for (const r of results) {
|
|
17528
|
+
if (r.ok) {
|
|
17529
|
+
process.stdout.write(`written ${r.target} ${r.id} ${r.posture}${r.healed ? " (manifest healed \u2014 brain was wired but manifest-less, vfkb#193)" : ""}
|
|
17530
|
+
`);
|
|
17531
|
+
} else {
|
|
17532
|
+
failed++;
|
|
17533
|
+
process.stdout.write(`REFUSED ${r.target} ${r.reason}
|
|
17534
|
+
`);
|
|
17535
|
+
}
|
|
17536
|
+
}
|
|
17537
|
+
process.stdout.write(`
|
|
17538
|
+
broadcast: ${results.length - failed}/${results.length} written${failed ? ` \u2014 ${failed} refused (partial broadcast, visible by design)` : ""}
|
|
17539
|
+
`);
|
|
17540
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
17541
|
+
} catch (err) {
|
|
17542
|
+
process.stderr.write(`error: ${err.message}
|
|
17543
|
+
`);
|
|
17544
|
+
process.exit(1);
|
|
17545
|
+
}
|
|
17546
|
+
}
|
|
17163
17547
|
if (cmd === "export") {
|
|
17164
17548
|
const p = parseArgs("export", rest, { out: "value" });
|
|
17165
17549
|
if (p.positionals.length > 0) {
|
|
@@ -17465,7 +17849,18 @@ imported ${results.length} entr${results.length === 1 ? "y" : "ies"} (role=impor
|
|
|
17465
17849
|
const project = defaultProject();
|
|
17466
17850
|
const lim = flag(rest, "limit");
|
|
17467
17851
|
const session = SessionState.load(effectiveSessionId(payloadId));
|
|
17468
|
-
|
|
17852
|
+
let restoreNote = "";
|
|
17853
|
+
try {
|
|
17854
|
+
const rec = withExclusive(() => recoverFromJournal(brainDir()));
|
|
17855
|
+
if (rec.restored > 0) {
|
|
17856
|
+
writeMeta();
|
|
17857
|
+
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.
|
|
17858
|
+
|
|
17859
|
+
`;
|
|
17860
|
+
}
|
|
17861
|
+
} catch {
|
|
17862
|
+
}
|
|
17863
|
+
const additionalContext = restoreNote + (rest.includes("--naive") ? renderNaiveDump(project, void 0, lim ? Number(lim) : void 0) : renderResume(project, session));
|
|
17469
17864
|
session.save();
|
|
17470
17865
|
process.stdout.write(
|
|
17471
17866
|
JSON.stringify({
|
|
@@ -17582,6 +17977,23 @@ imported ${results.length} entr${results.length === 1 ? "y" : "ies"} (role=impor
|
|
|
17582
17977
|
process.stdout.write((r.committed ? "committed: " : "no-op: ") + r.message + "\n");
|
|
17583
17978
|
return;
|
|
17584
17979
|
}
|
|
17980
|
+
if (cmd === "journal") {
|
|
17981
|
+
if (sub !== "purge") {
|
|
17982
|
+
throw new UsageError("usage: vfkb journal purge (--id <id> | --all)");
|
|
17983
|
+
}
|
|
17984
|
+
const p = parseArgs("journal purge", rest, { id: "value", all: "boolean" });
|
|
17985
|
+
const id = flagValue(p, "id");
|
|
17986
|
+
const all = p.flags.has("all");
|
|
17987
|
+
if (!!id === all) {
|
|
17988
|
+
throw new UsageError("usage: vfkb journal purge (--id <id> | --all)");
|
|
17989
|
+
}
|
|
17990
|
+
const r = withExclusive(() => purgeJournal(brainDir(), { id, all }));
|
|
17991
|
+
process.stdout.write(
|
|
17992
|
+
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.
|
|
17993
|
+
` : "no matching journal lines\n"
|
|
17994
|
+
);
|
|
17995
|
+
return;
|
|
17996
|
+
}
|
|
17585
17997
|
process.stderr.write(USAGE);
|
|
17586
17998
|
process.exit(1);
|
|
17587
17999
|
}
|