@viloforge/vfkb 0.4.0 → 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/bundles/vfkb-mcp.mjs +1 -1
- package/dist/bundles/vfkb.mjs +59 -15
- package/dist/doctor.js +104 -11
- package/package.json +1 -1
|
@@ -32007,7 +32007,7 @@ function queryExplained(opts = {}) {
|
|
|
32007
32007
|
}
|
|
32008
32008
|
|
|
32009
32009
|
// src/version.ts
|
|
32010
|
-
var ENGINE_VERSION = true ? "0.4.
|
|
32010
|
+
var ENGINE_VERSION = true ? "0.4.1" : ownPackageVersion();
|
|
32011
32011
|
|
|
32012
32012
|
// src/mcp-server.ts
|
|
32013
32013
|
var SEARCH_DEFAULT_LIMIT = 25;
|
package/dist/bundles/vfkb.mjs
CHANGED
|
@@ -16077,8 +16077,8 @@ import { dirname as dirname4, join as join7 } from "node:path";
|
|
|
16077
16077
|
|
|
16078
16078
|
// src/version.ts
|
|
16079
16079
|
var SCHEMA_VERSION = 1;
|
|
16080
|
-
var ENGINE_VERSION = true ? "0.4.
|
|
16081
|
-
var ENGINE_COMMIT = true ? "
|
|
16080
|
+
var ENGINE_VERSION = true ? "0.4.1" : ownPackageVersion();
|
|
16081
|
+
var ENGINE_COMMIT = true ? "66ee821" : "dev";
|
|
16082
16082
|
|
|
16083
16083
|
// src/manifest.ts
|
|
16084
16084
|
function manifestPath(brainDir2) {
|
|
@@ -16869,8 +16869,8 @@ function approvalNotice(project) {
|
|
|
16869
16869
|
|
|
16870
16870
|
// src/doctor.ts
|
|
16871
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";
|
|
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";
|
|
16874
16874
|
function readJson2(path) {
|
|
16875
16875
|
if (!existsSync9(path)) return void 0;
|
|
16876
16876
|
try {
|
|
@@ -17065,11 +17065,45 @@ async function checkNpmCurrency(opts) {
|
|
|
17065
17065
|
clearTimeout(timer);
|
|
17066
17066
|
}
|
|
17067
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
|
+
}
|
|
17068
17101
|
function runDoctor(opts) {
|
|
17069
17102
|
const { root, brainDir: brainDir2, env } = opts;
|
|
17103
|
+
const runningCommit = opts.engineCommit ?? ENGINE_COMMIT;
|
|
17070
17104
|
const checks = [];
|
|
17071
17105
|
const add = (name, status, detail) => checks.push({ name, status, detail });
|
|
17072
|
-
add("engine", "ok", `version ${ENGINE_VERSION} \xB7 commit ${
|
|
17106
|
+
add("engine", "ok", `version ${ENGINE_VERSION} \xB7 commit ${runningCommit} \xB7 schema v${SCHEMA_VERSION}`);
|
|
17073
17107
|
const settings = readJson2(join13(root, ".claude", "settings.json"));
|
|
17074
17108
|
const configDir = claudeConfigDir(env);
|
|
17075
17109
|
const pluginsFile = opts.pluginsFile ?? (configDir ? join13(configDir, "plugins", "installed_plugins.json") : void 0);
|
|
@@ -17079,7 +17113,16 @@ function runDoctor(opts) {
|
|
|
17079
17113
|
add(
|
|
17080
17114
|
"brain manifest",
|
|
17081
17115
|
"warn",
|
|
17082
|
-
|
|
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)`
|
|
17083
17126
|
);
|
|
17084
17127
|
} else if (typeof mf.schema_version !== "number") {
|
|
17085
17128
|
add("brain manifest", "warn", "manifest has no numeric schema_version");
|
|
@@ -17088,9 +17131,10 @@ function runDoctor(opts) {
|
|
|
17088
17131
|
} else if (mf.schema_version < SCHEMA_VERSION) {
|
|
17089
17132
|
add("brain\u2194engine compat", "warn", `brain schema v${mf.schema_version} is older than engine v${SCHEMA_VERSION} \u2014 migration may be needed`);
|
|
17090
17133
|
} else {
|
|
17091
|
-
|
|
17092
|
-
|
|
17093
|
-
|
|
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`);
|
|
17094
17138
|
}
|
|
17095
17139
|
}
|
|
17096
17140
|
{
|
|
@@ -17110,7 +17154,7 @@ function runDoctor(opts) {
|
|
|
17110
17154
|
} else {
|
|
17111
17155
|
add("journal", "ok", `${js.walLines} line(s) in the uncommitted window, nothing to restore`);
|
|
17112
17156
|
}
|
|
17113
|
-
if (existsSync9(join13(brainDir2, ".journal"))) {
|
|
17157
|
+
if (existsSync9(join13(brainDir2, ".journal")) && isUnder(repoToplevel(root), join13(brainDir2, ".journal"))) {
|
|
17114
17158
|
try {
|
|
17115
17159
|
execFileSync6("git", ["-C", root, "check-ignore", "-q", join13(brainDir2, ".journal", "wal.jsonl")], {
|
|
17116
17160
|
stdio: "ignore"
|
|
@@ -17160,7 +17204,7 @@ function runDoctor(opts) {
|
|
|
17160
17204
|
}
|
|
17161
17205
|
const hooks = settings?.hooks ?? {};
|
|
17162
17206
|
const expected = ["SessionStart", "PreToolUse", "Stop", "SessionEnd"];
|
|
17163
|
-
const have = expected.filter((e) => JSON.stringify(hooks[e] ?? "")
|
|
17207
|
+
const have = expected.filter((e) => isEngineWiring(JSON.stringify(hooks[e] ?? "")));
|
|
17164
17208
|
if (plugin && have.length > 0) {
|
|
17165
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)`);
|
|
17166
17210
|
} else if (plugin) {
|
|
@@ -17380,13 +17424,13 @@ function packageVersion() {
|
|
|
17380
17424
|
}
|
|
17381
17425
|
}
|
|
17382
17426
|
function readStdin() {
|
|
17383
|
-
return new Promise((
|
|
17427
|
+
return new Promise((resolve5) => {
|
|
17384
17428
|
let data = "";
|
|
17385
|
-
if (process.stdin.isTTY) return
|
|
17429
|
+
if (process.stdin.isTTY) return resolve5("");
|
|
17386
17430
|
process.stdin.setEncoding("utf8");
|
|
17387
17431
|
process.stdin.on("data", (c) => data += c);
|
|
17388
|
-
process.stdin.on("end", () =>
|
|
17389
|
-
setTimeout(() =>
|
|
17432
|
+
process.stdin.on("end", () => resolve5(data));
|
|
17433
|
+
setTimeout(() => resolve5(data), 2e3).unref?.();
|
|
17390
17434
|
});
|
|
17391
17435
|
}
|
|
17392
17436
|
function flag(args, name) {
|
package/dist/doctor.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// or inconsistent wiring, and the dual-clone drift signal (a brain last stamped by
|
|
4
4
|
// a different engine build). Deterministic; unit-tested (the inner gate per ADR-0023).
|
|
5
5
|
import { execFileSync } from 'node:child_process';
|
|
6
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
7
|
-
import { join, dirname } from 'node:path';
|
|
6
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, realpathSync } from 'node:fs';
|
|
7
|
+
import { join, dirname, relative, resolve, isAbsolute } from 'node:path';
|
|
8
8
|
import { SCHEMA_VERSION, ENGINE_VERSION, ENGINE_COMMIT } from './version.js';
|
|
9
9
|
import { journalStatus } from './journal.js';
|
|
10
10
|
import { readManifest } from './manifest.js';
|
|
@@ -313,12 +313,81 @@ export async function checkNpmCurrency(opts) {
|
|
|
313
313
|
clearTimeout(timer);
|
|
314
314
|
}
|
|
315
315
|
}
|
|
316
|
+
// #186 — what OLD (pre-plugin) vfkb wiring actually is: a hook command that
|
|
317
|
+
// invokes the ENGINE's hook dispatcher, either through the committed bootstrap
|
|
318
|
+
// (ADR-0031) or by naming a hook subcommand directly.
|
|
319
|
+
//
|
|
320
|
+
// The predicate used to be `JSON.stringify(hook).includes('vfkb')`, which
|
|
321
|
+
// classified ANY hook mentioning the string as old wiring — including the
|
|
322
|
+
// ADR-0059 INACTIVE guard (`.claude/vfkb-guard.mjs`) that the plugin's own
|
|
323
|
+
// MIGRATION_GUIDE tells every consumer to commit. So a correctly migrated repo
|
|
324
|
+
// was told to delete its own INACTIVE detector, and real double wiring was
|
|
325
|
+
// buried in a warning consumers had learned to ignore.
|
|
326
|
+
const HOOK_SUBCOMMANDS = ['session-start', 'pre-tool-use', 'post-tool-use', 'stop', 'session-end'];
|
|
327
|
+
export function isEngineWiring(hookJson) {
|
|
328
|
+
// Compare against the LITERAL command text, not its JSON encoding: a tab in the
|
|
329
|
+
// command is the two characters \\t once stringified, and a quoted subcommand is
|
|
330
|
+
// \\". Un-escaping first means the separator rule below is about real whitespace
|
|
331
|
+
// and quoting rather than about JSON spelling. (Review of PR #216, minor 5: the
|
|
332
|
+
// previous `\\?` sat before the whitespace, where JSON never puts a backslash.)
|
|
333
|
+
const text = hookJson.replace(/\\[tnr]/g, ' ').replace(/\\(.)/g, '$1');
|
|
334
|
+
if (/bootstrap\.mjs/.test(text))
|
|
335
|
+
return true;
|
|
336
|
+
return new RegExp(`\\bhook["'\\s]+(${HOOK_SUBCOMMANDS.join('|')})\\b`).test(text);
|
|
337
|
+
}
|
|
338
|
+
// #212 — a sentinel commit means "this build does not know its own identity"
|
|
339
|
+
// (version.ts's honest `dev` fallback on the tsc/dist path), NOT "a different
|
|
340
|
+
// engine stamped this brain". Comparing a sentinel against a real sha is not a
|
|
341
|
+
// drift observation, it is an absence of one.
|
|
342
|
+
//
|
|
343
|
+
// The old predicate exempted a dev RUNNING engine but not a dev MANIFEST, so a
|
|
344
|
+
// brain stamped by a dist-path build (e.g. the documented `node dist/cli.js`
|
|
345
|
+
// fallback, which is how ViloGate's manifest got `"dev"`) reported drift forever
|
|
346
|
+
// against every real-sha engine.
|
|
347
|
+
export function engineDrift(manifestCommit, runningCommit) {
|
|
348
|
+
if (!manifestCommit || manifestCommit === 'dev')
|
|
349
|
+
return false;
|
|
350
|
+
if (!runningCommit || runningCommit === 'dev')
|
|
351
|
+
return false;
|
|
352
|
+
return manifestCommit !== runningCommit;
|
|
353
|
+
}
|
|
354
|
+
// #206 — the work tree that actually governs a path, or undefined outside a repo.
|
|
355
|
+
function repoToplevel(root) {
|
|
356
|
+
try {
|
|
357
|
+
return execFileSync('git', ['-C', root, 'rev-parse', '--show-toplevel'], {
|
|
358
|
+
encoding: 'utf8',
|
|
359
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
360
|
+
}).trim();
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
return undefined;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
// Containment on path BOUNDARIES: `/repo` must not be judged to contain
|
|
367
|
+
// `/repo-other/...`. Both sides are realpath'd so a symlinked brain dir (or a
|
|
368
|
+
// /tmp that resolves to /private/tmp) compares honestly rather than by spelling.
|
|
369
|
+
function isUnder(parent, child) {
|
|
370
|
+
if (!parent)
|
|
371
|
+
return false;
|
|
372
|
+
const real = (p) => {
|
|
373
|
+
try {
|
|
374
|
+
return realpathSync(p);
|
|
375
|
+
}
|
|
376
|
+
catch {
|
|
377
|
+
return resolve(p);
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
const rel = relative(real(parent), real(child));
|
|
381
|
+
return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
|
|
382
|
+
}
|
|
316
383
|
export function runDoctor(opts) {
|
|
317
384
|
const { root, brainDir, env } = opts;
|
|
385
|
+
// The identity we compare the brain's stamp against (injectable — see DoctorOpts).
|
|
386
|
+
const runningCommit = opts.engineCommit ?? ENGINE_COMMIT;
|
|
318
387
|
const checks = [];
|
|
319
388
|
const add = (name, status, detail) => checks.push({ name, status, detail });
|
|
320
389
|
// 1. Engine identity (info).
|
|
321
|
-
add('engine', 'ok', `version ${ENGINE_VERSION} · commit ${
|
|
390
|
+
add('engine', 'ok', `version ${ENGINE_VERSION} · commit ${runningCommit} · schema v${SCHEMA_VERSION}`);
|
|
322
391
|
// Read the project settings once (used by plugin detection, hooks checks, and
|
|
323
392
|
// project-consistency below), and detect ADR-0045 plugin wiring up front — it changes
|
|
324
393
|
// what "healthy" means for the wiring checks AND which advice is safe to give
|
|
@@ -330,9 +399,19 @@ export function runDoctor(opts) {
|
|
|
330
399
|
// 2. Brain ↔ engine compat (the load-bearing check).
|
|
331
400
|
const mf = readManifest(brainDir);
|
|
332
401
|
if (!mf) {
|
|
333
|
-
add('brain manifest', 'warn',
|
|
334
|
-
|
|
335
|
-
|
|
402
|
+
add('brain manifest', 'warn',
|
|
403
|
+
// #188 — the ordinary write path (addEntry → appendRecord → backend.append)
|
|
404
|
+
// never touches manifest.json. `writeManifest` has exactly two callers:
|
|
405
|
+
// `vfkb init` (init.ts) and the broadcast heal, which fires only when the
|
|
406
|
+
// manifest is ABSENT (broadcast.ts). Saying "on the next write" claimed
|
|
407
|
+
// behaviour the code does not have.
|
|
408
|
+
// On a PLUGIN-wired repo the message must not prescribe `vfkb init` — that
|
|
409
|
+
// advice is what scaffolds double wiring (issue #77), and the invariant is
|
|
410
|
+
// asserted in doctor.test.ts. A plugin-born brain simply has no manifest
|
|
411
|
+
// until a cross-repo broadcast heals it (vfkb#193).
|
|
412
|
+
plugin
|
|
413
|
+
? `no manifest.json in ${brainDir} — plugin-born brains have none until a cross-repo broadcast heals it (vfkb#193); the ordinary write path never creates one`
|
|
414
|
+
: `no manifest.json in ${brainDir} — run \`vfkb init\` to stamp it (the ordinary write path never creates one)`);
|
|
336
415
|
}
|
|
337
416
|
else if (typeof mf.schema_version !== 'number') {
|
|
338
417
|
add('brain manifest', 'warn', 'manifest has no numeric schema_version');
|
|
@@ -344,10 +423,19 @@ export function runDoctor(opts) {
|
|
|
344
423
|
add('brain↔engine compat', 'warn', `brain schema v${mf.schema_version} is older than engine v${SCHEMA_VERSION} — migration may be needed`);
|
|
345
424
|
}
|
|
346
425
|
else {
|
|
347
|
-
|
|
426
|
+
// #212 — a sentinel stamp is suppressed as DRIFT (it is not drift), but it must
|
|
427
|
+
// not become invisible: the brain's provenance genuinely is unrecorded, and the
|
|
428
|
+
// stamping half of #212 (a build that does not know its commit should not write
|
|
429
|
+
// one) is still open. Reported in the detail rather than as a new warn — it is a
|
|
430
|
+
// true statement, but not one the consumer can act on, and this PR exists to stop
|
|
431
|
+
// doctor crying wolf.
|
|
432
|
+
const sentinel = mf.engine_commit === 'dev'
|
|
433
|
+
? ` · stamped by a build that did not know its own commit ("dev") — provenance unknown (#212)`
|
|
434
|
+
: '';
|
|
435
|
+
add('brain↔engine compat', 'ok', `schema v${mf.schema_version} matches${sentinel}`);
|
|
348
436
|
// Drift signal: same schema but a different engine build last stamped the brain.
|
|
349
|
-
if (mf.engine_commit
|
|
350
|
-
add('engine drift', 'warn', `brain last stamped by engine ${mf.engine_commit}, running ${
|
|
437
|
+
if (engineDrift(mf.engine_commit, runningCommit)) {
|
|
438
|
+
add('engine drift', 'warn', `brain last stamped by engine ${mf.engine_commit}, running ${runningCommit} — possible dual-clone drift`);
|
|
351
439
|
}
|
|
352
440
|
}
|
|
353
441
|
// 2b. Durable-capture journal (ADR-0064): what would recovery do, and is a
|
|
@@ -367,7 +455,12 @@ export function runDoctor(opts) {
|
|
|
367
455
|
// has no .journal/ gitignore line — its next `git add .vfkb` would COMMIT
|
|
368
456
|
// the wal (a tracked journal dies by the same reset --hard, RFC-034
|
|
369
457
|
// Alternatives; and a committed wal defeats §4 redaction).
|
|
370
|
-
|
|
458
|
+
// #206 — judge only a journal the repo actually governs. With the brain on
|
|
459
|
+
// the default ~/.vfkb tier (VFKB_DATA_DIR outside the work tree),
|
|
460
|
+
// `check-ignore` exits 128 ("path outside work tree"), which the catch-all
|
|
461
|
+
// below read as plain "not ignored" — so doctor advised editing a .gitignore
|
|
462
|
+
// that does not govern that path at all.
|
|
463
|
+
if (existsSync(join(brainDir, '.journal')) && isUnder(repoToplevel(root), join(brainDir, '.journal'))) {
|
|
371
464
|
try {
|
|
372
465
|
execFileSync('git', ['-C', root, 'check-ignore', '-q', join(brainDir, '.journal', 'wal.jsonl')], {
|
|
373
466
|
stdio: 'ignore',
|
|
@@ -430,7 +523,7 @@ export function runDoctor(opts) {
|
|
|
430
523
|
// 5. Hooks wiring (same plugin logic as check 4).
|
|
431
524
|
const hooks = settings?.hooks ?? {};
|
|
432
525
|
const expected = ['SessionStart', 'PreToolUse', 'Stop', 'SessionEnd'];
|
|
433
|
-
const have = expected.filter((e) => JSON.stringify(hooks[e] ?? '')
|
|
526
|
+
const have = expected.filter((e) => isEngineWiring(JSON.stringify(hooks[e] ?? '')));
|
|
434
527
|
if (plugin && have.length > 0) {
|
|
435
528
|
add('.claude/settings.json', 'warn', `vfkb hooks present ALONGSIDE the plugin (double wiring) — remove them; the plugin's hooks are primary (ADR-0045)`);
|
|
436
529
|
}
|
package/package.json
CHANGED