@djolex999/vir-cli 0.7.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -4
- package/dist/cli/reconcile.js +223 -0
- package/dist/cli/reconcile.js.map +1 -0
- package/dist/cli/runAction.js +19 -0
- package/dist/cli/runAction.js.map +1 -0
- package/dist/cli.js +167 -48
- package/dist/cli.js.map +1 -1
- package/dist/config.js +3 -0
- package/dist/config.js.map +1 -1
- package/dist/output/json.js +12 -4
- package/dist/output/json.js.map +1 -1
- package/dist/pipeline/composer.js +208 -0
- package/dist/pipeline/composer.js.map +1 -0
- package/dist/pipeline/distiller.js +48 -4
- package/dist/pipeline/distiller.js.map +1 -1
- package/dist/pipeline/writer.js +79 -1
- package/dist/pipeline/writer.js.map +1 -1
- package/dist/state/db.js +114 -0
- package/dist/state/db.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -5,7 +5,7 @@ import select from "@inquirer/select";
|
|
|
5
5
|
import chalk from "chalk";
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
import { createInterface } from "node:readline/promises";
|
|
8
|
-
import { stdin, stdout, argv
|
|
8
|
+
import { stdin, stdout, argv } from "node:process";
|
|
9
9
|
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
10
10
|
import { homedir } from "node:os";
|
|
11
11
|
import { basename, dirname, join } from "node:path";
|
|
@@ -21,7 +21,9 @@ import { parseSession } from "./pipeline/parser.js";
|
|
|
21
21
|
import { scoreSession } from "./pipeline/filter.js";
|
|
22
22
|
import { scrub } from "./pipeline/scrubber.js";
|
|
23
23
|
import { filterToolCalls } from "./pipeline/toolCallFilter.js";
|
|
24
|
-
import { Distiller } from "./pipeline/distiller.js";
|
|
24
|
+
import { Distiller, normalizeModelName, resolveModelShorthand, } from "./pipeline/distiller.js";
|
|
25
|
+
import { composeFromSources, estimateComposeCostTokens, gatherSources, } from "./pipeline/composer.js";
|
|
26
|
+
import { computeCost } from "./cost/pricing.js";
|
|
25
27
|
import { summarizeAll, summarizeProject } from "./pipeline/summarizer.js";
|
|
26
28
|
import { embeddingForNote, isOllamaAvailable, } from "./search/embedder.js";
|
|
27
29
|
import { search, vaultRoot } from "./search/retriever.js";
|
|
@@ -29,6 +31,8 @@ import { buildQueryResults, errorPayload } from "./output/json.js";
|
|
|
29
31
|
import { synthesize } from "./search/synthesizer.js";
|
|
30
32
|
import { runMcpServer } from "./mcp/server.js";
|
|
31
33
|
import { runReview } from "./cli/review.js";
|
|
34
|
+
import { runAction } from "./cli/runAction.js";
|
|
35
|
+
import { runReconcile } from "./cli/reconcile.js";
|
|
32
36
|
import { installToClaudeCode, isClaudeAvailable, isInstalled, uninstallFromClaudeCode, } from "./mcp/install.js";
|
|
33
37
|
import { install as installDaemon, status as daemonStatus, uninstall as uninstallDaemon, } from "./daemon/index.js";
|
|
34
38
|
import { StateDb } from "./state/db.js";
|
|
@@ -49,9 +53,9 @@ program
|
|
|
49
53
|
program
|
|
50
54
|
.command("init")
|
|
51
55
|
.description("Interactive setup")
|
|
52
|
-
.action(async () => {
|
|
56
|
+
.action(runAction(async () => {
|
|
53
57
|
await cmdInit();
|
|
54
|
-
});
|
|
58
|
+
}));
|
|
55
59
|
program
|
|
56
60
|
.command("run")
|
|
57
61
|
.description("Run pipeline once")
|
|
@@ -62,7 +66,7 @@ program
|
|
|
62
66
|
.option("--yes", "Skip the cost confirmation prompt")
|
|
63
67
|
.option("--force-model <model>", "Override the distill model for this run only: haiku | sonnet")
|
|
64
68
|
.option("--dry-run", "Estimate per-session cost after filtering, then exit before any LLM call")
|
|
65
|
-
.action(async (opts) => {
|
|
69
|
+
.action(runAction(async (opts) => {
|
|
66
70
|
const cfg = loadConfig();
|
|
67
71
|
const daemon = opts.daemon === true;
|
|
68
72
|
const rewriteOnly = opts.rewriteOnly === true;
|
|
@@ -70,10 +74,11 @@ program
|
|
|
70
74
|
const dryRun = opts.dryRun === true;
|
|
71
75
|
if (opts.forceModel && !["haiku", "sonnet"].includes(opts.forceModel)) {
|
|
72
76
|
console.error(chalk.red(`--force-model must be 'haiku' or 'sonnet', got '${opts.forceModel}'`));
|
|
73
|
-
|
|
77
|
+
process.exitCode = 1;
|
|
78
|
+
return;
|
|
74
79
|
}
|
|
75
80
|
const skipPrompt = opts.yes === true || daemon || rewriteOnly || articlesOnly || dryRun;
|
|
76
|
-
await runPipeline(cfg, {
|
|
81
|
+
const summary = await runPipeline(cfg, {
|
|
77
82
|
full: opts.full,
|
|
78
83
|
quiet: daemon,
|
|
79
84
|
logToFile: daemon,
|
|
@@ -85,7 +90,13 @@ program
|
|
|
85
90
|
? undefined
|
|
86
91
|
: async (newCount) => confirmCostIfNeeded(cfg, newCount),
|
|
87
92
|
});
|
|
88
|
-
|
|
93
|
+
// Surface per-session distill failures via a non-zero exit so external
|
|
94
|
+
// callers (and the user) don't get false "success" — the silent-success
|
|
95
|
+
// bug that hid Kie's 200-with-error responses pre-0.7.2.
|
|
96
|
+
if (summary.errored > 0 || summary.articlesErrored > 0) {
|
|
97
|
+
process.exitCode = 1;
|
|
98
|
+
}
|
|
99
|
+
}));
|
|
89
100
|
async function confirmCostIfNeeded(cfg, newCount) {
|
|
90
101
|
if (newCount <= 20)
|
|
91
102
|
return true;
|
|
@@ -108,7 +119,7 @@ program
|
|
|
108
119
|
.option("--since <duration>", "Time window, e.g. 7d, 24h, 2w", "7d")
|
|
109
120
|
.option("--by-session", "Show the full per-session distribution")
|
|
110
121
|
.option("--top <n>", "How many top sessions to show (default 5)", "5")
|
|
111
|
-
.action((opts) => {
|
|
122
|
+
.action(runAction(async (opts) => {
|
|
112
123
|
ui.header("cost");
|
|
113
124
|
ui.blank();
|
|
114
125
|
const since = opts.since ?? "7d";
|
|
@@ -118,7 +129,8 @@ program
|
|
|
118
129
|
}
|
|
119
130
|
catch {
|
|
120
131
|
console.error(chalk.red(`invalid --since value: ${since}`));
|
|
121
|
-
|
|
132
|
+
process.exitCode = 1;
|
|
133
|
+
return;
|
|
122
134
|
}
|
|
123
135
|
const records = readCostLog(cutoffMs);
|
|
124
136
|
if (records.length === 0) {
|
|
@@ -146,22 +158,24 @@ program
|
|
|
146
158
|
const label = s.project ? `${s.project}/${id}` : id;
|
|
147
159
|
ui.line(` ${ui.dim(ui.BULLET)} ${ui.text(label.padEnd(42))} ${ui.dim(`${s.calls}×`)} ${ui.warn(ui.formatUsd(s.cost))}`);
|
|
148
160
|
}
|
|
149
|
-
});
|
|
161
|
+
}));
|
|
150
162
|
program
|
|
151
163
|
.command("calibrate <sessionId>")
|
|
152
164
|
.description("Distill ONE session to stdout for A/B model comparison — never writes vault or DB")
|
|
153
165
|
.option("--model <model>", "Distill model: haiku | sonnet", "sonnet")
|
|
154
|
-
.action(async (sessionId, opts) => {
|
|
166
|
+
.action(runAction(async (sessionId, opts) => {
|
|
155
167
|
const cfg = loadConfig();
|
|
156
168
|
const model = opts.model ?? "sonnet";
|
|
157
169
|
if (!["haiku", "sonnet"].includes(model)) {
|
|
158
170
|
console.error(chalk.red(`--model must be 'haiku' or 'sonnet', got '${model}'`));
|
|
159
|
-
|
|
171
|
+
process.exitCode = 1;
|
|
172
|
+
return;
|
|
160
173
|
}
|
|
161
174
|
const found = scanSessions(cfg.claudeProjectsDir).find((s) => basename(s.path, ".jsonl") === sessionId);
|
|
162
175
|
if (!found) {
|
|
163
176
|
console.error(chalk.red(`session not found under ${cfg.claudeProjectsDir}: ${sessionId}`));
|
|
164
|
-
|
|
177
|
+
process.exitCode = 1;
|
|
178
|
+
return;
|
|
165
179
|
}
|
|
166
180
|
// Same pipeline as production up to (but NOT including) writer.write / db.record.
|
|
167
181
|
// classify always runs on Haiku (matches production); only distill varies.
|
|
@@ -190,23 +204,23 @@ program
|
|
|
190
204
|
else {
|
|
191
205
|
console.log(`\n## cost\n(no distill cost record found for ${sessionId})`);
|
|
192
206
|
}
|
|
193
|
-
});
|
|
207
|
+
}));
|
|
194
208
|
const schedule = program
|
|
195
209
|
.command("schedule")
|
|
196
210
|
.description("Manage the background daemon (launchd / systemd / cron)");
|
|
197
211
|
schedule
|
|
198
212
|
.command("install")
|
|
199
213
|
.description("Install + start the scheduled daemon")
|
|
200
|
-
.action(async () => {
|
|
214
|
+
.action(runAction(async () => {
|
|
201
215
|
const cfg = loadConfig();
|
|
202
216
|
await installDaemon(cfg);
|
|
203
217
|
const ds = await daemonStatus();
|
|
204
218
|
console.log(chalk.green(`installed via ${ds.method}: ${ds.configPath ?? "(no path)"} (active=${ds.active})`));
|
|
205
|
-
});
|
|
219
|
+
}));
|
|
206
220
|
schedule
|
|
207
221
|
.command("uninstall")
|
|
208
222
|
.description("Stop + remove the scheduled daemon")
|
|
209
|
-
.action(async () => {
|
|
223
|
+
.action(runAction(async () => {
|
|
210
224
|
const before = await daemonStatus();
|
|
211
225
|
await uninstallDaemon();
|
|
212
226
|
if (before.installed) {
|
|
@@ -215,14 +229,14 @@ schedule
|
|
|
215
229
|
else {
|
|
216
230
|
console.log(chalk.yellow("no vir daemon found"));
|
|
217
231
|
}
|
|
218
|
-
});
|
|
232
|
+
}));
|
|
219
233
|
program
|
|
220
234
|
.command("sync-claude [project]")
|
|
221
235
|
.description("Update Vir blocks in CLAUDE.md files (global + per-project)")
|
|
222
236
|
.option("--dry-run", "Show diff only, never write")
|
|
223
237
|
.option("--force", "Apply without confirmation")
|
|
224
238
|
.option("--global", "Only update ~/.claude/CLAUDE.md")
|
|
225
|
-
.action(async (projectArg, opts) => {
|
|
239
|
+
.action(runAction(async (projectArg, opts) => {
|
|
226
240
|
const cfg = loadConfig();
|
|
227
241
|
const db = new StateDb();
|
|
228
242
|
try {
|
|
@@ -269,11 +283,11 @@ program
|
|
|
269
283
|
finally {
|
|
270
284
|
db.close();
|
|
271
285
|
}
|
|
272
|
-
});
|
|
286
|
+
}));
|
|
273
287
|
program
|
|
274
288
|
.command("dedupe")
|
|
275
289
|
.description("Interactive duplicate detection + merge")
|
|
276
|
-
.action(async () => {
|
|
290
|
+
.action(runAction(async () => {
|
|
277
291
|
const cfg = loadConfig();
|
|
278
292
|
const db = new StateDb();
|
|
279
293
|
try {
|
|
@@ -339,14 +353,14 @@ program
|
|
|
339
353
|
finally {
|
|
340
354
|
db.close();
|
|
341
355
|
}
|
|
342
|
-
});
|
|
356
|
+
}));
|
|
343
357
|
program
|
|
344
358
|
.command("lint")
|
|
345
359
|
.description("Run orphan, staleness, and contradiction checks on the vault")
|
|
346
360
|
.option("--orphans", "Run only the orphan check (free)")
|
|
347
361
|
.option("--stale", "Run only the staleness check (free)")
|
|
348
362
|
.option("--contradictions", "Run only the contradiction check (Haiku tokens)")
|
|
349
|
-
.action(async (opts) => {
|
|
363
|
+
.action(runAction(async (opts) => {
|
|
350
364
|
const cfg = loadConfig();
|
|
351
365
|
const db = new StateDb();
|
|
352
366
|
try {
|
|
@@ -429,12 +443,12 @@ program
|
|
|
429
443
|
finally {
|
|
430
444
|
db.close();
|
|
431
445
|
}
|
|
432
|
-
});
|
|
446
|
+
}));
|
|
433
447
|
program
|
|
434
448
|
.command("summarize [project]")
|
|
435
449
|
.description("Generate or regenerate a project knowledge summary")
|
|
436
450
|
.option("--all", "Regenerate summaries for every project with notes")
|
|
437
|
-
.action(async (project, opts) => {
|
|
451
|
+
.action(runAction(async (project, opts) => {
|
|
438
452
|
const cfg = loadConfig();
|
|
439
453
|
const db = new StateDb();
|
|
440
454
|
try {
|
|
@@ -452,7 +466,8 @@ program
|
|
|
452
466
|
}
|
|
453
467
|
if (!project) {
|
|
454
468
|
console.error(chalk.red("usage: vir summarize <project> | --all"));
|
|
455
|
-
|
|
469
|
+
process.exitCode = 1;
|
|
470
|
+
return;
|
|
456
471
|
}
|
|
457
472
|
const res = await summarizeProject(cfg, project, db);
|
|
458
473
|
if (!res) {
|
|
@@ -465,12 +480,12 @@ program
|
|
|
465
480
|
finally {
|
|
466
481
|
db.close();
|
|
467
482
|
}
|
|
468
|
-
});
|
|
483
|
+
}));
|
|
469
484
|
program
|
|
470
485
|
.command("embed")
|
|
471
486
|
.description("Generate Ollama embeddings for distilled notes")
|
|
472
487
|
.option("--force", "Regenerate even if embedding already exists")
|
|
473
|
-
.action(async (opts) => {
|
|
488
|
+
.action(runAction(async (opts) => {
|
|
474
489
|
const cfg = loadConfig();
|
|
475
490
|
ui.header("embed");
|
|
476
491
|
ui.blank();
|
|
@@ -479,7 +494,8 @@ program
|
|
|
479
494
|
ui.line(ui.dim(" brew install ollama"));
|
|
480
495
|
ui.line(ui.dim(" ollama pull nomic-embed-text"));
|
|
481
496
|
ui.line(ui.dim(" ollama serve"));
|
|
482
|
-
|
|
497
|
+
process.exitCode = 1;
|
|
498
|
+
return;
|
|
483
499
|
}
|
|
484
500
|
const db = new StateDb();
|
|
485
501
|
try {
|
|
@@ -530,7 +546,7 @@ program
|
|
|
530
546
|
finally {
|
|
531
547
|
db.close();
|
|
532
548
|
}
|
|
533
|
-
});
|
|
549
|
+
}));
|
|
534
550
|
// JSON path for `vir query --json`: stdout gets a single JSON array on success
|
|
535
551
|
// (`[]` when nothing matched), exit 0. On failure stdout stays EMPTY so the
|
|
536
552
|
// plugin can `JSON.parse(stdout)` unguarded — the error goes to stderr as a
|
|
@@ -570,7 +586,7 @@ program
|
|
|
570
586
|
.description("Search the vault: embedding/TF-IDF retrieval + Claude synthesis")
|
|
571
587
|
.option("--json", "Emit machine-readable JSON for programmatic consumers")
|
|
572
588
|
.option("--limit <n>", "Number of notes to retrieve", "8")
|
|
573
|
-
.action(async (question, opts) => {
|
|
589
|
+
.action(runAction(async (question, opts) => {
|
|
574
590
|
const limit = Math.max(1, Number.parseInt(opts.limit ?? "8", 10) || 8);
|
|
575
591
|
if (opts.json) {
|
|
576
592
|
await runQueryJson(question, limit);
|
|
@@ -623,11 +639,100 @@ program
|
|
|
623
639
|
finally {
|
|
624
640
|
db.close();
|
|
625
641
|
}
|
|
626
|
-
});
|
|
642
|
+
}));
|
|
643
|
+
program
|
|
644
|
+
.command("compose <topic>")
|
|
645
|
+
.description("Synthesize a topic page from related vault notes")
|
|
646
|
+
.option("--limit <n>", "Top N notes to synthesize from (max 50)", "20")
|
|
647
|
+
.option("--model <model>", "Synthesis model: haiku | sonnet")
|
|
648
|
+
.option("--dry-run", "Show top sources + estimated cost, exit before LLM")
|
|
649
|
+
.option("--yes", "Skip the cost confirmation prompt")
|
|
650
|
+
.action(runAction(async (topic, opts) => {
|
|
651
|
+
const cfg = loadConfig();
|
|
652
|
+
if (opts.model && !["haiku", "sonnet"].includes(opts.model)) {
|
|
653
|
+
console.error(chalk.red(`--model must be 'haiku' or 'sonnet', got '${opts.model}'`));
|
|
654
|
+
process.exitCode = 1;
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
const limit = Math.min(50, Math.max(1, Number.parseInt(opts.limit ?? "20", 10) || 20));
|
|
658
|
+
const db = new StateDb();
|
|
659
|
+
try {
|
|
660
|
+
ui.header("compose");
|
|
661
|
+
ui.divider();
|
|
662
|
+
console.log(ui.text(topic));
|
|
663
|
+
ui.divider();
|
|
664
|
+
ui.blank();
|
|
665
|
+
const sp = ui.spinner("searching vault for related notes").start();
|
|
666
|
+
const sources = await gatherSources(cfg, db, topic, limit);
|
|
667
|
+
sp.stop();
|
|
668
|
+
if (sources.length === 0) {
|
|
669
|
+
ui.row(ui.warn(ui.WARN_GLYPH), ui.text("no related notes found — nothing to synthesize"));
|
|
670
|
+
ui.line(ui.dim(" run `vir run` to distill more sessions first"));
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
for (const s of sources.slice(0, 10))
|
|
674
|
+
ui.sourceRow(s.title, s.score);
|
|
675
|
+
ui.divider();
|
|
676
|
+
const model = normalizeModelName(resolveModelShorthand(opts.model ?? cfg.models.distill), cfg.provider);
|
|
677
|
+
const { inputTokens, outputTokens } = estimateComposeCostTokens(topic, sources);
|
|
678
|
+
const estCost = computeCost(cfg.provider, model, inputTokens, outputTokens, cfg.pricing);
|
|
679
|
+
ui.summary({
|
|
680
|
+
sources: { value: sources.length, color: ui.info },
|
|
681
|
+
model: { value: model, color: ui.accent },
|
|
682
|
+
"est. cost": { value: ui.formatUsd(estCost), color: ui.warn },
|
|
683
|
+
});
|
|
684
|
+
ui.divider();
|
|
685
|
+
if (opts.dryRun) {
|
|
686
|
+
ui.line(ui.dim(" dry run — no synthesis performed; actuals may vary ±30%"));
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
if (opts.yes !== true) {
|
|
690
|
+
const proceed = await confirm({
|
|
691
|
+
message: `synthesize with ${model} (~${ui.formatUsd(estCost)})?`,
|
|
692
|
+
default: true,
|
|
693
|
+
});
|
|
694
|
+
if (!proceed) {
|
|
695
|
+
ui.line(ui.dim("aborted"));
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
const writer = new VaultWriter(cfg, db);
|
|
700
|
+
const sp2 = ui.spinner("synthesizing topic page").start();
|
|
701
|
+
let result;
|
|
702
|
+
try {
|
|
703
|
+
result = await composeFromSources(cfg, db, topic, sources, writer, {
|
|
704
|
+
forceModel: opts.model,
|
|
705
|
+
});
|
|
706
|
+
sp2.stop();
|
|
707
|
+
}
|
|
708
|
+
catch (err) {
|
|
709
|
+
sp2.fail(ui.errorColor(err.message));
|
|
710
|
+
process.exitCode = 1;
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
ui.row(ui.success(ui.CHECK), ui.text(`wrote ${result.relPath}`));
|
|
714
|
+
ui.blank();
|
|
715
|
+
// Actual cost from the record callLLM just appended for this compose.
|
|
716
|
+
const rec = [...readCostLog()]
|
|
717
|
+
.reverse()
|
|
718
|
+
.find((r) => r.stage === "compose" && r.session === result.slug);
|
|
719
|
+
ui.summary({
|
|
720
|
+
title: { value: result.title, color: ui.text },
|
|
721
|
+
sources: { value: result.sourceCount, color: ui.info },
|
|
722
|
+
confidence: { value: result.confidence.toFixed(2), color: ui.info },
|
|
723
|
+
...(rec
|
|
724
|
+
? { cost: { value: ui.formatUsd(rec.estimated_cost_usd), color: ui.warn } }
|
|
725
|
+
: {}),
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
finally {
|
|
729
|
+
db.close();
|
|
730
|
+
}
|
|
731
|
+
}));
|
|
627
732
|
program
|
|
628
733
|
.command("status")
|
|
629
734
|
.description("Show processing status + knowledge base breakdown")
|
|
630
|
-
.action(async () => {
|
|
735
|
+
.action(runAction(async () => {
|
|
631
736
|
const cfg = configExists() ? loadConfig() : null;
|
|
632
737
|
if (!cfg) {
|
|
633
738
|
ui.header("status");
|
|
@@ -643,25 +748,34 @@ program
|
|
|
643
748
|
renderKnowledge(knowledge);
|
|
644
749
|
ui.blank();
|
|
645
750
|
renderDaemon(ds, cfg.cadenceHours);
|
|
646
|
-
});
|
|
751
|
+
}));
|
|
752
|
+
program
|
|
753
|
+
.command("reconcile")
|
|
754
|
+
.description("Retry sessions that silently failed pre-0.7.2 (null/empty content despite skipped=0)")
|
|
755
|
+
.option("--dry-run", "Report recoverable count + estimated cost + false-cost collateral; exit before any LLM call")
|
|
756
|
+
.option("--yes", "Skip the cost confirmation prompt")
|
|
757
|
+
.action(runAction(async (opts) => {
|
|
758
|
+
const cfg = loadConfig();
|
|
759
|
+
await runReconcile(cfg, { dryRun: opts.dryRun, yes: opts.yes });
|
|
760
|
+
}));
|
|
647
761
|
program
|
|
648
762
|
.command("review")
|
|
649
763
|
.description("Walk through new distilled notes and approve/edit/reject")
|
|
650
764
|
.option("--all", "Review all notes, including verified ones")
|
|
651
765
|
.option("--project <slug>", "Filter by project")
|
|
652
766
|
.option("--limit <n>", "Max notes to review in this session", "50")
|
|
653
|
-
.action(runReview);
|
|
767
|
+
.action(runAction(runReview));
|
|
654
768
|
program
|
|
655
769
|
.command("doctor")
|
|
656
770
|
.description("Run diagnostic checks on Vir installation")
|
|
657
771
|
.option("--json", "Emit machine-readable JSON for programmatic consumers")
|
|
658
|
-
.action(async (opts) => {
|
|
772
|
+
.action(runAction(async (opts) => {
|
|
659
773
|
if (opts.json) {
|
|
660
774
|
await runDoctorJson();
|
|
661
775
|
return;
|
|
662
776
|
}
|
|
663
777
|
await runDoctor();
|
|
664
|
-
});
|
|
778
|
+
}));
|
|
665
779
|
const mcpCmd = program
|
|
666
780
|
.command("mcp")
|
|
667
781
|
.description("MCP server + Claude Code registration")
|
|
@@ -685,35 +799,35 @@ const runMcp = async () => {
|
|
|
685
799
|
mcpCmd
|
|
686
800
|
.command("run")
|
|
687
801
|
.description("Run the MCP server over stdio")
|
|
688
|
-
.action(runMcp);
|
|
802
|
+
.action(runAction(runMcp));
|
|
689
803
|
mcpCmd
|
|
690
804
|
.command("install")
|
|
691
805
|
.description("Register Vir with Claude Code")
|
|
692
806
|
.option("--scope <scope>", "user or project", "user")
|
|
693
|
-
.action(async (opts) => {
|
|
807
|
+
.action(runAction(async (opts) => {
|
|
694
808
|
await installToClaudeCode(opts.scope);
|
|
695
|
-
});
|
|
809
|
+
}));
|
|
696
810
|
mcpCmd
|
|
697
811
|
.command("uninstall")
|
|
698
812
|
.description("Unregister Vir from Claude Code")
|
|
699
|
-
.action(async () => {
|
|
813
|
+
.action(runAction(async () => {
|
|
700
814
|
await uninstallFromClaudeCode();
|
|
701
|
-
});
|
|
815
|
+
}));
|
|
702
816
|
mcpCmd
|
|
703
817
|
.command("status")
|
|
704
818
|
.description("Check Vir MCP registration")
|
|
705
|
-
.action(async () => {
|
|
819
|
+
.action(runAction(async () => {
|
|
706
820
|
if (!(await isClaudeAvailable())) {
|
|
707
821
|
ui.row(ui.warn(ui.WARN_GLYPH), ui.text("claude CLI not detected"), "install: https://claude.com/claude-code");
|
|
708
822
|
return;
|
|
709
823
|
}
|
|
710
824
|
const installed = await isInstalled();
|
|
711
825
|
ui.row(installed ? ui.success(ui.CHECK) : ui.errorColor(ui.CROSS), ui.text(installed ? "registered with Claude Code" : "not registered"), installed ? undefined : "run: vir mcp install");
|
|
712
|
-
});
|
|
826
|
+
}));
|
|
713
827
|
// Backwards compat: `vir mcp` with no subcommand runs the server. The MCP
|
|
714
828
|
// registration (`claude mcp add vir vir mcp`) invokes exactly this, so it must
|
|
715
829
|
// keep launching the stdio server — don't change it to print help.
|
|
716
|
-
mcpCmd.action(runMcp);
|
|
830
|
+
mcpCmd.action(runAction(runMcp));
|
|
717
831
|
function renderKnowledge(k) {
|
|
718
832
|
if (k.total === 0) {
|
|
719
833
|
ui.box([
|
|
@@ -994,7 +1108,8 @@ async function cmdInit() {
|
|
|
994
1108
|
for (const issue of parsed.error.issues) {
|
|
995
1109
|
console.error(` - ${issue.path.join(".")}: ${issue.message}`);
|
|
996
1110
|
}
|
|
997
|
-
|
|
1111
|
+
process.exitCode = 1;
|
|
1112
|
+
return;
|
|
998
1113
|
}
|
|
999
1114
|
saveConfig(parsed.data);
|
|
1000
1115
|
ui.blank();
|
|
@@ -1056,8 +1171,12 @@ function safeLoad() {
|
|
|
1056
1171
|
return null;
|
|
1057
1172
|
}
|
|
1058
1173
|
}
|
|
1174
|
+
// Safety net for anything that escapes the per-action `runAction` wrapper (e.g.
|
|
1175
|
+
// commander-internal rejections before the action handler is reached). Set
|
|
1176
|
+
// `process.exitCode` instead of calling `process.exit` so buffered stdout/stderr
|
|
1177
|
+
// can drain before the process exits.
|
|
1059
1178
|
program.parseAsync(argv).catch((err) => {
|
|
1060
1179
|
console.error(chalk.red(err.message ?? String(err)));
|
|
1061
|
-
|
|
1180
|
+
process.exitCode = 1;
|
|
1062
1181
|
});
|
|
1063
1182
|
//# sourceMappingURL=cli.js.map
|