@su-record/vibe 4.1.5 → 4.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -2,551 +2,43 @@
2
2
  /**
3
3
  * vibe CLI — verdicts, records, tokens. Skills call this from the shell.
4
4
  * Every command accepts --json. The exit code is the verdict (errors.ts).
5
+ * Commands live in cli/: setup · work · human · memory. This file only dispatches.
5
6
  */
6
7
  import fs from 'node:fs';
7
8
  import path from 'node:path';
8
- import { runChecks } from './core/check.js';
9
- import { detectClient, detectModel } from './core/client.js';
10
- import { irreversibleNeedsToken, parseTokenPolicy, readConfig, writeConfig } from './core/config.js';
11
- import { denied, usage, VibeError } from './core/errors.js';
12
- import { answer, ask, openQuestions, resolve as resolveQuestion } from './core/inbox.js';
13
- import { abandon, approve, draft, intentPath, loadScenarios } from './core/intent.js';
14
- import { addKnowledge } from './core/knowledge.js';
15
- import { compare, EDGE_TYPES, readEdges, readLedger, record, why } from './core/ledger.js';
16
- import { findProjectRoot, vibePath } from './core/paths.js';
17
- import { listRegressions, recordRegression } from './core/regress.js';
18
- import { profileFile } from './core/profile.js';
19
- import { readDocument } from './core/docs/read.js';
20
- import { research, SOURCES } from './core/research.js';
21
- import { addSkill, createSkill, dismissProposal, listSkills, markUsed, pruneSkills, suggestSkills } from './core/skills.js';
22
- import { graphMermaid } from './core/scenarios.js';
23
- import { readState } from './core/state.js';
24
- import { readJson, readText } from './core/store.js';
25
- import { verifyAndConsume } from './core/tokens.js';
26
- import { buildStateView } from './core/view.js';
27
- import { installPlugin, pluginStatus } from './install/plugin.js';
28
- import { checkPluginTree, writePluginTree } from './install/tree.js';
29
- import { checkUpdate, runUpdate } from './install/update.js';
30
- import { buildMcpb } from './install/mcpb.js';
31
- import { spawnSync } from 'node:child_process';
32
- import { ensureGlobal, globalStatus, uninstallGlobal, uninstallProjectSurfaces } from './install/global.js';
33
- import { ensureProject, projectStatus, purgeProject } from './install/project.js';
34
- const BOOLEAN_FLAGS = new Set(['json', 'all', 'stdin', 'purge-state', 'dry-run', 'yes', 'help', 'version']);
35
- export function parseArgs(argv) {
36
- const positionals = [];
37
- const flags = {};
38
- for (let i = 0; i < argv.length; i += 1) {
39
- const arg = argv[i];
40
- if (!arg.startsWith('--')) {
41
- positionals.push(arg);
42
- continue;
43
- }
44
- const eq = arg.indexOf('=');
45
- const key = eq === -1 ? arg.slice(2) : arg.slice(2, eq);
46
- if (eq !== -1) {
47
- flags[key] = arg.slice(eq + 1);
48
- }
49
- else if (BOOLEAN_FLAGS.has(key) || i + 1 >= argv.length || argv[i + 1].startsWith('--')) {
50
- flags[key] = true;
51
- }
52
- else {
53
- flags[key] = argv[i + 1];
54
- i += 1;
55
- }
56
- }
57
- return { positionals, flags };
58
- }
59
- function flagString(flags, key) {
60
- const value = flags[key];
61
- return typeof value === 'string' ? value : undefined;
62
- }
63
- const HELP = `vibe — an AX/FDE harness. The harness judges; a human approves.
64
-
65
- setup update [--check] · status · tokens [strict|irreversible|off] · uninstall [--purge-state] (card, skills and hook live in ~/.claude and ~/.codex;
66
- npm i -g puts them there and any vibe command repairs them; uninstall also clears what an older init left in the project)
67
- plugin build [--check] (manifests from package.json) · plugin mcpb [--out vibe.mcpb] (Claude desktop app bundle) · plugin install | status [--home <dir>]
68
- work state [--graph] · read <file> [--sheet] [--pages] (xlsx·docx·pptx·pdf·hwp·hwpx·html·tables) · profile <file> [--sheet] (csv·tsv·jsonl·json·xlsx) · intent draft <intent.md> <scenarios.yaml> | --stdin · intent show
69
- approve [token] · check [id…] [--all] · evidence [run] · abandon --reason "…"
70
- checks run (exit code) · file (exists·pattern·contains·schema·sum) · http (status·schema·maxMs) · eval (matching cases ≥ expect.pass) · human (inbox, no verdict)
71
- human ask "question" [--options "a|b"] [--default a] [--needs approve|authorize:<action>] [--target "…"]
72
- authorize <token> --action push|deploy|send|delete|spend [--target "…"] · inbox [list|answer <id> "text"|resolve <id>]
73
- memory regress record --scenario <id> --title "…" [--check-from-evidence <run>] · regress list
74
- knowledge add <file|--stdin> --title "…"
75
- research research --from-intent | "query" [--sources repos,code,skills] [--max 5] (GitHub · skill catalogs · 24h cache)
76
- skills skill suggest [--all] · skill create <name> --check run|file|http|eval [--from-scenario <id>]
77
- skill add owner/repo[@name] [--pin <sha>] [--yes] · skill search <keyword> · skill list
78
- skill used <name> · skill prune [--unused-runs 10] [--dry-run] · skill dismiss <ref>
79
- ledger ledger [--since 7d] · ledger compare --by client|model|harness --metric checks|turns|cost [--min-runs 5] [--ledger <file>]
80
- ledger why <node> [--depth 3] · ledger edges [--type supersedes|decided-by|implements|caused]
81
-
82
- A scenario may declare needs: [ids] — independent scenarios are checked in parallel, dependents after their parents pass.
83
-
84
- Every command accepts --json. Exit codes: 0 ok · 1 verdict failed · 2 usage · 3 token · 4 invalid transition
85
- `;
86
- function readStdin() {
87
- return fs.readFileSync(0, 'utf-8');
88
- }
89
- function parseSince(value) {
90
- if (!value)
91
- return undefined;
92
- const match = /^(\d+)([dhm])$/.exec(value);
93
- if (!match)
94
- throw usage('--since takes forms like 7d · 12h · 30m');
95
- const n = Number(match[1]);
96
- const unit = { d: 86_400_000, h: 3_600_000, m: 60_000 }[match[2]];
97
- return n * unit;
98
- }
99
- // ─── Commands ───────────────────────────────────────────────────────
100
- function cmdTokens(root, policyRaw) {
101
- if (policyRaw) {
102
- ensureProject(root);
103
- writeConfig(root, { tokens: parseTokenPolicy(policyRaw) });
104
- }
105
- const policy = readConfig(root).tokens;
106
- return { json: { tokens: policy }, text: `tokens: ${policy}`, code: 0 };
107
- }
108
- function cmdStatus(root, flags) {
109
- const g = globalStatus(flagString(flags, 'home'));
110
- const p = projectStatus(root);
111
- const update = process.env['VIBE_OFFLINE'] ? { installed: packageVersion(), latest: null, available: false } : checkUpdate();
112
- const lines = [
113
- `vibe ${packageVersion()} — ${g.home}`,
114
- ...Object.entries(g.clients).map(([client, c]) => ` ${client.padEnd(9)} ${c.mode === 'plugin' ? `plugin ${c.pluginVersion ?? 'not installed'}` : `home · card ${c.card ? 'ok' : '-'} · skills ${c.skills} · hook ${c.hook ? 'ok' : '-'}`}${c.current ? '' : ' — stale; any vibe command repairs it'}`),
115
- ` card ${g.cardBytes} bytes${g.cardOver ? ' — over 1KB!' : ''}`,
116
- ...(update.available ? [` update ${update.latest} available — \`vibe update\``] : []),
117
- `project ${p.root}`,
118
- ` .vibe ${p.vibe ? 'ok' : 'none yet — the first record creates it'}`,
119
- ` state ${p.state}`,
120
- ` inbox ${p.inboxOpen} open`,
121
- ];
122
- return { json: { version: packageVersion(), ...g, project: p, update }, text: lines.join('\n'), code: 0 };
123
- }
124
- function cmdUpdate(flags) {
125
- if (flags['check'] === true) {
126
- const c = checkUpdate();
127
- const text = c.latest === null ? `installed ${c.installed} · registry unreachable` : c.available ? `${c.installed} → ${c.latest} available — run \`vibe update\`` : `${c.installed} is current`;
128
- return { json: c, text, code: 0 };
129
- }
130
- const r = runUpdate();
131
- if (!r.updated)
132
- return { json: r, text: r.detail, code: r.latest === null ? 2 : 0 };
133
- // The new binary registers the plugins itself; run it once so the report shows the new state.
134
- const after = spawnSync('vibe', ['status'], { encoding: 'utf-8', timeout: 120_000, shell: process.platform === 'win32' });
135
- const tail = after.status === 0 ? after.stdout.trim() : `run \`vibe status\` to finish the setup`;
136
- return { json: { ...r, status: after.stdout }, text: `updated ${r.detail}\n${tail}`, code: 0 };
137
- }
138
- function cmdUninstall(root, flags) {
139
- const home = uninstallGlobal(flagString(flags, 'home'));
140
- const project = uninstallProjectSurfaces(root);
141
- if (flags['purge-state'] === true && purgeProject(root))
142
- project.push('.vibe/');
143
- const removed = [...home, ...project.map((r) => path.join(path.relative(process.cwd(), root) || '.', r))];
144
- return { json: { removed, home, project, root }, text: removed.length ? `removed: ${removed.join(', ')}` : 'nothing to remove', code: 0 };
145
- }
146
- function cmdPlugin(sub, flags) {
147
- const home = flagString(flags, 'home');
148
- if (sub === 'mcpb') {
149
- const r = buildMcpb(flagString(flags, 'out') ?? 'vibe.mcpb');
150
- return { json: r, text: `${r.file} · ${r.bytes} bytes · vibe ${r.version}\n open it in the Claude desktop app (macOS · Windows), pick the project folder; the app needs the vibe CLI: npm i -g @su-record/vibe`, code: 0 };
151
- }
152
- if (sub === 'build') {
153
- if (flags['check'] === true) {
154
- const drift = checkPluginTree();
155
- return { json: { drift }, text: drift.length ? `plugin tree drift — run \`vibe plugin build\`:\n ${drift.join('\n ')}` : 'plugin tree matches package.json', code: drift.length ? 1 : 0 };
156
- }
157
- const written = writePluginTree();
158
- return { json: { written }, text: written.length ? `wrote ${written.join(', ')}` : 'plugin tree already current', code: 0 };
159
- }
160
- if (sub === 'install') {
161
- const r = installPlugin(home);
162
- const text = [
163
- `plugin ${r.version} → ${r.tree}`,
164
- ` files ${r.files.join(', ')}`,
165
- ` marketplace ${r.marketplace}`,
166
- ' next:',
167
- ...r.next.map((n) => ` ${n}`),
168
- ].join('\n');
169
- return { json: r, text, code: 0 };
170
- }
171
- if (sub === 'status') {
172
- const r = pluginStatus(home);
173
- const text = [
174
- `plugin tree ${r.exists ? 'ok' : 'missing'} — ${r.tree}`,
175
- ` manifest ${r.manifestVersion ?? '-'} · package ${r.packageVersion} · skills ${r.skills} · hooks ${r.hooks ? 'ok' : '-'} · registered ${r.registered ? 'yes' : 'no'}`,
176
- ...(r.drift.length ? r.drift.map((d) => ` drift: ${d}`) : [' no drift']),
177
- ].join('\n');
178
- return { json: r, text, code: r.drift.length ? 1 : 0 };
179
- }
180
- throw usage('plugin build [--check] | plugin mcpb [--out vibe.mcpb] | plugin install | plugin status [--home <dir>]');
181
- }
182
- const GLYPH = { pass: '✔', fail: '✘', pending: '?', blocked: '⊘', never: '·' };
183
- function cmdState(root, flags) {
184
- const view = buildStateView(root);
185
- if (flags['graph'] === true) {
186
- const scenarios = [...loadScenarios(root), ...listRegressions(root)];
187
- const graph = graphMermaid(scenarios, (id) => view.scenarios.find((s) => s.id === id)?.last ?? 'never');
188
- return { json: { ...view, graph }, text: graph, code: 0 };
189
- }
190
- const lines = [
191
- `${view.state} · ${view.stage}${view.intent ? ` · ${view.intent.title}` : ''}`,
192
- ...view.scenarios.map((s) => ` ${GLYPH[s.last] ?? '·'} ${s.id} [${s.type}]${s.needs ? ` needs ${s.needs.join(', ')}` : ''} ${s.then}${s.regression ? ' (regression)' : ''}${s.irreversible ? ` ⚠ ${s.irreversible}` : ''}`),
193
- ` remaining ${view.remaining.length ? view.remaining.join(', ') : 'none'}`,
194
- ` inbox ${view.inbox.open} open${view.inbox.items.map((q) => `\n [${q.id}] ${q.question}`).join('')}`,
195
- ...view.notices.map((n) => ` ! ${n}`),
196
- ...view.proposals.map((p) => ` → ${p.kind}: ${p.ref} (${p.why})`),
197
- ];
198
- return { json: view, text: lines.join('\n'), code: 0 };
199
- }
200
- async function cmdResearch(root, sub, flags) {
201
- ensureProject(root);
202
- const sourcesRaw = flagString(flags, 'sources');
203
- const sources = sourcesRaw ? sourcesRaw.split(',').map((s) => s.trim()) : undefined;
204
- if (sources?.some((s) => !SOURCES.includes(s)))
205
- throw usage(`--sources ${SOURCES.join(',')}`);
206
- const options = { fromIntent: flags['from-intent'] === true };
207
- if (sub)
208
- options.query = sub;
209
- if (sources)
210
- options.sources = sources;
211
- const max = flagString(flags, 'max');
212
- if (max)
213
- options.max = Number(max);
214
- const r = await research(root, options);
215
- const lines = [
216
- `research ${r.queries.map((q) => JSON.stringify(q)).join(' · ')} · ${r.candidates.length} candidates${r.cached ? ' (cached)' : ''}${r.authenticated ? '' : ' · unauthenticated — code search skipped, catalogs only'}`,
217
- ...r.candidates.map((c) => ` ${c.kind.padEnd(5)} ${c.ref}\n ${c.why}\n → ${c.action}`),
218
- ...(r.file ? [` note ${path.relative(root, r.file)}`] : []),
219
- ];
220
- return { json: r, text: lines.join('\n'), code: 0 };
221
- }
222
- function skillCreateOutput(root, name, flags) {
223
- if (!name)
224
- throw usage('skill create <name> --check run|file|http|eval [--from-scenario <id>]');
225
- const input = { name };
226
- const checkType = flagString(flags, 'check');
227
- const from = flagString(flags, 'from-scenario');
228
- if (checkType)
229
- input.checkType = checkType;
230
- if (from)
231
- input.fromScenario = from;
232
- const r = createSkill(root, input);
233
- return { json: r, text: `skill ${r.name} [${r.check.type}] → ${r.paths.join(', ')}\n fill in SKILL.md: description · When · Procedure. It is installed because it carries a check.`, code: 0 };
234
- }
235
- async function skillAddOutput(root, spec, flags) {
236
- if (!spec)
237
- throw usage('skill add owner/repo[@name] [--pin <sha>] [--yes]');
238
- const input = { spec, yes: flags['yes'] === true };
239
- const pin = flagString(flags, 'pin');
240
- if (pin)
241
- input.pin = pin;
242
- const r = await addSkill(root, input);
243
- const commands = r.commands.length ? r.commands.map((c) => ` ${c}`).join('\n') : ' (none)';
244
- if (!r.installed) {
245
- return { json: r, text: `${r.ref} · ${r.license ?? 'no license'} · files ${r.files.join(', ')}\n commands the skill would have the model run:\n${commands}\n nothing installed — rerun with --yes to install these files pinned to ${r.sha.slice(0, 12)}`, code: 3 };
246
- }
247
- return { json: r, text: `installed ${r.ref} · ${r.license ?? 'no license'}\n ${r.paths.join('\n ')}\n commands inside:\n${commands}`, code: 0 };
248
- }
249
- function skillSimple(root, sub, args, flags) {
250
- if (sub === 'list') {
251
- const l = listSkills(root);
252
- const project = l.project.map((s) => ` ${s.name} [${s.kind}] ${s.source}${s.check ? ` · check ${s.check.type}` : ''} · ${s.lastUsedRun === null ? 'never used' : `last used run ${s.lastUsedRun}`} (now ${l.currentRun})`);
253
- return { json: l, text: [`common ${l.common.join(' · ')}`, `project ${l.project.length ? '' : '(none)'}`, ...project].join('\n'), code: 0 };
254
- }
255
- if (sub === 'used') {
256
- if (!args[0])
257
- throw usage('skill used <name>');
258
- const s = markUsed(root, args[0]);
259
- return { json: s, text: `${s.name} used at run ${s.lastUsedRun}`, code: 0 };
260
- }
261
- if (sub === 'prune') {
262
- const options = { dryRun: flags['dry-run'] === true };
263
- const n = flagString(flags, 'unused-runs');
264
- if (n)
265
- options.unusedRuns = Number(n);
266
- const r = pruneSkills(root, options);
267
- return { json: r, text: `${options.dryRun ? 'would remove' : 'removed'} ${r.removed.length ? r.removed.join(', ') : 'nothing'} (unused for ${r.threshold} runs) · kept ${r.kept.length ? r.kept.join(', ') : 'none'}`, code: 0 };
268
- }
269
- if (sub === 'dismiss') {
270
- dismissProposal(root, args[0] ?? '');
271
- return { json: { dismissed: args[0] }, text: 'dismissed — it will not be proposed again', code: 0 };
272
- }
273
- const proposals = suggestSkills(root, flags['all'] === true);
274
- return { json: proposals, text: proposals.length ? proposals.map((p) => `${p.kind.padEnd(9)} ${p.ref}\n ${p.why} [${p.source}]`).join('\n') : 'no proposals — no signal in scenarios, regressions, inbox or state', code: 0 };
275
- }
276
- async function cmdSkill(root, sub, args, flags) {
277
- if (sub && !['list', 'suggest'].includes(sub))
278
- ensureProject(root);
279
- switch (sub) {
280
- case 'create':
281
- return skillCreateOutput(root, args[0], flags);
282
- case 'add':
283
- return skillAddOutput(root, args[0], flags);
284
- case 'search': {
285
- if (!args[0])
286
- throw usage('skill search <keyword>');
287
- const r = await research(root, { query: args[0], sources: ['skills', 'code'] });
288
- return { json: r, text: r.candidates.length ? r.candidates.map((c) => `${c.ref}\n ${c.why}\n → ${c.action}`).join('\n') : `no skill matches "${args[0]}"${r.authenticated ? '' : ' (unauthenticated — catalogs only)'}`, code: 0 };
289
- }
290
- case 'suggest':
291
- case 'list':
292
- case 'used':
293
- case 'prune':
294
- case 'dismiss':
295
- return skillSimple(root, sub, args, flags);
296
- default:
297
- throw usage('skill suggest | create | add | search | list | used | prune | dismiss');
298
- }
299
- }
300
- function cmdRead(root, file, flags) {
301
- if (!file)
302
- throw usage('read <file.xlsx|docx|pptx|pdf|csv|…> [--sheet name] [--pages A-B]');
303
- const options = {};
304
- const sheet = flagString(flags, 'sheet');
305
- const pages = flagString(flags, 'pages');
306
- if (sheet)
307
- options.sheet = sheet;
308
- if (pages)
309
- options.pages = pages;
310
- const d = readDocument(root, file, options);
311
- return { json: d, text: `${d.file} · ${d.format} · read by ${d.method} · ${d.sections.length} section(s)${d.truncated ? ' · truncated' : ''}\n\n${d.text}`, code: 0 };
312
- }
313
- function cmdProfile(root, file, flags) {
314
- if (!file)
315
- throw usage('profile <file.csv|tsv|jsonl|json|xlsx> [--sheet name]');
316
- const p = profileFile(root, file, flagString(flags, 'sheet'));
317
- const lines = [
318
- `${p.file} · ${p.format} · ${p.rows} rows · ${p.columns.length} columns · ${p.duplicateRows} duplicate rows`,
319
- ...p.anomalies.map((a) => ` ! ${a}`),
320
- ...p.columns.map((c) => ` ${c.name || '(no header)'} ${c.type}${c.missing ? ` · missing ${c.missing}` : ''} · distinct ${c.distinct}${c.min !== undefined ? ` · ${c.min}…${c.max}` : ''} · e.g. ${c.sample.map((s) => JSON.stringify(s)).join(', ')}`),
321
- ];
322
- return { json: p, text: lines.join('\n'), code: 0 };
323
- }
324
- function cmdIntent(root, sub, args, flags) {
325
- if (sub === 'show') {
326
- const scenarios = loadScenarios(root);
327
- const intent = readText(intentPath(root)) ?? '';
328
- return { json: { intent, scenarios }, text: `${intent.trim()}\n\n${scenarios.map((s) => `- ${s.id} [${s.check.type}] ${s.then}`).join('\n')}`, code: 0 };
329
- }
330
- if (sub !== 'draft')
331
- throw usage('intent draft | intent show');
332
- ensureProject(root);
333
- let intentText;
334
- let scenariosText;
335
- if (flags['stdin'] === true) {
336
- const payload = JSON.parse(readStdin());
337
- intentText = payload.intent ?? '';
338
- scenariosText = payload.scenarios ?? '';
339
- }
340
- else {
341
- const [intentFile, scenariosFile] = args;
342
- if (!intentFile || !scenariosFile)
343
- throw usage('intent draft <intent.md> <scenarios.yaml> or --stdin');
344
- intentText = readText(path.resolve(root, intentFile)) ?? '';
345
- scenariosText = readText(path.resolve(root, scenariosFile)) ?? '';
346
- if (!intentText)
347
- throw usage(`cannot read ${intentFile}`);
348
- if (!scenariosText)
349
- throw usage(`cannot read ${scenariosFile}`);
350
- }
351
- const result = draft(root, intentText, scenariosText);
352
- if (!result.ok) {
353
- return { json: result, text: `rejected ${result.rejections.length} — nothing was saved\n${result.rejections.map((r) => ` ${r.id}: ${r.reason}`).join('\n')}`, code: 1 };
354
- }
355
- const text = [
356
- `DRAFT saved · ${result.scenarios.length} scenarios · hash ${result.hash}`,
357
- ...result.scenarios.map((s) => ` ${s.id} [${s.check.type}] ${s.then}${s.irreversible ? ` ⚠ ${s.irreversible}` : ''}`),
358
- result.token
359
- ? `approval token: ${result.token} (valid until ${result.expiresAt}) — show it to the user; when they paste it back, run \`vibe approve <token>\``
360
- : `tokens: ${result.policy} — when the user says yes in chat, run \`vibe approve\``,
361
- ].join('\n');
362
- return { json: result, text, code: 0 };
363
- }
364
- function cmdApprove(root, args) {
365
- ensureProject(root);
366
- const token = args.join(' ') || null;
367
- const result = approve(root, token);
368
- return { json: { ok: true, ...result, state: 'APPROVED' }, text: `APPROVED · ${result.hash} (by ${result.basis})`, code: 0 };
369
- }
370
- async function cmdCheck(root, args, flags) {
371
- ensureProject(root);
372
- const options = flags['all'] === true ? { all: true } : args.length ? { ids: args } : {};
373
- const report = await runChecks(root, options);
374
- const lines = [
375
- `${report.run} · ${report.state} · pass ${report.passed} · fail ${report.failed}${report.pending ? ` · pending ${report.pending}` : ''}`,
376
- ...report.outcomes.map((o) => ` ${o.status === 'pass' ? '✔' : o.status === 'fail' ? '✘' : '?'} ${o.id} [${o.type}] exit=${o.exit ?? '-'} ${o.ms}ms${o.reason ? ` — ${o.reason}` : ''}${o.tail && o.status !== 'pass' ? `\n ${o.tail.split('\n').join('\n ')}` : ''}`),
377
- report.done ? ' DONE — every gate scenario passed' : ` remaining ${report.remaining.join(', ') || 'none'}`,
378
- ...(report.stuck ? [' STUCK — the same failure twice in a row; see the inbox'] : []),
379
- ];
380
- const code = report.stuck || report.failed > 0 ? 1 : 0;
381
- return { json: report, text: lines.join('\n'), code };
382
- }
383
- function cmdEvidence(root, args) {
384
- const dir = vibePath(root, 'evidence');
385
- const runId = args[0] ?? (fs.existsSync(dir) ? fs.readdirSync(dir).filter((n) => n.endsWith('.json')).sort((a, b) => Number(a.slice(2, -5)) - Number(b.slice(2, -5))).at(-1)?.replace('.json', '') : undefined);
386
- if (!runId)
387
- throw usage('no evidence yet');
388
- const evidence = readJson(path.join(dir, `${runId}.json`));
389
- if (!evidence)
390
- throw usage(`no such run: ${runId}`);
391
- return { json: evidence, text: JSON.stringify(evidence, null, 2), code: 0 };
392
- }
393
- function cmdAbandon(root, flags) {
394
- ensureProject(root);
395
- abandon(root, flagString(flags, 'reason') ?? '');
396
- return { json: { state: 'ABANDONED' }, text: 'ABANDONED', code: 0 };
397
- }
398
- function cmdAsk(root, args, flags) {
399
- ensureProject(root);
400
- const question = args.join(' ');
401
- if (!question)
402
- throw usage('ask "question"');
403
- const options = flagString(flags, 'options')?.split('|').map((s) => s.trim()).filter(Boolean);
404
- const needsRaw = flagString(flags, 'needs');
405
- let needs;
406
- const policy = readConfig(root).tokens;
407
- if (needsRaw === 'approve') {
408
- const state = readState(root);
409
- if (state.state !== 'DRAFT' || !state.intentHash)
410
- throw denied('an approval token can only be issued in DRAFT');
411
- if (policy === 'strict')
412
- needs = { kind: 'approve', target: state.intentHash };
413
- }
414
- else if (needsRaw?.startsWith('authorize:')) {
415
- const action = needsRaw.slice('authorize:'.length);
416
- if (irreversibleNeedsToken(policy))
417
- needs = { kind: 'authorize', target: `${action}:${flagString(flags, 'target') ?? ''}` };
418
- }
419
- else if (needsRaw) {
420
- throw usage('--needs takes approve or authorize:<action>');
421
- }
422
- const input = { question };
423
- if (options && options.length)
424
- input.options = options;
425
- const def = flagString(flags, 'default');
426
- if (def)
427
- input.default = def;
428
- if (needs)
429
- input.needs = needs;
430
- const result = ask(root, input);
431
- record(root, { event: 'ask', client: detectClient(), model: detectModel(), detail: question.slice(0, 120) });
432
- const text = [
433
- `question recorded [${result.id}]`,
434
- ...(result.token ? [`token: ${result.token} (valid until ${result.expiresAt}) — the user must paste it back in chat before anything proceeds`] : []),
435
- ...(needsRaw && !result.token ? [`tokens: ${policy} — no token needed; proceed when the user says yes`] : []),
436
- ].join('\n');
437
- return { json: { ...result, policy }, text, code: 0 };
438
- }
439
- function cmdAuthorize(root, args, flags) {
440
- ensureProject(root);
441
- const token = args.join(' ') || null;
442
- const action = flagString(flags, 'action');
443
- if (!action)
444
- throw usage('authorize [token] --action push|deploy|send|delete|spend [--target "…"]');
445
- const target = `${action}:${flagString(flags, 'target') ?? ''}`;
446
- const policy = readConfig(root).tokens;
447
- let basis = 'auto';
448
- if (irreversibleNeedsToken(policy)) {
449
- if (!token)
450
- throw denied(`"${action}" needs a human token here (tokens: ${policy})`);
451
- const verdict = verifyAndConsume(root, 'authorize', target, token);
452
- if (!verdict.ok)
453
- throw denied(verdict.reason);
454
- basis = 'token';
455
- }
456
- record(root, { event: 'authorize', client: detectClient(), model: detectModel(), detail: `${target} by ${basis}` });
457
- return { json: { ok: true, action, target, basis }, text: `authorized: ${target} (by ${basis})`, code: 0 };
458
- }
459
- function cmdInbox(root, sub, args) {
460
- if (sub === 'answer') {
461
- const [id, ...rest] = args;
462
- if (!id || rest.length === 0)
463
- throw usage('inbox answer <id> "text"');
464
- if (!answer(root, id, rest.join(' ')))
465
- throw usage(`no such question: ${id}`);
466
- return { json: { ok: true }, text: `answered ${id}`, code: 0 };
467
- }
468
- if (sub === 'resolve') {
469
- const [id] = args;
470
- if (!id)
471
- throw usage('inbox resolve <id>');
472
- if (!resolveQuestion(root, id))
473
- throw usage(`no such question: ${id}`);
474
- return { json: { ok: true }, text: `resolved ${id}`, code: 0 };
475
- }
476
- const open = openQuestions(root);
477
- return { json: open, text: open.length ? open.map((q) => `[${q.id}] ${q.question}${q.options ? ` (${q.options.join(' | ')})` : ''}${q.answer ? `\n answer: ${q.answer}` : ''}`).join('\n') : 'inbox is empty', code: 0 };
478
- }
479
- function cmdRegress(root, sub, flags) {
480
- if (sub === 'list') {
481
- const list = listRegressions(root);
482
- return { json: list, text: list.length ? list.map((s) => `- ${s.id} [${s.check.type}] ${s.then}`).join('\n') : 'no regressions', code: 0 };
483
- }
484
- if (sub !== 'record')
485
- throw usage('regress record --scenario <id> --title "…" | regress list');
486
- ensureProject(root);
487
- const scenario = flagString(flags, 'scenario');
488
- const title = flagString(flags, 'title');
489
- if (!scenario || !title)
490
- throw usage('regress record --scenario <id> --title "…"');
491
- const input = { scenario, title };
492
- const from = flagString(flags, 'check-from-evidence');
493
- if (from)
494
- input.fromEvidence = from;
495
- const result = recordRegression(root, input);
496
- return { json: result, text: `regression ${result.id} → ${path.relative(root, result.file)}`, code: 0 };
497
- }
498
- function cmdKnowledge(root, sub, args, flags) {
499
- ensureProject(root);
500
- if (sub !== 'add')
501
- throw usage('knowledge add <file|--stdin> --title "…"');
502
- const title = flagString(flags, 'title') ?? '';
503
- const body = flags['stdin'] === true ? readStdin() : args[0] ? readText(path.resolve(root, args[0])) ?? '' : '';
504
- const result = addKnowledge(root, title, body);
505
- return { json: result, text: `knowledge → ${path.relative(root, result.file)}`, code: 0 };
506
- }
507
- function cmdLedger(root, sub, args, flags) {
508
- if (sub === 'why') {
509
- const node = args[0];
510
- if (!node)
511
- throw usage('ledger why <node> — a scenario id, regression id, file path, intent hash or run id');
512
- const result = why(root, node, Number(flagString(flags, 'depth') ?? 3));
513
- if (!result.node)
514
- return { json: result, text: `no edge touches ${node}`, code: 1 };
515
- const text = [`${result.node}`, ...result.steps.map((s) => `${' '.repeat(s.depth)}${s.edge.from} ─${s.edge.type}→ ${s.edge.to} (${s.edge.event} ${s.edge.at})`)].join('\n');
516
- return { json: result, text, code: 0 };
517
- }
518
- if (sub === 'edges') {
519
- const type = flagString(flags, 'type');
520
- if (type && !EDGE_TYPES.includes(type))
521
- throw usage(`--type ${EDGE_TYPES.join('|')}`);
522
- const edges = readEdges(root, type);
523
- return { json: edges, text: edges.length ? edges.map((e) => `${e.at} ${e.from} ─${e.type}→ ${e.to}`).join('\n') : 'no edges', code: 0 };
524
- }
525
- if (sub === 'compare') {
526
- const by = (flagString(flags, 'by') ?? 'client');
527
- const metric = (flagString(flags, 'metric') ?? 'checks');
528
- if (!['client', 'model', 'harness'].includes(by))
529
- throw usage('--by client|model|harness');
530
- if (!['checks', 'turns', 'cost'].includes(metric))
531
- throw usage('--metric checks|turns|cost');
532
- const minRuns = Number(flagString(flags, 'min-runs') ?? 5);
533
- const c = compare(root, by, metric, minRuns, flagString(flags, 'ledger'));
534
- const text = [
535
- `compare by ${by} · metric ${metric} · verdict ${c.verdict}`,
536
- ` ${c.reason}`,
537
- ...c.arms.map((a) => ` ${a.arm}: runs ${a.runs} · usable ${a.usable}${a.range ? ` · min ${a.range.min} · max ${a.range.max} · mean ${a.range.mean.toFixed(2)}` : ''}`),
538
- ...(c.delta !== null ? [` delta ${c.delta.toFixed(2)} (absolute units)`] : []),
539
- ].join('\n');
540
- return { json: c, text, code: 0 };
541
- }
542
- const events = readLedger(root, parseSince(flagString(flags, 'since')));
543
- const text = events.map((e) => `${e.at} ${e.event.padEnd(9)} ${e.client}${e.model ? `/${e.model}` : ''}${e.run ? ` ${e.run}` : ''}${typeof e.passed === 'number' ? ` pass ${e.passed} fail ${e.failed ?? 0}` : ''}${e.detail ? ` ${e.detail}` : ''}`).join('\n');
544
- return { json: events, text: text || 'ledger is empty', code: 0 };
545
- }
546
- // ─── Dispatch ───────────────────────────────────────────────────────
547
- function packageVersion() {
548
- return readJson(path.join(path.dirname(new URL(import.meta.url).pathname), '..', 'package.json'))?.version ?? 'unknown';
549
- }
9
+ import { usage, VibeError } from './core/errors.js';
10
+ import { findProjectRoot } from './core/paths.js';
11
+ import { ensureGlobal, globalStatus } from './install/global.js';
12
+ import { flagString, HELP, packageVersion, parseArgs } from './cli/common.js';
13
+ import { cmdAsk, cmdAuthorize, cmdInbox } from './cli/human.js';
14
+ import { cmdKnowledge, cmdLedger, cmdRegress, cmdResearch, cmdSkill } from './cli/memory.js';
15
+ import { cmdPlugin, cmdStatus, cmdTokens, cmdUninstall, cmdUpdate } from './cli/setup.js';
16
+ import { cmdAbandon, cmdApprove, cmdCheck, cmdEvidence, cmdIntent, cmdProfile, cmdRead, cmdSize, cmdState } from './cli/work.js';
17
+ export { parseArgs } from './cli/common.js';
18
+ const COMMANDS = {
19
+ status: (root, _s, _r, _t, flags) => cmdStatus(root, flags),
20
+ update: (_root, _s, _r, _t, flags) => cmdUpdate(flags),
21
+ tokens: (root, sub) => cmdTokens(root, sub),
22
+ uninstall: (root, _s, _r, _t, flags) => cmdUninstall(root, flags),
23
+ plugin: (_root, sub, _r, _t, flags) => cmdPlugin(sub, flags),
24
+ state: (root, _s, _r, _t, flags) => cmdState(root, flags),
25
+ profile: (root, sub, _r, _t, flags) => cmdProfile(root, sub, flags),
26
+ read: (root, sub, _r, _t, flags) => cmdRead(root, sub, flags),
27
+ size: (root, _s, _r, tail, flags) => cmdSize(root, tail, flags),
28
+ intent: (root, sub, rest, _t, flags) => cmdIntent(root, sub, rest, flags),
29
+ approve: (root, _s, _r, tail) => cmdApprove(root, tail),
30
+ check: (root, _s, _r, tail, flags) => cmdCheck(root, tail, flags),
31
+ evidence: (root, _s, _r, tail) => cmdEvidence(root, tail),
32
+ abandon: (root, _s, _r, _t, flags) => cmdAbandon(root, flags),
33
+ ask: (root, _s, _r, tail, flags) => cmdAsk(root, tail, flags),
34
+ authorize: (root, _s, _r, tail, flags) => cmdAuthorize(root, tail, flags),
35
+ inbox: (root, sub, rest) => cmdInbox(root, sub, rest),
36
+ regress: (root, sub, _r, _t, flags) => cmdRegress(root, sub, flags),
37
+ knowledge: (root, sub, rest, _t, flags) => cmdKnowledge(root, sub, rest, flags),
38
+ ledger: (root, sub, rest, _t, flags) => cmdLedger(root, sub, rest, flags),
39
+ research: (root, sub, _r, _t, flags) => cmdResearch(root, sub, flags),
40
+ skill: (root, sub, rest, _t, flags) => cmdSkill(root, sub, rest, flags),
41
+ };
550
42
  export async function dispatch(argv) {
551
43
  const { positionals, flags } = parseArgs(argv);
552
44
  const [cmd, sub, ...rest] = positionals;
@@ -554,6 +46,9 @@ export async function dispatch(argv) {
554
46
  return { json: { version: packageVersion() }, text: packageVersion(), code: 0 };
555
47
  if (!cmd || flags['help'] === true)
556
48
  return { json: { help: HELP }, text: HELP, code: 0 };
49
+ const handler = COMMANDS[cmd];
50
+ if (!handler)
51
+ throw usage(`unknown command: ${cmd}\n${HELP}`);
557
52
  const repaired = process.env['VIBE_SKIP_SETUP'] || cmd === 'uninstall' ? [] : ensureGlobal(flagString(flags, 'home'));
558
53
  if (repaired.length > 0) {
559
54
  const modes = globalStatus(flagString(flags, 'home')).clients;
@@ -561,52 +56,7 @@ export async function dispatch(argv) {
561
56
  }
562
57
  const root = cmd === 'plugin' ? process.cwd() : findProjectRoot();
563
58
  const tail = [sub, ...rest].filter((s) => Boolean(s));
564
- switch (cmd) {
565
- case 'status':
566
- return cmdStatus(root, flags);
567
- case 'update':
568
- return cmdUpdate(flags);
569
- case 'tokens':
570
- return cmdTokens(root, sub);
571
- case 'uninstall':
572
- return cmdUninstall(root, flags);
573
- case 'plugin':
574
- return cmdPlugin(sub, flags);
575
- case 'state':
576
- return cmdState(root, flags);
577
- case 'profile':
578
- return cmdProfile(root, sub, flags);
579
- case 'read':
580
- return cmdRead(root, sub, flags);
581
- case 'intent':
582
- return cmdIntent(root, sub, rest, flags);
583
- case 'approve':
584
- return cmdApprove(root, tail);
585
- case 'check':
586
- return cmdCheck(root, tail, flags);
587
- case 'evidence':
588
- return cmdEvidence(root, tail);
589
- case 'abandon':
590
- return cmdAbandon(root, flags);
591
- case 'ask':
592
- return cmdAsk(root, tail, flags);
593
- case 'authorize':
594
- return cmdAuthorize(root, tail, flags);
595
- case 'inbox':
596
- return cmdInbox(root, sub, rest);
597
- case 'regress':
598
- return cmdRegress(root, sub, flags);
599
- case 'knowledge':
600
- return cmdKnowledge(root, sub, rest, flags);
601
- case 'ledger':
602
- return cmdLedger(root, sub, rest, flags);
603
- case 'research':
604
- return cmdResearch(root, sub, flags);
605
- case 'skill':
606
- return cmdSkill(root, sub, rest, flags);
607
- default:
608
- throw usage(`unknown command: ${cmd}\n${HELP}`);
609
- }
59
+ return handler(root, sub, rest, tail, flags);
610
60
  }
611
61
  async function main() {
612
62
  const argv = process.argv.slice(2);