@viloforge/vfkb 0.2.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/LICENSE +21 -0
- package/README.md +290 -0
- package/dist/args.js +82 -0
- package/dist/backend.js +115 -0
- package/dist/bootstrap.test.js +64 -0
- package/dist/bundles/vfkb-mcp.mjs +32139 -0
- package/dist/bundles/vfkb.mjs +17583 -0
- package/dist/bundles.test.js +177 -0
- package/dist/cli.js +635 -0
- package/dist/counters.js +61 -0
- package/dist/curator.js +87 -0
- package/dist/distiller.js +116 -0
- package/dist/doctor.js +479 -0
- package/dist/doctor.test.js +179 -0
- package/dist/engine.js +668 -0
- package/dist/env-compat.test.js +23 -0
- package/dist/export.js +371 -0
- package/dist/gating.js +34 -0
- package/dist/git.js +43 -0
- package/dist/import.js +99 -0
- package/dist/import.test.js +54 -0
- package/dist/index-store.js +101 -0
- package/dist/init.js +264 -0
- package/dist/init.test.js +148 -0
- package/dist/lock.js +158 -0
- package/dist/manifest.js +38 -0
- package/dist/mcp-server.js +213 -0
- package/dist/pi-extension.js +117 -0
- package/dist/pi-mcp-bridge.js +94 -0
- package/dist/pi-types.js +6 -0
- package/dist/read.js +112 -0
- package/dist/secrets.js +36 -0
- package/dist/session-end.js +183 -0
- package/dist/session-end.test.js +149 -0
- package/dist/session.js +157 -0
- package/dist/stop-reminder.js +130 -0
- package/dist/storage.js +145 -0
- package/dist/types.js +6 -0
- package/dist/validate.js +83 -0
- package/dist/version.js +32 -0
- package/package.json +51 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// FR-2 (ADR-0030) inner gate — the single-file engine bundles must resolve and
|
|
2
|
+
// run PORTABLY: from a working directory that is NOT the repo and has NO
|
|
3
|
+
// node_modules (the consumer condition). This is the deterministic backstop for
|
|
4
|
+
// "portable engine resolution"; the agent-driven consumer-onboarding L4 scenario
|
|
5
|
+
// is the capability-level DoD (ADR-0029).
|
|
6
|
+
import { describe, it, expect, beforeAll } from 'vitest';
|
|
7
|
+
import { spawn } from 'node:child_process';
|
|
8
|
+
import { mkdtempSync, readFileSync, existsSync } from 'node:fs';
|
|
9
|
+
import { tmpdir } from 'node:os';
|
|
10
|
+
import { join, resolve } from 'node:path';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
const repoRoot = resolve(fileURLToPath(new URL('..', import.meta.url)));
|
|
13
|
+
const buildScript = join(repoRoot, 'scripts', 'build-bundles.mjs');
|
|
14
|
+
let bundles;
|
|
15
|
+
function run(cmd, args, opts) {
|
|
16
|
+
return new Promise((resolveP) => {
|
|
17
|
+
const child = spawn(cmd, args, {
|
|
18
|
+
cwd: opts.cwd,
|
|
19
|
+
env: { ...process.env, ...(opts.env ?? {}) },
|
|
20
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
21
|
+
});
|
|
22
|
+
let stdout = '';
|
|
23
|
+
let stderr = '';
|
|
24
|
+
child.stdout.on('data', (d) => (stdout += d));
|
|
25
|
+
child.stderr.on('data', (d) => (stderr += d));
|
|
26
|
+
const t = setTimeout(() => child.kill(), opts.timeoutMs ?? 15000);
|
|
27
|
+
child.on('close', (code) => {
|
|
28
|
+
clearTimeout(t);
|
|
29
|
+
resolveP({ code, stdout, stderr });
|
|
30
|
+
});
|
|
31
|
+
if (opts.input)
|
|
32
|
+
child.stdin.write(opts.input);
|
|
33
|
+
child.stdin.end();
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
// Drive the MCP server over stdio: initialize → initialized → tools/list.
|
|
37
|
+
function mcpToolNames(bundle, cwd, brain) {
|
|
38
|
+
return new Promise((resolveP, reject) => {
|
|
39
|
+
const child = spawn('node', [bundle], {
|
|
40
|
+
cwd,
|
|
41
|
+
env: { ...process.env, VFKB_DIR: brain, VFKB_PROJECT: 'bundle-test' },
|
|
42
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
43
|
+
});
|
|
44
|
+
let buf = '';
|
|
45
|
+
const t = setTimeout(() => {
|
|
46
|
+
child.kill();
|
|
47
|
+
reject(new Error('mcp tools/list timed out'));
|
|
48
|
+
}, 15000);
|
|
49
|
+
child.stdout.on('data', (d) => {
|
|
50
|
+
buf += d;
|
|
51
|
+
let i;
|
|
52
|
+
while ((i = buf.indexOf('\n')) >= 0) {
|
|
53
|
+
const line = buf.slice(0, i).trim();
|
|
54
|
+
buf = buf.slice(i + 1);
|
|
55
|
+
if (!line)
|
|
56
|
+
continue;
|
|
57
|
+
let msg;
|
|
58
|
+
try {
|
|
59
|
+
msg = JSON.parse(line);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (msg.id === 2) {
|
|
65
|
+
clearTimeout(t);
|
|
66
|
+
child.kill();
|
|
67
|
+
resolveP((msg.result?.tools ?? []).map((x) => x.name).sort());
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
const send = (o) => child.stdin.write(JSON.stringify(o) + '\n');
|
|
72
|
+
send({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 't', version: '0' } } });
|
|
73
|
+
send({ jsonrpc: '2.0', method: 'notifications/initialized' });
|
|
74
|
+
send({ jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} });
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
// Drive the MCP server over stdio for a kb_add(why) → kb_get round-trip; returns the
|
|
78
|
+
// stored entry text. Proves the BUNDLE (the consumer surface) folds `why` (gotcha 91338268).
|
|
79
|
+
function mcpAddWhyGetText(bundle, cwd, brain) {
|
|
80
|
+
return new Promise((resolveP, reject) => {
|
|
81
|
+
const child = spawn('node', [bundle], {
|
|
82
|
+
cwd,
|
|
83
|
+
env: { ...process.env, VFKB_DIR: brain, VFKB_PROJECT: 'bundle-test' },
|
|
84
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
85
|
+
});
|
|
86
|
+
let buf = '';
|
|
87
|
+
const t = setTimeout(() => {
|
|
88
|
+
child.kill();
|
|
89
|
+
reject(new Error('mcp why round-trip timed out'));
|
|
90
|
+
}, 15000);
|
|
91
|
+
const send = (o) => child.stdin.write(JSON.stringify(o) + '\n');
|
|
92
|
+
child.stdout.on('data', (d) => {
|
|
93
|
+
buf += d;
|
|
94
|
+
let i;
|
|
95
|
+
while ((i = buf.indexOf('\n')) >= 0) {
|
|
96
|
+
const line = buf.slice(0, i).trim();
|
|
97
|
+
buf = buf.slice(i + 1);
|
|
98
|
+
if (!line)
|
|
99
|
+
continue;
|
|
100
|
+
let msg;
|
|
101
|
+
try {
|
|
102
|
+
msg = JSON.parse(line);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (msg.id === 2) {
|
|
108
|
+
const added = msg.result?.content?.[0]?.text ?? '';
|
|
109
|
+
const id = (added.match(/added\s+(\S+)/) ?? [])[1];
|
|
110
|
+
send({ jsonrpc: '2.0', id: 3, method: 'tools/call', params: { name: 'kb_get', arguments: { id } } });
|
|
111
|
+
}
|
|
112
|
+
else if (msg.id === 3) {
|
|
113
|
+
clearTimeout(t);
|
|
114
|
+
child.kill();
|
|
115
|
+
try {
|
|
116
|
+
resolveP(JSON.parse(msg.result?.content?.[0]?.text ?? '{}').text ?? '');
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
resolveP('');
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
send({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 't', version: '0' } } });
|
|
125
|
+
send({ jsonrpc: '2.0', method: 'notifications/initialized' });
|
|
126
|
+
send({
|
|
127
|
+
jsonrpc: '2.0',
|
|
128
|
+
id: 2,
|
|
129
|
+
method: 'tools/call',
|
|
130
|
+
params: { name: 'kb_add', arguments: { type: 'decision', text: 'adopt the bundle', why: 'consumer surface' } },
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
describe('FR-2 portable single-file engine bundles (ADR-0030)', () => {
|
|
135
|
+
beforeAll(async () => {
|
|
136
|
+
const out = mkdtempSync(join(tmpdir(), 'vfkb-bundles-'));
|
|
137
|
+
const r = await run('node', [buildScript, out], { cwd: repoRoot, timeoutMs: 60000 });
|
|
138
|
+
expect(r.code, `build-bundles failed:\n${r.stderr}`).toBe(0);
|
|
139
|
+
bundles = { cli: join(out, 'vfkb.mjs'), mcp: join(out, 'vfkb-mcp.mjs') };
|
|
140
|
+
expect(existsSync(bundles.cli)).toBe(true);
|
|
141
|
+
expect(existsSync(bundles.mcp)).toBe(true);
|
|
142
|
+
}, 60000);
|
|
143
|
+
it('vfkb-mcp.mjs advertises all 9 kb_* tools from a non-repo cwd with no node_modules', async () => {
|
|
144
|
+
const cwd = mkdtempSync(join(tmpdir(), 'vfkb-consumer-'));
|
|
145
|
+
const brain = mkdtempSync(join(tmpdir(), 'vfkb-brain-'));
|
|
146
|
+
const tools = await mcpToolNames(bundles.mcp, cwd, brain);
|
|
147
|
+
expect(tools).toEqual([
|
|
148
|
+
'kb_add',
|
|
149
|
+
'kb_context',
|
|
150
|
+
'kb_get',
|
|
151
|
+
'kb_list',
|
|
152
|
+
'kb_map',
|
|
153
|
+
'kb_resume',
|
|
154
|
+
'kb_search',
|
|
155
|
+
'kb_supersede',
|
|
156
|
+
'kb_transition',
|
|
157
|
+
]);
|
|
158
|
+
}, 30000);
|
|
159
|
+
it('vfkb-mcp.mjs folds kb_add `why` into the entry text (gotcha 91338268)', async () => {
|
|
160
|
+
const cwd = mkdtempSync(join(tmpdir(), 'vfkb-consumer-'));
|
|
161
|
+
const brain = mkdtempSync(join(tmpdir(), 'vfkb-brain-'));
|
|
162
|
+
const text = await mcpAddWhyGetText(bundles.mcp, cwd, brain);
|
|
163
|
+
expect(text).toContain('adopt the bundle');
|
|
164
|
+
expect(text).toContain('Why: consumer surface');
|
|
165
|
+
}, 30000);
|
|
166
|
+
it('vfkb.mjs runs the engine (add → persisted) from a non-repo cwd with no node_modules', async () => {
|
|
167
|
+
const cwd = mkdtempSync(join(tmpdir(), 'vfkb-consumer-'));
|
|
168
|
+
const brain = mkdtempSync(join(tmpdir(), 'vfkb-brain-'));
|
|
169
|
+
const r = await run('node', [bundles.cli, 'add', 'fact', 'bundle-portability-smoke', '--role', 'human'], {
|
|
170
|
+
cwd,
|
|
171
|
+
env: { VFKB_DIR: brain, VFKB_PROJECT: 'bundle-test' },
|
|
172
|
+
});
|
|
173
|
+
expect(r.code, `cli add failed:\n${r.stderr}`).toBe(0);
|
|
174
|
+
const entries = readFileSync(join(brain, 'entries.jsonl'), 'utf8');
|
|
175
|
+
expect(entries).toContain('bundle-portability-smoke');
|
|
176
|
+
}, 30000);
|
|
177
|
+
});
|