@djolex999/vir-cli 0.23.3 → 0.24.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 +3 -2
- package/dist/claude/updater.js +24 -4
- package/dist/claude/updater.js.map +1 -1
- package/dist/cli/audit.js +136 -0
- package/dist/cli/audit.js.map +1 -0
- package/dist/cli/compose.js +101 -0
- package/dist/cli/compose.js.map +1 -0
- package/dist/cli/costLabel.js +11 -0
- package/dist/cli/costLabel.js.map +1 -0
- package/dist/cli/dedupe.js +92 -0
- package/dist/cli/dedupe.js.map +1 -0
- package/dist/cli/embed.js +183 -0
- package/dist/cli/embed.js.map +1 -0
- package/dist/cli/guards.js +40 -0
- package/dist/cli/guards.js.map +1 -0
- package/dist/cli/init.js +361 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/lint.js +160 -0
- package/dist/cli/lint.js.map +1 -0
- package/dist/cli/reconcile.js +16 -53
- package/dist/cli/reconcile.js.map +1 -1
- package/dist/cli/review.js +14 -1
- package/dist/cli/review.js.map +1 -1
- package/dist/cli/summarize.js +148 -0
- package/dist/cli/summarize.js.map +1 -0
- package/dist/cli.js +21 -1096
- package/dist/cli.js.map +1 -1
- package/dist/mcp/server.js +40 -28
- package/dist/mcp/server.js.map +1 -1
- package/dist/pipeline/claudeCli.js +41 -5
- package/dist/pipeline/claudeCli.js.map +1 -1
- package/dist/pipeline/composer.js +1 -0
- package/dist/pipeline/composer.js.map +1 -1
- package/dist/pipeline/distillSession.js +67 -0
- package/dist/pipeline/distillSession.js.map +1 -0
- package/dist/pipeline/embeddingSweep.js +9 -5
- package/dist/pipeline/embeddingSweep.js.map +1 -1
- package/dist/pipeline/run.js +30 -63
- package/dist/pipeline/run.js.map +1 -1
- package/dist/pipeline/writer.js +83 -10
- package/dist/pipeline/writer.js.map +1 -1
- package/dist/state/db.js +8 -8
- package/dist/state/db.js.map +1 -1
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -1,26 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import confirm from "@inquirer/confirm";
|
|
3
|
-
import input from "@inquirer/input";
|
|
4
|
-
import select from "@inquirer/select";
|
|
5
2
|
import chalk from "chalk";
|
|
6
3
|
import { Command } from "commander";
|
|
7
4
|
import { createInterface } from "node:readline/promises";
|
|
8
5
|
import { stdin, stdout, argv } from "node:process";
|
|
9
|
-
import { existsSync,
|
|
6
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
10
7
|
import { homedir } from "node:os";
|
|
11
8
|
import { basename, dirname, join } from "node:path";
|
|
12
9
|
import { fileURLToPath } from "node:url";
|
|
13
|
-
import {
|
|
10
|
+
import { configExists, loadConfig } from "./config.js";
|
|
14
11
|
import { applyPlan, planUpdates } from "./claude/updater.js";
|
|
15
12
|
import { pruneCommand } from "./cli/pruneAction.js";
|
|
16
13
|
import { setupNotifications } from "./cli/notificationsSetup.js";
|
|
17
|
-
import { detectDuplicates } from "./dedupe/detector.js";
|
|
18
|
-
import { mergeNotes } from "./dedupe/merger.js";
|
|
19
|
-
import { contradictionCheck, orphanCheck, stalenessCheck, } from "./lint/linter.js";
|
|
20
|
-
import { legacyRelatedCheck, migrateLegacyRelated, } from "./lint/legacyRelated.js";
|
|
21
|
-
import { demoteStrays, strayFileCheck } from "./lint/strayFiles.js";
|
|
22
14
|
import { runPipeline } from "./pipeline/run.js";
|
|
23
|
-
import {
|
|
15
|
+
import { projectNameFor } from "./pipeline/projects.js";
|
|
24
16
|
import { persistProjectDecisions, promptProjectDecisions, } from "./cli/projectSelect.js";
|
|
25
17
|
import { gatherProjectsReport } from "./cli/projects.js";
|
|
26
18
|
import { scanSessions } from "./pipeline/scanner.js";
|
|
@@ -28,15 +20,7 @@ import { parseSession } from "./pipeline/parser.js";
|
|
|
28
20
|
import { scoreSession } from "./pipeline/filter.js";
|
|
29
21
|
import { scrub } from "./pipeline/scrubber.js";
|
|
30
22
|
import { filterToolCalls } from "./pipeline/toolCallFilter.js";
|
|
31
|
-
import { Distiller
|
|
32
|
-
import { applyAuditRejects, previewRejects } from "./audit/apply.js";
|
|
33
|
-
import { batchByProject } from "./audit/batch.js";
|
|
34
|
-
import { noteIsVerified, parseLimitOption, runAudit, selectAuditRows } from "./audit/run.js";
|
|
35
|
-
import { composeFromSources, estimateComposeCostTokens, gatherSources, } from "./pipeline/composer.js";
|
|
36
|
-
import { computeCost } from "./cost/pricing.js";
|
|
37
|
-
import { countByCategory, summarizeAll, summarizeProject, } from "./pipeline/summarizer.js";
|
|
38
|
-
import { buildPeriodPrompt, estimatePeriodCostTokens, periodLabel, periodRange, selectNotesInPeriod, summarizePeriod, } from "./pipeline/periodSummary.js";
|
|
39
|
-
import { embedNoteWithProvider, resolveEmbeddingProvider, } from "./search/provider.js";
|
|
23
|
+
import { Distiller } from "./pipeline/distiller.js";
|
|
40
24
|
import { acquireLock, LockHeldError, releaseLock } from "./pipeline/lock.js";
|
|
41
25
|
import { loadIndex, searchWithOutcome, vaultRoot } from "./search/retriever.js";
|
|
42
26
|
import { QUERY_LOG_PATH, readQueryLog, recordQueryEvent, } from "./search/queryLog.js";
|
|
@@ -47,9 +31,7 @@ import { runMcpServer } from "./mcp/server.js";
|
|
|
47
31
|
import { runReview } from "./cli/review.js";
|
|
48
32
|
import { runReviewJson } from "./cli/reviewJson.js";
|
|
49
33
|
import { runAction } from "./cli/runAction.js";
|
|
50
|
-
import { buildInitConfig } from "./cli/initConfig.js";
|
|
51
34
|
import { runReconcile } from "./cli/reconcile.js";
|
|
52
|
-
import { runEmbedSetup } from "./cli/embedSetup.js";
|
|
53
35
|
import { installToClaudeCode, isClaudeAvailable, isInstalled, uninstallFromClaudeCode, } from "./mcp/install.js";
|
|
54
36
|
import { install as installDaemon, status as daemonStatus, uninstall as uninstallDaemon, } from "./daemon/index.js";
|
|
55
37
|
import { StateDb } from "./state/db.js";
|
|
@@ -57,7 +39,15 @@ import { parseDuration, readCostLog } from "./cost/log.js";
|
|
|
57
39
|
import { buildReport } from "./cost/report.js";
|
|
58
40
|
import * as ui from "./ui/display.js";
|
|
59
41
|
import { VaultWriter } from "./pipeline/writer.js";
|
|
42
|
+
import { withPipelineLock } from "./cli/guards.js";
|
|
60
43
|
import { runDoctor, runDoctorJson } from "./diagnostics/doctor.js";
|
|
44
|
+
import { embedCommand } from "./cli/embed.js";
|
|
45
|
+
import { lintCommand } from "./cli/lint.js";
|
|
46
|
+
import { summarizeCommand } from "./cli/summarize.js";
|
|
47
|
+
import { auditCommand } from "./cli/audit.js";
|
|
48
|
+
import { composeCommand } from "./cli/compose.js";
|
|
49
|
+
import { dedupeCommand } from "./cli/dedupe.js";
|
|
50
|
+
import { cmdInit } from "./cli/init.js";
|
|
61
51
|
// Read version at runtime from package.json (one dir up from dist/cli.js) so
|
|
62
52
|
// `vir --version` never drifts from the published version. rootDir is ./src,
|
|
63
53
|
// so package.json can't be imported — read it instead.
|
|
@@ -168,14 +158,6 @@ program
|
|
|
168
158
|
process.exitCode = 1;
|
|
169
159
|
}
|
|
170
160
|
}));
|
|
171
|
-
// Dollar-estimate label for the ACTIVE provider. On the subscription path the
|
|
172
|
-
// honest label is quota, never "$0.00" — a zero would read as "free API call"
|
|
173
|
-
// and hide that the run consumes the user's Claude Code limits.
|
|
174
|
-
function estCostLabel(cfg, model, inputTokens, outputTokens) {
|
|
175
|
-
if (cfg.provider === "claude-cli")
|
|
176
|
-
return "subscription quota (no $)";
|
|
177
|
-
return ui.formatUsd(computeCost(cfg.provider, model, inputTokens, outputTokens, cfg.pricing, cfg.kieTopUpTier));
|
|
178
|
-
}
|
|
179
161
|
async function confirmCostIfNeeded(cfg, newCount) {
|
|
180
162
|
if (newCount <= 20)
|
|
181
163
|
return true;
|
|
@@ -420,73 +402,8 @@ program
|
|
|
420
402
|
program
|
|
421
403
|
.command("dedupe")
|
|
422
404
|
.description("Interactive duplicate detection + merge")
|
|
423
|
-
.
|
|
424
|
-
|
|
425
|
-
const db = new StateDb();
|
|
426
|
-
try {
|
|
427
|
-
console.log("scanning for duplicate candidates...");
|
|
428
|
-
const result = await detectDuplicates(cfg, db);
|
|
429
|
-
console.log(`${result.checked} candidate pairs checked, ${result.duplicates.length} flagged as duplicates`);
|
|
430
|
-
if (result.duplicates.length === 0) {
|
|
431
|
-
return;
|
|
432
|
-
}
|
|
433
|
-
const rl = createInterface({ input: stdin, output: stdout });
|
|
434
|
-
let merged = 0;
|
|
435
|
-
let skipped = 0;
|
|
436
|
-
for (const dup of result.duplicates) {
|
|
437
|
-
console.log("\nDuplicate found:");
|
|
438
|
-
console.log(`A: ${noteRefOf(dup.a)} (conf: ${dup.a.confidence.toFixed(2)}, ${dup.a.startedAt?.slice(0, 10) ?? "?"})`);
|
|
439
|
-
console.log(` "${preview(dup.a.content)}"`);
|
|
440
|
-
console.log(`B: ${noteRefOf(dup.b)} (conf: ${dup.b.confidence.toFixed(2)}, ${dup.b.startedAt?.slice(0, 10) ?? "?"})`);
|
|
441
|
-
console.log(` "${preview(dup.b.content)}"`);
|
|
442
|
-
console.log(`Reason: ${dup.reason}`);
|
|
443
|
-
const suggestion = dup.keepWhich === "merge"
|
|
444
|
-
? "merge both"
|
|
445
|
-
: `keep ${dup.keepWhich}`;
|
|
446
|
-
console.log(`Suggested: ${suggestion}`);
|
|
447
|
-
const ans = (await rl.question("[k]eep suggestion / [s]wap / [m]erge / [x] skip: "))
|
|
448
|
-
.trim()
|
|
449
|
-
.toLowerCase();
|
|
450
|
-
let action = null;
|
|
451
|
-
if (ans === "k" || ans === "") {
|
|
452
|
-
action = dup.keepWhich;
|
|
453
|
-
}
|
|
454
|
-
else if (ans === "s") {
|
|
455
|
-
action =
|
|
456
|
-
dup.keepWhich === "A"
|
|
457
|
-
? "B"
|
|
458
|
-
: dup.keepWhich === "B"
|
|
459
|
-
? "A"
|
|
460
|
-
: "merge";
|
|
461
|
-
}
|
|
462
|
-
else if (ans === "m") {
|
|
463
|
-
action = "merge";
|
|
464
|
-
}
|
|
465
|
-
else if (ans === "x") {
|
|
466
|
-
skipped += 1;
|
|
467
|
-
continue;
|
|
468
|
-
}
|
|
469
|
-
else {
|
|
470
|
-
console.log(chalk.yellow("unknown input — skipping"));
|
|
471
|
-
skipped += 1;
|
|
472
|
-
continue;
|
|
473
|
-
}
|
|
474
|
-
try {
|
|
475
|
-
const outcome = await mergeNotes(cfg, db, dup.a, dup.b, action);
|
|
476
|
-
merged += 1;
|
|
477
|
-
console.log(chalk.green(`merged (${outcome.action}): winner=${outcome.winnerPath} archived=${outcome.archivedPath}`));
|
|
478
|
-
}
|
|
479
|
-
catch (err) {
|
|
480
|
-
console.error(chalk.red(`merge failed: ${err.message}`));
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
rl.close();
|
|
484
|
-
console.log(`\n${result.duplicates.length} pairs reviewed, ${merged} merged, ${skipped} skipped.`);
|
|
485
|
-
}
|
|
486
|
-
finally {
|
|
487
|
-
db.close();
|
|
488
|
-
}
|
|
489
|
-
}));
|
|
405
|
+
.option("--yes", "Skip the cost confirmation prompt")
|
|
406
|
+
.action(runAction(dedupeCommand));
|
|
490
407
|
program
|
|
491
408
|
.command("lint")
|
|
492
409
|
.description("Run orphan, stray-file, legacy-Related, staleness, and contradiction checks on the vault")
|
|
@@ -496,156 +413,8 @@ program
|
|
|
496
413
|
.option("--fix", "Move retitle-duplicate strays into archived/ (other strays are left alone), and move legacy Related content into ## Details")
|
|
497
414
|
.option("--stale", "Run only the staleness check (free)")
|
|
498
415
|
.option("--contradictions", "Run only the contradiction check (Haiku tokens)")
|
|
499
|
-
.
|
|
500
|
-
|
|
501
|
-
const db = new StateDb();
|
|
502
|
-
try {
|
|
503
|
-
const runAll = !opts.orphans &&
|
|
504
|
-
!opts.strays &&
|
|
505
|
-
!opts.legacyRelated &&
|
|
506
|
-
!opts.stale &&
|
|
507
|
-
!opts.contradictions;
|
|
508
|
-
const checks = [];
|
|
509
|
-
if (runAll || opts.orphans)
|
|
510
|
-
checks.push("orphans");
|
|
511
|
-
if (runAll || opts.strays)
|
|
512
|
-
checks.push("strays");
|
|
513
|
-
if (runAll || opts.legacyRelated)
|
|
514
|
-
checks.push("legacy-related");
|
|
515
|
-
if (runAll || opts.stale)
|
|
516
|
-
checks.push("stale");
|
|
517
|
-
if (runAll || opts.contradictions)
|
|
518
|
-
checks.push("contradictions");
|
|
519
|
-
ui.header("lint");
|
|
520
|
-
ui.blank();
|
|
521
|
-
let orphanCount = 0;
|
|
522
|
-
let strayCount = 0;
|
|
523
|
-
let legacyCount = 0;
|
|
524
|
-
let staleCount = 0;
|
|
525
|
-
let contradictionCount = 0;
|
|
526
|
-
let issues = 0;
|
|
527
|
-
if (runAll || opts.orphans) {
|
|
528
|
-
const sp = ui.spinner("checking orphans").start();
|
|
529
|
-
const r = orphanCheck(cfg);
|
|
530
|
-
sp.stop();
|
|
531
|
-
orphanCount = r.orphans.length;
|
|
532
|
-
issues += orphanCount;
|
|
533
|
-
if (orphanCount === 0) {
|
|
534
|
-
ui.row(ui.success(ui.CHECK), `${ui.text("orphans")} ${ui.dim("none")}`);
|
|
535
|
-
}
|
|
536
|
-
else {
|
|
537
|
-
ui.row(ui.errorColor(ui.CROSS), `${ui.text("orphans")} ${ui.dim("(" + orphanCount + ")")}`);
|
|
538
|
-
for (const o of r.orphans) {
|
|
539
|
-
console.log(` ${ui.dim(ui.BULLET)} ${ui.text(ui.shortNotePath(o))}`);
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
if (runAll || opts.strays) {
|
|
544
|
-
const sp = ui.spinner("checking stray files").start();
|
|
545
|
-
const r = strayFileCheck(cfg, db);
|
|
546
|
-
sp.stop();
|
|
547
|
-
strayCount = r.strays.length;
|
|
548
|
-
issues += strayCount;
|
|
549
|
-
if (strayCount === 0) {
|
|
550
|
-
ui.row(ui.success(ui.CHECK), `${ui.text("strays")} ${ui.dim("none")}`);
|
|
551
|
-
}
|
|
552
|
-
else {
|
|
553
|
-
ui.row(ui.errorColor(ui.CROSS), `${ui.text("strays")} ${ui.dim("(" + strayCount + " of " + r.scanned + " files)")}`);
|
|
554
|
-
for (const st of r.strays) {
|
|
555
|
-
// The sibling is the whole point: it is what makes a stray safe
|
|
556
|
-
// to demote. An `unknown` stray has none, and gets no advice.
|
|
557
|
-
const note = st.liveSibling !== null
|
|
558
|
-
? ui.dim(`${ui.ARROW} live: ${st.liveSibling}`)
|
|
559
|
-
: ui.warn("only copy — inspect before removing");
|
|
560
|
-
console.log(` ${ui.dim(ui.BULLET)} ${ui.text(ui.shortNotePath(st.relPath))} ${ui.muted(st.kind)} ${note}`);
|
|
561
|
-
}
|
|
562
|
-
if (opts.fix) {
|
|
563
|
-
const fixed = demoteStrays(cfg, r);
|
|
564
|
-
strayCount -= fixed.moved;
|
|
565
|
-
issues -= fixed.moved;
|
|
566
|
-
ui.row(ui.success(ui.CHECK), `${ui.text("strays")} ${ui.dim(`moved ${fixed.moved} to archived/, left ${fixed.left} untouched`)}`);
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
if (runAll || opts.legacyRelated) {
|
|
571
|
-
const sp = ui.spinner("checking legacy Related sections").start();
|
|
572
|
-
const r = legacyRelatedCheck(db);
|
|
573
|
-
sp.stop();
|
|
574
|
-
const withContent = r.rows.filter((x) => x.kept > 0);
|
|
575
|
-
legacyCount = r.rows.length;
|
|
576
|
-
issues += legacyCount;
|
|
577
|
-
if (legacyCount === 0) {
|
|
578
|
-
ui.row(ui.success(ui.CHECK), `${ui.text("legacy-related")} ${ui.dim("none")}`);
|
|
579
|
-
}
|
|
580
|
-
else {
|
|
581
|
-
const bullets = withContent.reduce((n, x) => n + x.kept, 0);
|
|
582
|
-
ui.row(ui.errorColor(ui.CROSS), `${ui.text("legacy-related")} ${ui.dim(`(${legacyCount} of ${r.scanned} stored notes; ${withContent.length} hold ${bullets} content bullets a rewrite drops)`)}`);
|
|
583
|
-
if (opts.fix) {
|
|
584
|
-
const fixed = await migrateLegacyRelated(db, new VaultWriter(cfg, db));
|
|
585
|
-
legacyCount -= fixed.migrated;
|
|
586
|
-
issues -= fixed.migrated;
|
|
587
|
-
ui.row(ui.success(ui.CHECK), `${ui.text("legacy-related")} ${ui.dim(`moved content into ## Details on ${fixed.migrated} rows, re-rendered ${fixed.rewritten} notes`)}`);
|
|
588
|
-
for (const e of fixed.errors) {
|
|
589
|
-
console.log(` ${ui.dim(ui.BULLET)} ${ui.errorColor(e.path)} ${ui.muted(e.message)}`);
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
else {
|
|
593
|
-
console.log(` ${ui.dim(ui.ARROW)} ${ui.muted("vir lint --legacy-related --fix")}`);
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
if (runAll || opts.stale) {
|
|
598
|
-
const sp = ui.spinner("checking staleness").start();
|
|
599
|
-
const stale = stalenessCheck(cfg, db);
|
|
600
|
-
sp.stop();
|
|
601
|
-
staleCount = stale.length;
|
|
602
|
-
issues += staleCount;
|
|
603
|
-
if (staleCount === 0) {
|
|
604
|
-
ui.row(ui.success(ui.CHECK), `${ui.text("stale")} ${ui.dim("none")}`);
|
|
605
|
-
}
|
|
606
|
-
else {
|
|
607
|
-
ui.row(ui.errorColor(ui.CROSS), `${ui.text("stale")} ${ui.dim("(" + staleCount + ")")}`);
|
|
608
|
-
for (const s of stale) {
|
|
609
|
-
console.log(` ${ui.dim(ui.BULLET)} ${ui.text(ui.shortNotePath(s.relPath))} ${ui.muted(`${s.ageDays}d`)} ${ui.dim(`${s.newerSameProjectCount} newer ${s.project} sessions`)}`);
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
if (runAll || opts.contradictions) {
|
|
614
|
-
const sp = ui.spinner("checking contradictions (haiku)").start();
|
|
615
|
-
const c = await contradictionCheck(cfg, db);
|
|
616
|
-
sp.stop();
|
|
617
|
-
contradictionCount = c.contradictions.length;
|
|
618
|
-
issues += contradictionCount;
|
|
619
|
-
if (contradictionCount === 0) {
|
|
620
|
-
ui.row(ui.success(ui.CHECK), `${ui.text("contradictions")} ${ui.dim(`none found in ${c.checked} pairs`)}`);
|
|
621
|
-
}
|
|
622
|
-
else {
|
|
623
|
-
ui.row(ui.errorColor(ui.CROSS), `${ui.text("contradictions")} ${ui.dim("(" + contradictionCount + ")")}`);
|
|
624
|
-
for (const x of c.contradictions) {
|
|
625
|
-
console.log(` ${ui.dim(ui.BULLET)} ${ui.text(ui.shortNotePath(x.a))} ${ui.dim("vs")} ${ui.text(ui.shortNotePath(x.b))}`);
|
|
626
|
-
console.log(` ${ui.muted(x.reason)}`);
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
ui.blank();
|
|
631
|
-
ui.divider();
|
|
632
|
-
ui.summary({
|
|
633
|
-
issues: {
|
|
634
|
-
value: issues,
|
|
635
|
-
color: issues > 0 ? ui.errorColor : ui.success,
|
|
636
|
-
},
|
|
637
|
-
orphans: { value: orphanCount, color: ui.muted },
|
|
638
|
-
strays: { value: strayCount, color: ui.muted },
|
|
639
|
-
"legacy-related": { value: legacyCount, color: ui.muted },
|
|
640
|
-
stale: { value: staleCount, color: ui.muted },
|
|
641
|
-
contradictions: { value: contradictionCount, color: ui.muted },
|
|
642
|
-
});
|
|
643
|
-
ui.divider();
|
|
644
|
-
}
|
|
645
|
-
finally {
|
|
646
|
-
db.close();
|
|
647
|
-
}
|
|
648
|
-
}));
|
|
416
|
+
.option("--yes", "Skip the cost confirmation for the contradiction check")
|
|
417
|
+
.action(runAction(lintCommand));
|
|
649
418
|
program
|
|
650
419
|
.command("summarize [project]")
|
|
651
420
|
.description("Generate a project, --all, or period (--week/--month) knowledge summary")
|
|
@@ -655,304 +424,14 @@ program
|
|
|
655
424
|
.option("--model <model>", "Synthesis model: haiku | sonnet (period only)")
|
|
656
425
|
.option("--dry-run", "Show note count + estimated cost, exit before LLM")
|
|
657
426
|
.option("--yes", "Skip the cost confirmation prompt (period only)")
|
|
658
|
-
.action(runAction(
|
|
659
|
-
const cfg = loadConfig();
|
|
660
|
-
const wantWeek = opts.week !== undefined;
|
|
661
|
-
const wantMonth = opts.month !== undefined;
|
|
662
|
-
if (wantWeek && wantMonth) {
|
|
663
|
-
console.error(chalk.red("use --week or --month, not both"));
|
|
664
|
-
process.exitCode = 1;
|
|
665
|
-
return;
|
|
666
|
-
}
|
|
667
|
-
// ── period summary path (--week / --month) ──────────────────────────
|
|
668
|
-
if (wantWeek || wantMonth) {
|
|
669
|
-
if (project || opts.all) {
|
|
670
|
-
console.error(chalk.red("--week/--month cannot be combined with a project or --all"));
|
|
671
|
-
process.exitCode = 1;
|
|
672
|
-
return;
|
|
673
|
-
}
|
|
674
|
-
if (opts.model && !["haiku", "sonnet"].includes(opts.model)) {
|
|
675
|
-
console.error(chalk.red(`--model must be 'haiku' or 'sonnet', got '${opts.model}'`));
|
|
676
|
-
process.exitCode = 1;
|
|
677
|
-
return;
|
|
678
|
-
}
|
|
679
|
-
const kind = wantWeek ? "week" : "month";
|
|
680
|
-
const raw = wantWeek ? opts.week : opts.month;
|
|
681
|
-
// commander yields `true` for a bare flag, or the string value for `--week 2`
|
|
682
|
-
const offset = typeof raw === "string" ? Number.parseInt(raw, 10) : 0;
|
|
683
|
-
if (!Number.isInteger(offset) || offset < 0) {
|
|
684
|
-
console.error(chalk.red(`--${kind} offset must be a non-negative integer`));
|
|
685
|
-
process.exitCode = 1;
|
|
686
|
-
return;
|
|
687
|
-
}
|
|
688
|
-
const period = { kind, offset };
|
|
689
|
-
const now = new Date();
|
|
690
|
-
const db = new StateDb();
|
|
691
|
-
try {
|
|
692
|
-
ui.header("summarize");
|
|
693
|
-
ui.divider();
|
|
694
|
-
const range = periodRange(period, now);
|
|
695
|
-
const label = periodLabel(period, range);
|
|
696
|
-
console.log(ui.text(label));
|
|
697
|
-
ui.divider();
|
|
698
|
-
ui.blank();
|
|
699
|
-
const notes = selectNotesInPeriod(db.listDistilled(), period, now);
|
|
700
|
-
if (notes.length === 0) {
|
|
701
|
-
ui.row(ui.warn(ui.WARN_GLYPH), ui.text(`no notes distilled in ${label} — nothing to summarize`));
|
|
702
|
-
ui.line(ui.dim(" run `vir run` to distill more sessions first"));
|
|
703
|
-
return;
|
|
704
|
-
}
|
|
705
|
-
const model = normalizeModelName(resolveModelShorthand(opts.model ?? cfg.models.distill), cfg.provider);
|
|
706
|
-
const counts = countByCategory(notes);
|
|
707
|
-
const prompt = buildPeriodPrompt(label, notes, counts);
|
|
708
|
-
const { inputTokens, outputTokens } = estimatePeriodCostTokens(prompt);
|
|
709
|
-
const estCost = estCostLabel(cfg, model, inputTokens, outputTokens);
|
|
710
|
-
ui.summary({
|
|
711
|
-
notes: { value: notes.length, color: ui.info },
|
|
712
|
-
model: { value: model, color: ui.accent },
|
|
713
|
-
"est. cost": { value: estCost, color: ui.warn },
|
|
714
|
-
});
|
|
715
|
-
ui.divider();
|
|
716
|
-
if (opts.dryRun === true) {
|
|
717
|
-
ui.line(ui.dim(" dry run — no synthesis performed; actuals may vary ±30%"));
|
|
718
|
-
return;
|
|
719
|
-
}
|
|
720
|
-
if (opts.yes !== true) {
|
|
721
|
-
const proceed = await confirm({
|
|
722
|
-
message: `synthesize with ${model} (~${estCost})?`,
|
|
723
|
-
default: true,
|
|
724
|
-
});
|
|
725
|
-
if (!proceed) {
|
|
726
|
-
ui.line(ui.dim("aborted"));
|
|
727
|
-
return;
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
const sp = ui.spinner("synthesizing period summary").start();
|
|
731
|
-
let result;
|
|
732
|
-
try {
|
|
733
|
-
result = await summarizePeriod(cfg, db, period, {
|
|
734
|
-
now,
|
|
735
|
-
model: opts.model,
|
|
736
|
-
});
|
|
737
|
-
sp.stop();
|
|
738
|
-
}
|
|
739
|
-
catch (err) {
|
|
740
|
-
sp.fail(ui.errorColor(err.message));
|
|
741
|
-
process.exitCode = 1;
|
|
742
|
-
return;
|
|
743
|
-
}
|
|
744
|
-
if (!result) {
|
|
745
|
-
ui.row(ui.warn(ui.WARN_GLYPH), ui.text("nothing to summarize"));
|
|
746
|
-
return;
|
|
747
|
-
}
|
|
748
|
-
ui.row(ui.success(ui.CHECK), ui.text(`wrote ${result.relPath} (${result.noteCount} notes)`));
|
|
749
|
-
ui.blank();
|
|
750
|
-
}
|
|
751
|
-
finally {
|
|
752
|
-
db.close();
|
|
753
|
-
}
|
|
754
|
-
return;
|
|
755
|
-
}
|
|
756
|
-
// ── project / --all path (unchanged) ────────────────────────────────
|
|
757
|
-
const db = new StateDb();
|
|
758
|
-
try {
|
|
759
|
-
if (opts.all) {
|
|
760
|
-
const results = await summarizeAll(cfg, db);
|
|
761
|
-
if (results.length === 0) {
|
|
762
|
-
console.log(chalk.yellow("no projects with notes"));
|
|
763
|
-
return;
|
|
764
|
-
}
|
|
765
|
-
for (const r of results) {
|
|
766
|
-
console.log(chalk.green(`summarized project/${r.slug}`) +
|
|
767
|
-
` (${r.counts.total} sessions)`);
|
|
768
|
-
}
|
|
769
|
-
return;
|
|
770
|
-
}
|
|
771
|
-
if (!project) {
|
|
772
|
-
console.error(chalk.red("usage: vir summarize <project> | --all | --week [n] | --month [n]"));
|
|
773
|
-
process.exitCode = 1;
|
|
774
|
-
return;
|
|
775
|
-
}
|
|
776
|
-
const res = await summarizeProject(cfg, project, db);
|
|
777
|
-
if (!res) {
|
|
778
|
-
console.log(chalk.yellow(`no distilled notes for project '${project}'`));
|
|
779
|
-
return;
|
|
780
|
-
}
|
|
781
|
-
console.log(chalk.green(`summarized project/${res.slug}`) +
|
|
782
|
-
` (${res.counts.total} sessions) → ${res.path}`);
|
|
783
|
-
}
|
|
784
|
-
finally {
|
|
785
|
-
db.close();
|
|
786
|
-
}
|
|
787
|
-
}));
|
|
427
|
+
.action(runAction(summarizeCommand));
|
|
788
428
|
program
|
|
789
429
|
.command("embed")
|
|
790
430
|
.description("Generate embeddings for distilled notes")
|
|
791
431
|
.option("--force", "Regenerate even if embedding already exists")
|
|
792
432
|
.option("--setup", "Install the local embedding provider (fastembed + bge-small) on demand")
|
|
793
433
|
.option("--yes", "Skip the --setup disk-cost confirmation")
|
|
794
|
-
.action(runAction(
|
|
795
|
-
if (opts.setup) {
|
|
796
|
-
await runEmbedSetup(opts.yes === true);
|
|
797
|
-
return;
|
|
798
|
-
}
|
|
799
|
-
const cfg = loadConfig();
|
|
800
|
-
ui.header("embed");
|
|
801
|
-
ui.blank();
|
|
802
|
-
const provider = await resolveEmbeddingProvider(cfg.embeddingProvider);
|
|
803
|
-
if (!provider) {
|
|
804
|
-
ui.row(ui.errorColor(ui.CROSS), ui.text("no embedding provider available"));
|
|
805
|
-
ui.line(ui.dim(" vir embed --setup # local provider, no Ollama needed (~233 MB)"));
|
|
806
|
-
ui.line(ui.dim(" or: brew install ollama && ollama pull nomic-embed-text && ollama serve"));
|
|
807
|
-
process.exitCode = 1;
|
|
808
|
-
return;
|
|
809
|
-
}
|
|
810
|
-
const provenance = provider.provenance();
|
|
811
|
-
const db = new StateDb();
|
|
812
|
-
try {
|
|
813
|
-
const rows = db.listDistilled();
|
|
814
|
-
const root = join(cfg.vaultPath, cfg.outputDir);
|
|
815
|
-
const embeddedRows = [
|
|
816
|
-
...db.getEmbeddings(root),
|
|
817
|
-
...db.getArticleEmbeddings(),
|
|
818
|
-
...db.getTopicEmbeddings(root, cfg.topicsDir),
|
|
819
|
-
...db.getPdfEmbeddings(),
|
|
820
|
-
];
|
|
821
|
-
// Crossing a model boundary is an index invalidation, not a gap fill —
|
|
822
|
-
// it needs --force AND explicit consent, with the cost stated first.
|
|
823
|
-
// Without --force, mismatched rows stay excluded from vector search
|
|
824
|
-
// (counted, reported) and only NULL-embedding gaps are filled.
|
|
825
|
-
const mismatched = embeddedRows.filter((r) => r.embeddingModel !== provider.modelName).length;
|
|
826
|
-
if (mismatched > 0 && !opts.force) {
|
|
827
|
-
ui.row(ui.warn(ui.WARN_GLYPH), ui.text(`${mismatched} note(s) are embedded under a different model than the active one (${provider.modelName})`));
|
|
828
|
-
ui.line(ui.dim("they are excluded from vector search until re-embedded"));
|
|
829
|
-
ui.line(ui.dim("run `vir embed --force` to re-embed the whole index"));
|
|
830
|
-
}
|
|
831
|
-
if (mismatched > 0 && opts.force) {
|
|
832
|
-
const estSecs = Math.max(1, Math.round(rows.length * 0.15));
|
|
833
|
-
ui.row(ui.warn(ui.WARN_GLYPH), ui.text(`model boundary: re-embedding ${rows.length} note(s) under ${provider.modelName} (~${estSecs}s), replacing vectors from other models`));
|
|
834
|
-
if (!opts.yes) {
|
|
835
|
-
const rl = createInterface({ input: stdin, output: stdout });
|
|
836
|
-
const answer = (await rl.question("Proceed? [y/N] ")).trim().toLowerCase();
|
|
837
|
-
rl.close();
|
|
838
|
-
if (answer !== "y" && answer !== "yes") {
|
|
839
|
-
ui.line(ui.dim("aborted — index unchanged"));
|
|
840
|
-
return;
|
|
841
|
-
}
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
const existing = new Set(db
|
|
845
|
-
.getEmbeddings(root)
|
|
846
|
-
.filter((r) => r.embeddingModel === provider.modelName)
|
|
847
|
-
.map((r) => r.sessionId));
|
|
848
|
-
const target = opts.force
|
|
849
|
-
? rows
|
|
850
|
-
: rows.filter((r) => !existing.has(r.sessionId));
|
|
851
|
-
// Topics live in their own table, so backfill them here too — a compose
|
|
852
|
-
// while Ollama was down heals on a manual `vir embed`, not only the next
|
|
853
|
-
// `vir run` sweep. --force re-embeds all topics; otherwise just NULL ones.
|
|
854
|
-
const topicTargets = opts.force
|
|
855
|
-
? db.listTopics().map((t) => ({ id: t.id, content: t.content }))
|
|
856
|
-
: db.listTopicEmbeddingTargets();
|
|
857
|
-
// Articles live in their own table too — back-fill them here so a clip
|
|
858
|
-
// distilled while Ollama was down heals on a manual `vir embed`, not only
|
|
859
|
-
// the next `vir run` sweep. --force re-embeds all embeddable articles
|
|
860
|
-
// (keyed by source path); otherwise just the NULL-embedding ones.
|
|
861
|
-
const articleTargets = opts.force
|
|
862
|
-
? db.listArticles().map((a) => ({ path: a.path, content: a.content }))
|
|
863
|
-
: db.listArticleEmbeddingTargets();
|
|
864
|
-
// PDFs live in their own table too — same back-fill rationale as articles.
|
|
865
|
-
const pdfTargets = opts.force
|
|
866
|
-
? db.listPdfs().map((p) => ({ path: p.path, content: p.content }))
|
|
867
|
-
: db.listPdfEmbeddingTargets();
|
|
868
|
-
const total = target.length +
|
|
869
|
-
topicTargets.length +
|
|
870
|
-
articleTargets.length +
|
|
871
|
-
pdfTargets.length;
|
|
872
|
-
if (total === 0) {
|
|
873
|
-
ui.row(ui.success(ui.CHECK), ui.text("all notes already embedded"));
|
|
874
|
-
return;
|
|
875
|
-
}
|
|
876
|
-
const sp = ui.spinner(`embedding notes (0/${total})`).start();
|
|
877
|
-
let embedded = 0;
|
|
878
|
-
let skipped = 0;
|
|
879
|
-
let errors = 0;
|
|
880
|
-
for (let i = 0; i < target.length; i += 1) {
|
|
881
|
-
const r = target[i];
|
|
882
|
-
if (!r)
|
|
883
|
-
continue;
|
|
884
|
-
if (r.content.trim().length === 0) {
|
|
885
|
-
skipped += 1;
|
|
886
|
-
continue;
|
|
887
|
-
}
|
|
888
|
-
const vec = await embedNoteWithProvider(provider, r.content);
|
|
889
|
-
if (!vec) {
|
|
890
|
-
errors += 1;
|
|
891
|
-
continue;
|
|
892
|
-
}
|
|
893
|
-
db.storeEmbedding(r.sessionId, vec, provenance);
|
|
894
|
-
embedded += 1;
|
|
895
|
-
sp.text = ui.dim(`embedding notes (${embedded}/${total})`);
|
|
896
|
-
}
|
|
897
|
-
for (const t of topicTargets) {
|
|
898
|
-
if (!t.content || t.content.trim().length === 0) {
|
|
899
|
-
skipped += 1;
|
|
900
|
-
continue;
|
|
901
|
-
}
|
|
902
|
-
const vec = await embedNoteWithProvider(provider, t.content);
|
|
903
|
-
if (!vec) {
|
|
904
|
-
errors += 1;
|
|
905
|
-
continue;
|
|
906
|
-
}
|
|
907
|
-
db.storeTopicEmbedding(t.id, vec, provenance);
|
|
908
|
-
embedded += 1;
|
|
909
|
-
sp.text = ui.dim(`embedding notes (${embedded}/${total})`);
|
|
910
|
-
}
|
|
911
|
-
for (const a of articleTargets) {
|
|
912
|
-
if (!a.content || a.content.trim().length === 0) {
|
|
913
|
-
skipped += 1;
|
|
914
|
-
continue;
|
|
915
|
-
}
|
|
916
|
-
const vec = await embedNoteWithProvider(provider, a.content);
|
|
917
|
-
if (!vec) {
|
|
918
|
-
errors += 1;
|
|
919
|
-
continue;
|
|
920
|
-
}
|
|
921
|
-
db.storeArticleEmbedding(a.path, vec, provenance);
|
|
922
|
-
embedded += 1;
|
|
923
|
-
sp.text = ui.dim(`embedding notes (${embedded}/${total})`);
|
|
924
|
-
}
|
|
925
|
-
for (const p of pdfTargets) {
|
|
926
|
-
if (!p.content || p.content.trim().length === 0) {
|
|
927
|
-
skipped += 1;
|
|
928
|
-
continue;
|
|
929
|
-
}
|
|
930
|
-
const vec = await embedNoteWithProvider(provider, p.content);
|
|
931
|
-
if (!vec) {
|
|
932
|
-
errors += 1;
|
|
933
|
-
continue;
|
|
934
|
-
}
|
|
935
|
-
db.storePdfEmbedding(p.path, vec, provenance);
|
|
936
|
-
embedded += 1;
|
|
937
|
-
sp.text = ui.dim(`embedding notes (${embedded}/${total})`);
|
|
938
|
-
}
|
|
939
|
-
sp.succeed(ui.text(`embedded ${embedded} notes`));
|
|
940
|
-
ui.blank();
|
|
941
|
-
ui.divider();
|
|
942
|
-
ui.summary({
|
|
943
|
-
embedded: { value: embedded, color: ui.success },
|
|
944
|
-
skipped: { value: skipped, color: ui.muted },
|
|
945
|
-
errors: {
|
|
946
|
-
value: errors,
|
|
947
|
-
color: errors > 0 ? ui.errorColor : ui.dim,
|
|
948
|
-
},
|
|
949
|
-
});
|
|
950
|
-
ui.divider();
|
|
951
|
-
}
|
|
952
|
-
finally {
|
|
953
|
-
db.close();
|
|
954
|
-
}
|
|
955
|
-
}));
|
|
434
|
+
.action(runAction(embedCommand));
|
|
956
435
|
// JSON path for `vir query --json`: stdout gets a single JSON array on success
|
|
957
436
|
// (`[]` when nothing matched), exit 0. On failure stdout stays EMPTY so the
|
|
958
437
|
// plugin can `JSON.parse(stdout)` unguarded — the error goes to stderr as a
|
|
@@ -1094,88 +573,7 @@ program
|
|
|
1094
573
|
.option("--model <model>", "Synthesis model: haiku | sonnet")
|
|
1095
574
|
.option("--dry-run", "Show top sources + estimated cost, exit before LLM")
|
|
1096
575
|
.option("--yes", "Skip the cost confirmation prompt")
|
|
1097
|
-
.action(runAction(
|
|
1098
|
-
const cfg = loadConfig();
|
|
1099
|
-
if (opts.model && !["haiku", "sonnet"].includes(opts.model)) {
|
|
1100
|
-
console.error(chalk.red(`--model must be 'haiku' or 'sonnet', got '${opts.model}'`));
|
|
1101
|
-
process.exitCode = 1;
|
|
1102
|
-
return;
|
|
1103
|
-
}
|
|
1104
|
-
const limit = Math.min(50, Math.max(1, Number.parseInt(opts.limit ?? "20", 10) || 20));
|
|
1105
|
-
const db = new StateDb();
|
|
1106
|
-
try {
|
|
1107
|
-
ui.header("compose");
|
|
1108
|
-
ui.divider();
|
|
1109
|
-
console.log(ui.text(topic));
|
|
1110
|
-
ui.divider();
|
|
1111
|
-
ui.blank();
|
|
1112
|
-
const sp = ui.spinner("searching vault for related notes").start();
|
|
1113
|
-
const sources = await gatherSources(cfg, db, topic, limit);
|
|
1114
|
-
sp.stop();
|
|
1115
|
-
if (sources.length === 0) {
|
|
1116
|
-
ui.row(ui.warn(ui.WARN_GLYPH), ui.text("no related notes found — nothing to synthesize"));
|
|
1117
|
-
ui.line(ui.dim(" run `vir run` to distill more sessions first"));
|
|
1118
|
-
return;
|
|
1119
|
-
}
|
|
1120
|
-
for (const s of sources.slice(0, 10))
|
|
1121
|
-
ui.sourceRow(s.title, s.score);
|
|
1122
|
-
ui.divider();
|
|
1123
|
-
const model = normalizeModelName(resolveModelShorthand(opts.model ?? cfg.models.distill), cfg.provider);
|
|
1124
|
-
const { inputTokens, outputTokens } = estimateComposeCostTokens(topic, sources);
|
|
1125
|
-
const estCost = estCostLabel(cfg, model, inputTokens, outputTokens);
|
|
1126
|
-
ui.summary({
|
|
1127
|
-
sources: { value: sources.length, color: ui.info },
|
|
1128
|
-
model: { value: model, color: ui.accent },
|
|
1129
|
-
"est. cost": { value: estCost, color: ui.warn },
|
|
1130
|
-
});
|
|
1131
|
-
ui.divider();
|
|
1132
|
-
if (opts.dryRun) {
|
|
1133
|
-
ui.line(ui.dim(" dry run — no synthesis performed; actuals may vary ±30%"));
|
|
1134
|
-
return;
|
|
1135
|
-
}
|
|
1136
|
-
if (opts.yes !== true) {
|
|
1137
|
-
const proceed = await confirm({
|
|
1138
|
-
message: `synthesize with ${model} (~${estCost})?`,
|
|
1139
|
-
default: true,
|
|
1140
|
-
});
|
|
1141
|
-
if (!proceed) {
|
|
1142
|
-
ui.line(ui.dim("aborted"));
|
|
1143
|
-
return;
|
|
1144
|
-
}
|
|
1145
|
-
}
|
|
1146
|
-
const writer = new VaultWriter(cfg, db);
|
|
1147
|
-
const sp2 = ui.spinner("synthesizing topic page").start();
|
|
1148
|
-
let result;
|
|
1149
|
-
try {
|
|
1150
|
-
result = await composeFromSources(cfg, db, topic, sources, writer, {
|
|
1151
|
-
forceModel: opts.model,
|
|
1152
|
-
});
|
|
1153
|
-
sp2.stop();
|
|
1154
|
-
}
|
|
1155
|
-
catch (err) {
|
|
1156
|
-
sp2.fail(ui.errorColor(err.message));
|
|
1157
|
-
process.exitCode = 1;
|
|
1158
|
-
return;
|
|
1159
|
-
}
|
|
1160
|
-
ui.row(ui.success(ui.CHECK), ui.text(`wrote ${result.relPath}`));
|
|
1161
|
-
ui.blank();
|
|
1162
|
-
// Actual cost from the record callLLM just appended for this compose.
|
|
1163
|
-
const rec = [...readCostLog()]
|
|
1164
|
-
.reverse()
|
|
1165
|
-
.find((r) => r.stage === "compose" && r.session === result.slug);
|
|
1166
|
-
ui.summary({
|
|
1167
|
-
title: { value: result.title, color: ui.text },
|
|
1168
|
-
sources: { value: result.sourceCount, color: ui.info },
|
|
1169
|
-
confidence: { value: result.confidence.toFixed(2), color: ui.info },
|
|
1170
|
-
...(rec && rec.estimated_cost_usd !== null
|
|
1171
|
-
? { cost: { value: ui.formatUsd(rec.estimated_cost_usd), color: ui.warn } }
|
|
1172
|
-
: {}),
|
|
1173
|
-
});
|
|
1174
|
-
}
|
|
1175
|
-
finally {
|
|
1176
|
-
db.close();
|
|
1177
|
-
}
|
|
1178
|
-
}));
|
|
576
|
+
.action(runAction(composeCommand));
|
|
1179
577
|
const projectsCmd = program
|
|
1180
578
|
.command("projects")
|
|
1181
579
|
.description("Per-project distillation decisions: table + include/exclude");
|
|
@@ -1299,7 +697,7 @@ program
|
|
|
1299
697
|
if (opts.approve !== undefined || opts.reject !== undefined) {
|
|
1300
698
|
throw new Error("--approve and --reject need --json (the terminal loop asks per note)");
|
|
1301
699
|
}
|
|
1302
|
-
await runReview(opts);
|
|
700
|
+
await withPipelineLock(() => runReview(opts));
|
|
1303
701
|
}));
|
|
1304
702
|
program
|
|
1305
703
|
.command("audit")
|
|
@@ -1311,125 +709,7 @@ program
|
|
|
1311
709
|
.option("--dry-run", "Show notes, batches and est. cost, exit before any LLM call")
|
|
1312
710
|
.option("--yes", "Skip the cost confirmation prompt")
|
|
1313
711
|
.option("--apply-rejects", "Move notes with a fresh reject verdict to .rejected/ (no model call; undo with vir review --restore)")
|
|
1314
|
-
.action(runAction(
|
|
1315
|
-
const limit = parseLimitOption(opts.limit);
|
|
1316
|
-
if (limit === null) {
|
|
1317
|
-
ui.header("audit");
|
|
1318
|
-
ui.blank();
|
|
1319
|
-
ui.row(ui.errorColor(ui.CROSS), ui.text(`--limit must be a positive integer, got "${opts.limit}"`));
|
|
1320
|
-
process.exitCode = 1;
|
|
1321
|
-
return;
|
|
1322
|
-
}
|
|
1323
|
-
const cfg = loadConfig();
|
|
1324
|
-
const db = new StateDb();
|
|
1325
|
-
try {
|
|
1326
|
-
const root = vaultRoot(cfg);
|
|
1327
|
-
if (opts.applyRejects) {
|
|
1328
|
-
const { toMove: targets, verified } = previewRejects(db.listDistilled(), db.listAudits(), (r) => noteIsVerified(root, r), opts.project);
|
|
1329
|
-
ui.header("audit");
|
|
1330
|
-
ui.blank();
|
|
1331
|
-
ui.summary({
|
|
1332
|
-
rejects: { value: targets.length, color: ui.errorColor },
|
|
1333
|
-
"approved, skipped": { value: verified.length, color: verified.length > 0 ? ui.warn : ui.muted },
|
|
1334
|
-
});
|
|
1335
|
-
for (const r of targets.slice(0, 20))
|
|
1336
|
-
ui.line(` ${ui.dim(ui.BULLET)} ${ui.text(r.topic)} ${ui.dim(r.project)}`);
|
|
1337
|
-
if (targets.length > 20)
|
|
1338
|
-
ui.line(ui.dim(` … and ${targets.length - 20} more`));
|
|
1339
|
-
ui.divider();
|
|
1340
|
-
if (targets.length === 0 || opts.dryRun) {
|
|
1341
|
-
if (opts.dryRun)
|
|
1342
|
-
ui.line(ui.dim(" dry run — nothing moved"));
|
|
1343
|
-
return;
|
|
1344
|
-
}
|
|
1345
|
-
if (opts.yes !== true) {
|
|
1346
|
-
const proceed = await confirm({ message: `move ${targets.length} notes to .rejected/?`, default: false });
|
|
1347
|
-
if (!proceed) {
|
|
1348
|
-
ui.line(ui.dim("aborted"));
|
|
1349
|
-
return;
|
|
1350
|
-
}
|
|
1351
|
-
}
|
|
1352
|
-
const s = applyAuditRejects(db, root, { project: opts.project });
|
|
1353
|
-
ui.summary({
|
|
1354
|
-
moved: { value: s.moved, color: ui.info },
|
|
1355
|
-
"missing file": { value: s.missingFile, color: ui.muted },
|
|
1356
|
-
collision: { value: s.collision, color: s.collision > 0 ? ui.warn : ui.muted },
|
|
1357
|
-
verified: { value: s.verified, color: s.verified > 0 ? ui.warn : ui.muted },
|
|
1358
|
-
});
|
|
1359
|
-
ui.line(ui.dim(" undo any of them with `vir review --restore <note>`"));
|
|
1360
|
-
return;
|
|
1361
|
-
}
|
|
1362
|
-
const auditOpts = { project: opts.project, limit, all: opts.all };
|
|
1363
|
-
const isVerified = (r) => noteIsVerified(root, r);
|
|
1364
|
-
const model = normalizeModelName(resolveModelShorthand(opts.model ?? cfg.models.distill), cfg.provider);
|
|
1365
|
-
const picked = selectAuditRows(db.listDistilled(), db.listAudits(), auditOpts, isVerified);
|
|
1366
|
-
const batches = batchByProject(picked.rows);
|
|
1367
|
-
const inputTokens = Math.ceil(picked.rows.reduce((n, r) => n + r.content.length, 0) / 4 + batches.length * 600);
|
|
1368
|
-
const outputTokens = picked.rows.length * 60;
|
|
1369
|
-
const estCost = estCostLabel(cfg, model, inputTokens, outputTokens);
|
|
1370
|
-
ui.header("audit");
|
|
1371
|
-
ui.blank();
|
|
1372
|
-
ui.summary({
|
|
1373
|
-
notes: { value: picked.rows.length, color: ui.info },
|
|
1374
|
-
batches: { value: batches.length, color: ui.info },
|
|
1375
|
-
"already audited": { value: picked.skippedFresh, color: ui.muted },
|
|
1376
|
-
verified: { value: picked.skippedVerified, color: ui.muted },
|
|
1377
|
-
model: { value: model, color: ui.accent },
|
|
1378
|
-
"est. cost": { value: estCost, color: ui.warn },
|
|
1379
|
-
});
|
|
1380
|
-
ui.divider();
|
|
1381
|
-
if (picked.rows.length === 0) {
|
|
1382
|
-
ui.row(ui.success(ui.CHECK), ui.text("nothing to audit"));
|
|
1383
|
-
return;
|
|
1384
|
-
}
|
|
1385
|
-
if (opts.dryRun) {
|
|
1386
|
-
ui.line(ui.dim(" dry run — no model calls made"));
|
|
1387
|
-
return;
|
|
1388
|
-
}
|
|
1389
|
-
if (opts.yes !== true) {
|
|
1390
|
-
const proceed = await confirm({ message: `audit ${picked.rows.length} notes with ${model} (~${estCost})?`, default: true });
|
|
1391
|
-
if (!proceed) {
|
|
1392
|
-
ui.line(ui.dim("aborted"));
|
|
1393
|
-
return;
|
|
1394
|
-
}
|
|
1395
|
-
}
|
|
1396
|
-
const client = maybeAnthropicClient(cfg);
|
|
1397
|
-
const sp = ui.spinner(`auditing ${batches.length} batches`).start();
|
|
1398
|
-
let summary;
|
|
1399
|
-
try {
|
|
1400
|
-
summary = await runAudit(db, auditOpts, {
|
|
1401
|
-
isVerified,
|
|
1402
|
-
llm: (prompt) => withRateLimitRetry(() => callLLM(cfg, client, {
|
|
1403
|
-
prompt,
|
|
1404
|
-
model,
|
|
1405
|
-
maxTokens: 4000,
|
|
1406
|
-
cost: { stage: "audit" },
|
|
1407
|
-
})),
|
|
1408
|
-
});
|
|
1409
|
-
sp.stop();
|
|
1410
|
-
}
|
|
1411
|
-
catch (err) {
|
|
1412
|
-
sp.fail(ui.errorColor(err.message));
|
|
1413
|
-
process.exitCode = 1;
|
|
1414
|
-
return;
|
|
1415
|
-
}
|
|
1416
|
-
ui.summary({
|
|
1417
|
-
audited: { value: summary.audited, color: ui.info },
|
|
1418
|
-
keep: { value: summary.byVerdict.keep, color: ui.success },
|
|
1419
|
-
verify: { value: summary.byVerdict.verify, color: ui.warn },
|
|
1420
|
-
merge: { value: summary.byVerdict.merge, color: ui.warn },
|
|
1421
|
-
reject: { value: summary.byVerdict.reject, color: ui.errorColor },
|
|
1422
|
-
unanswered: { value: summary.unanswered, color: ui.muted },
|
|
1423
|
-
"failed batches": { value: summary.failedBatches, color: summary.failedBatches > 0 ? ui.errorColor : ui.muted },
|
|
1424
|
-
});
|
|
1425
|
-
ui.line(ui.dim(" nothing was moved — walk the flagged notes with `vir review --audited`"));
|
|
1426
|
-
if (summary.failedBatches > 0)
|
|
1427
|
-
process.exitCode = 1;
|
|
1428
|
-
}
|
|
1429
|
-
finally {
|
|
1430
|
-
db.close();
|
|
1431
|
-
}
|
|
1432
|
-
}));
|
|
712
|
+
.action(runAction(auditCommand));
|
|
1433
713
|
program
|
|
1434
714
|
.command("prune")
|
|
1435
715
|
.description("Demote agent-derived notes to .rejected/ (dry run by default)")
|
|
@@ -1585,342 +865,6 @@ function renderDaemon(ds, cadenceHours) {
|
|
|
1585
865
|
`${ui.dim("config")} ${ui.muted(ds.configPath ? collapseHome(ds.configPath) : "—")}`,
|
|
1586
866
|
], { title: "daemon", width: 52 });
|
|
1587
867
|
}
|
|
1588
|
-
async function cmdInit() {
|
|
1589
|
-
ensureVirDir();
|
|
1590
|
-
const existing = configExists() ? safeLoad() : null;
|
|
1591
|
-
ui.header("init");
|
|
1592
|
-
ui.blank();
|
|
1593
|
-
// ── vault path ──────────────────────────────────────────────────────────
|
|
1594
|
-
let vaultPath = "";
|
|
1595
|
-
for (;;) {
|
|
1596
|
-
vaultPath = await input({
|
|
1597
|
-
message: "Obsidian vault path",
|
|
1598
|
-
default: existing?.vaultPath ??
|
|
1599
|
-
join(homedir(), "Documents", "Obsidian", "MyVault"),
|
|
1600
|
-
});
|
|
1601
|
-
const expanded = expandHome(vaultPath);
|
|
1602
|
-
if (existsSync(expanded))
|
|
1603
|
-
break;
|
|
1604
|
-
const create = await confirm({
|
|
1605
|
-
message: `Vault path does not exist (${expanded}). Create it?`,
|
|
1606
|
-
default: true,
|
|
1607
|
-
});
|
|
1608
|
-
if (create) {
|
|
1609
|
-
try {
|
|
1610
|
-
mkdirSync(expanded, { recursive: true });
|
|
1611
|
-
break;
|
|
1612
|
-
}
|
|
1613
|
-
catch (err) {
|
|
1614
|
-
console.error(chalk.red(`failed to create: ${err.message}`));
|
|
1615
|
-
}
|
|
1616
|
-
}
|
|
1617
|
-
}
|
|
1618
|
-
const outputDir = await input({
|
|
1619
|
-
message: "Output subdir inside vault",
|
|
1620
|
-
default: existing?.outputDir ?? "vir",
|
|
1621
|
-
});
|
|
1622
|
-
// ── claude projects dir ─────────────────────────────────────────────────
|
|
1623
|
-
let claudeProjectsDir = "";
|
|
1624
|
-
for (;;) {
|
|
1625
|
-
claudeProjectsDir = await input({
|
|
1626
|
-
message: "Claude Code projects dir",
|
|
1627
|
-
default: existing?.claudeProjectsDir ?? join(homedir(), ".claude", "projects"),
|
|
1628
|
-
});
|
|
1629
|
-
const expanded = expandHome(claudeProjectsDir);
|
|
1630
|
-
if (existsSync(expanded))
|
|
1631
|
-
break;
|
|
1632
|
-
console.warn(chalk.yellow("directory not found — Claude Code sessions may not exist yet"));
|
|
1633
|
-
const cont = await confirm({
|
|
1634
|
-
message: "continue anyway?",
|
|
1635
|
-
default: false,
|
|
1636
|
-
});
|
|
1637
|
-
if (cont)
|
|
1638
|
-
break;
|
|
1639
|
-
}
|
|
1640
|
-
// ── web articles (optional second input source) ─────────────────────────
|
|
1641
|
-
let articlesDir = existing?.articlesDir;
|
|
1642
|
-
const wantsArticles = await confirm({
|
|
1643
|
-
message: "Do you save web articles to a folder (e.g. Obsidian Web Clipper)?",
|
|
1644
|
-
default: existing?.articlesDir !== undefined,
|
|
1645
|
-
});
|
|
1646
|
-
if (wantsArticles) {
|
|
1647
|
-
for (;;) {
|
|
1648
|
-
articlesDir = await input({
|
|
1649
|
-
message: "Articles (raw/) directory",
|
|
1650
|
-
default: existing?.articlesDir ??
|
|
1651
|
-
join(homedir(), "Documents", "Obsidian", "raw"),
|
|
1652
|
-
});
|
|
1653
|
-
const expanded = expandHome(articlesDir);
|
|
1654
|
-
if (existsSync(expanded))
|
|
1655
|
-
break;
|
|
1656
|
-
const create = await confirm({
|
|
1657
|
-
message: `Path does not exist (${expanded}). Create it?`,
|
|
1658
|
-
default: true,
|
|
1659
|
-
});
|
|
1660
|
-
if (create) {
|
|
1661
|
-
try {
|
|
1662
|
-
mkdirSync(expanded, { recursive: true });
|
|
1663
|
-
break;
|
|
1664
|
-
}
|
|
1665
|
-
catch (err) {
|
|
1666
|
-
console.error(chalk.red(`failed to create: ${err.message}`));
|
|
1667
|
-
}
|
|
1668
|
-
}
|
|
1669
|
-
else {
|
|
1670
|
-
break;
|
|
1671
|
-
}
|
|
1672
|
-
}
|
|
1673
|
-
}
|
|
1674
|
-
else {
|
|
1675
|
-
articlesDir = undefined;
|
|
1676
|
-
}
|
|
1677
|
-
// ── PDFs / papers (optional third input source) ──────────────────────────
|
|
1678
|
-
let pdfsDir = existing?.pdfsDir;
|
|
1679
|
-
const wantsPdfs = await confirm({
|
|
1680
|
-
message: "Do you keep PDFs / papers in a folder to ingest?",
|
|
1681
|
-
default: existing?.pdfsDir !== undefined,
|
|
1682
|
-
});
|
|
1683
|
-
if (wantsPdfs) {
|
|
1684
|
-
for (;;) {
|
|
1685
|
-
pdfsDir = await input({
|
|
1686
|
-
message: "PDFs directory",
|
|
1687
|
-
default: existing?.pdfsDir ?? join(homedir(), "Documents", "papers"),
|
|
1688
|
-
});
|
|
1689
|
-
const expanded = expandHome(pdfsDir);
|
|
1690
|
-
if (existsSync(expanded))
|
|
1691
|
-
break;
|
|
1692
|
-
const create = await confirm({
|
|
1693
|
-
message: `Path does not exist (${expanded}). Create it?`,
|
|
1694
|
-
default: true,
|
|
1695
|
-
});
|
|
1696
|
-
if (create) {
|
|
1697
|
-
try {
|
|
1698
|
-
mkdirSync(expanded, { recursive: true });
|
|
1699
|
-
break;
|
|
1700
|
-
}
|
|
1701
|
-
catch (err) {
|
|
1702
|
-
console.error(chalk.red(`failed to create: ${err.message}`));
|
|
1703
|
-
}
|
|
1704
|
-
}
|
|
1705
|
-
else {
|
|
1706
|
-
break;
|
|
1707
|
-
}
|
|
1708
|
-
}
|
|
1709
|
-
}
|
|
1710
|
-
else {
|
|
1711
|
-
pdfsDir = undefined;
|
|
1712
|
-
}
|
|
1713
|
-
const cadenceHours = Number(await input({
|
|
1714
|
-
message: "Cadence (hours)",
|
|
1715
|
-
default: String(existing?.cadenceHours ?? 3),
|
|
1716
|
-
validate: (v) => {
|
|
1717
|
-
const n = Number(v);
|
|
1718
|
-
return Number.isFinite(n) && n > 0 ? true : "must be a positive number";
|
|
1719
|
-
},
|
|
1720
|
-
}));
|
|
1721
|
-
// ── provider picker ─────────────────────────────────────────────────────
|
|
1722
|
-
const provider = (await select({
|
|
1723
|
-
message: "Provider",
|
|
1724
|
-
default: existing?.provider ?? "anthropic",
|
|
1725
|
-
choices: [
|
|
1726
|
-
{
|
|
1727
|
-
name: "Anthropic (API key — predictable per-session cost, no effect on your Claude Code limits)",
|
|
1728
|
-
value: "anthropic",
|
|
1729
|
-
},
|
|
1730
|
-
{
|
|
1731
|
-
name: "Claude Code (subscription — free and keyless; distills consume your Claude Code usage limits)",
|
|
1732
|
-
value: "claude-cli",
|
|
1733
|
-
},
|
|
1734
|
-
{
|
|
1735
|
-
name: "Kie.ai (~72% cheaper API, third-party proxy — you trade reliability for cost)",
|
|
1736
|
-
value: "kie",
|
|
1737
|
-
},
|
|
1738
|
-
],
|
|
1739
|
-
}));
|
|
1740
|
-
// Secret prompt discipline: mask while typing, echo only a masked
|
|
1741
|
-
// confirmation (maskSecret), and NEVER render the existing key as the
|
|
1742
|
-
// visible inquirer default — that prints the full secret to the terminal
|
|
1743
|
-
// before a single keystroke. Empty input keeps the existing/env key.
|
|
1744
|
-
const promptSecret = async (message, keep, validate) => {
|
|
1745
|
-
const value = await input({
|
|
1746
|
-
message: keep
|
|
1747
|
-
? `${message} (Enter keeps ${maskSecret(keep)})`
|
|
1748
|
-
: message,
|
|
1749
|
-
transformer: (v, { isFinal }) => isFinal ? maskSecret(v || keep || "") : "•".repeat(v.length),
|
|
1750
|
-
validate: (v) => (v === "" && keep ? true : validate(v)),
|
|
1751
|
-
});
|
|
1752
|
-
return value === "" && keep ? keep : value;
|
|
1753
|
-
};
|
|
1754
|
-
let anthropicApiKey;
|
|
1755
|
-
let kieApiKey;
|
|
1756
|
-
if (provider === "anthropic") {
|
|
1757
|
-
anthropicApiKey = await promptSecret("Anthropic API key", existing?.anthropicApiKey ?? process.env.ANTHROPIC_API_KEY ?? undefined, (v) => (v.startsWith("sk-ant-") ? true : "key should start with sk-ant-"));
|
|
1758
|
-
}
|
|
1759
|
-
else if (provider === "kie") {
|
|
1760
|
-
kieApiKey = await promptSecret("Kie.ai API key", existing?.kieApiKey ?? process.env.KIE_API_KEY ?? undefined, (v) => (v.length > 10 ? true : "enter a valid Kie.ai API key"));
|
|
1761
|
-
}
|
|
1762
|
-
// claude-cli: no credential of any kind — it uses the `claude` login.
|
|
1763
|
-
// ── model pickers (provider-aware) ──────────────────────────────────────
|
|
1764
|
-
// claude-cli accepts the same full Anthropic model ids (and pins them per
|
|
1765
|
-
// invocation via --model), so it shares the anthropic id set.
|
|
1766
|
-
const classifyChoices = provider !== "kie"
|
|
1767
|
-
? [
|
|
1768
|
-
{
|
|
1769
|
-
name: "claude-haiku-4-5-20251001 (recommended)",
|
|
1770
|
-
value: "claude-haiku-4-5-20251001",
|
|
1771
|
-
},
|
|
1772
|
-
{ name: "claude-sonnet-4-6", value: "claude-sonnet-4-6" },
|
|
1773
|
-
]
|
|
1774
|
-
: [
|
|
1775
|
-
{
|
|
1776
|
-
name: "claude-haiku-4-5 (recommended)",
|
|
1777
|
-
value: "claude-haiku-4-5",
|
|
1778
|
-
},
|
|
1779
|
-
{ name: "claude-sonnet-4-6", value: "claude-sonnet-4-6" },
|
|
1780
|
-
];
|
|
1781
|
-
const classifyModel = await select({
|
|
1782
|
-
message: "Classify model (fast pass)",
|
|
1783
|
-
choices: classifyChoices,
|
|
1784
|
-
});
|
|
1785
|
-
const distillChoices = provider !== "kie"
|
|
1786
|
-
? [
|
|
1787
|
-
{
|
|
1788
|
-
name: "claude-sonnet-5 (recommended)",
|
|
1789
|
-
value: "claude-sonnet-5",
|
|
1790
|
-
},
|
|
1791
|
-
{ name: "claude-sonnet-4-6", value: "claude-sonnet-4-6" },
|
|
1792
|
-
{
|
|
1793
|
-
name: "claude-haiku-4-5-20251001 (faster, cheaper)",
|
|
1794
|
-
value: "claude-haiku-4-5-20251001",
|
|
1795
|
-
},
|
|
1796
|
-
]
|
|
1797
|
-
: [
|
|
1798
|
-
{
|
|
1799
|
-
name: "claude-sonnet-4-6 (recommended)",
|
|
1800
|
-
value: "claude-sonnet-4-6",
|
|
1801
|
-
},
|
|
1802
|
-
{
|
|
1803
|
-
name: "claude-haiku-4-5 (faster, cheaper)",
|
|
1804
|
-
value: "claude-haiku-4-5",
|
|
1805
|
-
},
|
|
1806
|
-
];
|
|
1807
|
-
const distillModel = await select({
|
|
1808
|
-
message: "Distill model (deep extraction)",
|
|
1809
|
-
choices: distillChoices,
|
|
1810
|
-
});
|
|
1811
|
-
const filterThreshold = Number(await input({
|
|
1812
|
-
message: "Filter threshold (0..1)",
|
|
1813
|
-
default: String(existing?.filterThreshold ?? 0.4),
|
|
1814
|
-
validate: (v) => {
|
|
1815
|
-
const n = Number(v);
|
|
1816
|
-
return Number.isFinite(n) && n >= 0 && n <= 1
|
|
1817
|
-
? true
|
|
1818
|
-
: "must be between 0 and 1";
|
|
1819
|
-
},
|
|
1820
|
-
}));
|
|
1821
|
-
// ── project triage (the primary decision point) ─────────────────────────
|
|
1822
|
-
// Every project found under the Claude projects dir is shown once with its
|
|
1823
|
-
// session count and rough cost; selected = include, unselected = exclude.
|
|
1824
|
-
// Projects that appear later start undecided and trigger the run prompt.
|
|
1825
|
-
let projectDecisions = {};
|
|
1826
|
-
let agentTranscriptsAnswer;
|
|
1827
|
-
try {
|
|
1828
|
-
const projectsDirX = expandHome(claudeProjectsDir);
|
|
1829
|
-
const allFound = scanSessions(projectsDirX);
|
|
1830
|
-
// Triage counts + the agent-transcript question. Only top-level
|
|
1831
|
-
// transcripts count (nested workflow/sidechain have their own filter),
|
|
1832
|
-
// and agent transcripts are excluded from the per-project multi-select
|
|
1833
|
-
// numbers so the costs shown reflect what would actually distill.
|
|
1834
|
-
const topLevel = allFound.filter((s) => classifyTranscript(s.path, projectsDirX) === "session");
|
|
1835
|
-
const counts = { interactive: 0, agent: 0, stub: 0 };
|
|
1836
|
-
const agentPaths = new Set();
|
|
1837
|
-
for (const s of topLevel) {
|
|
1838
|
-
const cat = categorizeTranscriptHead(readTranscriptHead(s.path), s.size);
|
|
1839
|
-
counts[cat] += 1;
|
|
1840
|
-
if (cat === "agent")
|
|
1841
|
-
agentPaths.add(s.path);
|
|
1842
|
-
}
|
|
1843
|
-
if (counts.agent > 0) {
|
|
1844
|
-
ui.blank();
|
|
1845
|
-
ui.line(ui.dim(` ${counts.interactive} interactive · ${counts.agent} SDK-launched · ${counts.stub} stubs`));
|
|
1846
|
-
agentTranscriptsAnswer = (await select({
|
|
1847
|
-
message: "SDK-launched agent transcripts (review/verify harness output) — distill them?",
|
|
1848
|
-
default: existing?.agentTranscripts ?? "exclude",
|
|
1849
|
-
choices: [
|
|
1850
|
-
{
|
|
1851
|
-
name: "Exclude (recommended — agent-internal execution, not your decisions)",
|
|
1852
|
-
value: "exclude",
|
|
1853
|
-
},
|
|
1854
|
-
{ name: "Include (distill them like any session)", value: "include" },
|
|
1855
|
-
],
|
|
1856
|
-
}));
|
|
1857
|
-
}
|
|
1858
|
-
const found = agentTranscriptsAnswer !== "include"
|
|
1859
|
-
? topLevel.filter((s) => !agentPaths.has(s.path))
|
|
1860
|
-
: topLevel;
|
|
1861
|
-
const groups = [...groupByProject(found, projectsDirX).values()];
|
|
1862
|
-
if (groups.length > 0) {
|
|
1863
|
-
const classifyId = normalizeModelName(classifyModel, provider);
|
|
1864
|
-
const distillId = normalizeModelName(distillModel, provider);
|
|
1865
|
-
const infos = groups
|
|
1866
|
-
.map((g) => ({
|
|
1867
|
-
name: g.name,
|
|
1868
|
-
sessionCount: g.sessions.length,
|
|
1869
|
-
totalBytes: g.totalBytes,
|
|
1870
|
-
estCost: g.sessions.reduce((sum, s) => sum +
|
|
1871
|
-
estimateSessionCost(provider, classifyId, distillId, s.size), 0),
|
|
1872
|
-
}))
|
|
1873
|
-
.sort((a, b) => b.estCost - a.estCost);
|
|
1874
|
-
ui.blank();
|
|
1875
|
-
ui.line(ui.dim(" Which projects should vir distill? Costs are rough upper bounds."));
|
|
1876
|
-
projectDecisions = await promptProjectDecisions(infos, existing?.projects ?? {}, "Projects to track");
|
|
1877
|
-
}
|
|
1878
|
-
}
|
|
1879
|
-
catch (err) {
|
|
1880
|
-
console.warn(chalk.yellow(`project scan failed (${err.message}) — you can decide later with vir projects`));
|
|
1881
|
-
}
|
|
1882
|
-
const parsed = ConfigSchema.safeParse(buildInitConfig(existing, {
|
|
1883
|
-
vaultPath,
|
|
1884
|
-
outputDir,
|
|
1885
|
-
claudeProjectsDir,
|
|
1886
|
-
cadenceHours,
|
|
1887
|
-
provider,
|
|
1888
|
-
anthropicApiKey,
|
|
1889
|
-
kieApiKey,
|
|
1890
|
-
filterThreshold,
|
|
1891
|
-
articlesDir,
|
|
1892
|
-
pdfsDir,
|
|
1893
|
-
classifyModel,
|
|
1894
|
-
distillModel,
|
|
1895
|
-
projects: projectDecisions,
|
|
1896
|
-
agentTranscripts: agentTranscriptsAnswer,
|
|
1897
|
-
}));
|
|
1898
|
-
if (!parsed.success) {
|
|
1899
|
-
console.error(chalk.red("invalid config:"));
|
|
1900
|
-
for (const issue of parsed.error.issues) {
|
|
1901
|
-
console.error(` - ${issue.path.join(".")}: ${issue.message}`);
|
|
1902
|
-
}
|
|
1903
|
-
process.exitCode = 1;
|
|
1904
|
-
return;
|
|
1905
|
-
}
|
|
1906
|
-
saveConfig(parsed.data);
|
|
1907
|
-
ui.blank();
|
|
1908
|
-
ui.row(ui.success(ui.CHECK), ui.text(`saved ${CONFIG_PATH}`));
|
|
1909
|
-
ui.blank();
|
|
1910
|
-
const wantsMcp = await confirm({
|
|
1911
|
-
message: "Register Vir with Claude Code now? (recommended)",
|
|
1912
|
-
default: true,
|
|
1913
|
-
});
|
|
1914
|
-
if (wantsMcp) {
|
|
1915
|
-
await installToClaudeCode("user");
|
|
1916
|
-
}
|
|
1917
|
-
if (process.platform === "darwin" && parsed.data.notifications) {
|
|
1918
|
-
ui.blank();
|
|
1919
|
-
setupNotifications({ test: false });
|
|
1920
|
-
}
|
|
1921
|
-
ui.blank();
|
|
1922
|
-
ui.line(ui.dim("next: `vir run` to test once, then `vir schedule install`"));
|
|
1923
|
-
}
|
|
1924
868
|
function renderPlan(p) {
|
|
1925
869
|
const title = collapseHome(p.target);
|
|
1926
870
|
if (!p.exists) {
|
|
@@ -1948,25 +892,6 @@ function collapseHome(p) {
|
|
|
1948
892
|
const h = homedir();
|
|
1949
893
|
return p.startsWith(h) ? "~" + p.slice(h.length) : p;
|
|
1950
894
|
}
|
|
1951
|
-
function noteRefOf(r) {
|
|
1952
|
-
const dir = `${r.category}s`;
|
|
1953
|
-
const slug = r.topic
|
|
1954
|
-
.toLowerCase()
|
|
1955
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
1956
|
-
.replace(/^-+|-+$/g, "");
|
|
1957
|
-
return `${dir}/${slug}-${r.sessionId.slice(0, 8)}`;
|
|
1958
|
-
}
|
|
1959
|
-
function preview(s) {
|
|
1960
|
-
return s.replace(/\s+/g, " ").trim().slice(0, 80);
|
|
1961
|
-
}
|
|
1962
|
-
function safeLoad() {
|
|
1963
|
-
try {
|
|
1964
|
-
return loadConfig();
|
|
1965
|
-
}
|
|
1966
|
-
catch {
|
|
1967
|
-
return null;
|
|
1968
|
-
}
|
|
1969
|
-
}
|
|
1970
895
|
// Safety net for anything that escapes the per-action `runAction` wrapper (e.g.
|
|
1971
896
|
// commander-internal rejections before the action handler is reached). Set
|
|
1972
897
|
// `process.exitCode` instead of calling `process.exit` so buffered stdout/stderr
|