@djolex999/vir-cli 0.13.0 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -23,6 +23,10 @@ export function buildInitConfig(existing, a) {
23
23
  distillPdfs: existing?.distillPdfs,
24
24
  filterToolCalls: existing?.filterToolCalls,
25
25
  retrievalDiversity: existing?.retrievalDiversity,
26
+ projects: { ...existing?.projects, ...a.projects },
27
+ notifications: existing?.notifications,
28
+ workflowTranscripts: existing?.workflowTranscripts,
29
+ agentTranscripts: a.agentTranscripts ?? existing?.agentTranscripts,
26
30
  models: {
27
31
  classify: a.classifyModel,
28
32
  distill: a.distillModel,
@@ -1 +1 @@
1
- {"version":3,"file":"initConfig.js","sourceRoot":"","sources":["../../src/cli/initConfig.ts"],"names":[],"mappings":"AAiBA,iEAAiE;AACjE,0EAA0E;AAC1E,0EAA0E;AAC1E,2EAA2E;AAC3E,MAAM,UAAU,eAAe,CAC7B,QAAuB,EACvB,CAAc;IAEd,OAAO;QACL,SAAS,EAAE,CAAC,CAAC,SAAS;QACtB,SAAS,EAAE,CAAC,CAAC,SAAS;QACtB,iBAAiB,EAAE,CAAC,CAAC,iBAAiB;QACtC,YAAY,EAAE,CAAC,CAAC,YAAY;QAC5B,QAAQ,EAAE,CAAC,CAAC,QAAQ;QACpB,0EAA0E;QAC1E,4EAA4E;QAC5E,eAAe,EAAE,CAAC,CAAC,eAAe,IAAI,QAAQ,EAAE,eAAe;QAC/D,SAAS,EAAE,CAAC,CAAC,SAAS,IAAI,QAAQ,EAAE,SAAS;QAC7C,YAAY,EAAE,QAAQ,EAAE,YAAY;QACpC,SAAS,EAAE,QAAQ,EAAE,SAAS;QAC9B,OAAO,EAAE,QAAQ,EAAE,OAAO;QAC1B,eAAe,EAAE,CAAC,CAAC,eAAe;QAClC,WAAW,EAAE,CAAC,CAAC,WAAW;QAC1B,eAAe,EAAE,QAAQ,EAAE,eAAe;QAC1C,OAAO,EAAE,CAAC,CAAC,OAAO;QAClB,WAAW,EAAE,QAAQ,EAAE,WAAW;QAClC,eAAe,EAAE,QAAQ,EAAE,eAAe;QAC1C,kBAAkB,EAAE,QAAQ,EAAE,kBAAkB;QAChD,MAAM,EAAE;YACN,QAAQ,EAAE,CAAC,CAAC,aAAa;YACzB,OAAO,EAAE,CAAC,CAAC,YAAY;YACvB,yEAAyE;YACzE,mEAAmE;YACnE,WAAW,EACT,QAAQ,EAAE,MAAM,EAAE,WAAW;gBAC7B,CAAC,CAAC,CAAC,QAAQ,KAAK,WAAW;oBACzB,CAAC,CAAC,2BAA2B;oBAC7B,CAAC,CAAC,kBAAkB,CAAC;YACzB,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,gBAAgB,IAAI,IAAI;gBAC5C,CAAC,CAAC,EAAE,gBAAgB,EAAE,QAAQ,CAAC,MAAM,CAAC,gBAAgB,EAAE;gBACxD,CAAC,CAAC,EAAE,CAAC;SACR;KACF,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"initConfig.js","sourceRoot":"","sources":["../../src/cli/initConfig.ts"],"names":[],"mappings":"AAuBA,iEAAiE;AACjE,0EAA0E;AAC1E,0EAA0E;AAC1E,2EAA2E;AAC3E,MAAM,UAAU,eAAe,CAC7B,QAAuB,EACvB,CAAc;IAEd,OAAO;QACL,SAAS,EAAE,CAAC,CAAC,SAAS;QACtB,SAAS,EAAE,CAAC,CAAC,SAAS;QACtB,iBAAiB,EAAE,CAAC,CAAC,iBAAiB;QACtC,YAAY,EAAE,CAAC,CAAC,YAAY;QAC5B,QAAQ,EAAE,CAAC,CAAC,QAAQ;QACpB,0EAA0E;QAC1E,4EAA4E;QAC5E,eAAe,EAAE,CAAC,CAAC,eAAe,IAAI,QAAQ,EAAE,eAAe;QAC/D,SAAS,EAAE,CAAC,CAAC,SAAS,IAAI,QAAQ,EAAE,SAAS;QAC7C,YAAY,EAAE,QAAQ,EAAE,YAAY;QACpC,SAAS,EAAE,QAAQ,EAAE,SAAS;QAC9B,OAAO,EAAE,QAAQ,EAAE,OAAO;QAC1B,eAAe,EAAE,CAAC,CAAC,eAAe;QAClC,WAAW,EAAE,CAAC,CAAC,WAAW;QAC1B,eAAe,EAAE,QAAQ,EAAE,eAAe;QAC1C,OAAO,EAAE,CAAC,CAAC,OAAO;QAClB,WAAW,EAAE,QAAQ,EAAE,WAAW;QAClC,eAAe,EAAE,QAAQ,EAAE,eAAe;QAC1C,kBAAkB,EAAE,QAAQ,EAAE,kBAAkB;QAChD,QAAQ,EAAE,EAAE,GAAG,QAAQ,EAAE,QAAQ,EAAE,GAAG,CAAC,CAAC,QAAQ,EAAE;QAClD,aAAa,EAAE,QAAQ,EAAE,aAAa;QACtC,mBAAmB,EAAE,QAAQ,EAAE,mBAAmB;QAClD,gBAAgB,EAAE,CAAC,CAAC,gBAAgB,IAAI,QAAQ,EAAE,gBAAgB;QAClE,MAAM,EAAE;YACN,QAAQ,EAAE,CAAC,CAAC,aAAa;YACzB,OAAO,EAAE,CAAC,CAAC,YAAY;YACvB,yEAAyE;YACzE,mEAAmE;YACnE,WAAW,EACT,QAAQ,EAAE,MAAM,EAAE,WAAW;gBAC7B,CAAC,CAAC,CAAC,QAAQ,KAAK,WAAW;oBACzB,CAAC,CAAC,2BAA2B;oBAC7B,CAAC,CAAC,kBAAkB,CAAC;YACzB,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,gBAAgB,IAAI,IAAI;gBAC5C,CAAC,CAAC,EAAE,gBAAgB,EAAE,QAAQ,CAAC,MAAM,CAAC,gBAAgB,EAAE;gBACxD,CAAC,CAAC,EAAE,CAAC;SACR;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,51 @@
1
+ import checkbox from "@inquirer/checkbox";
2
+ import { readFileSync } from "node:fs";
3
+ import { CONFIG_PATH, ConfigSchema, saveConfig, } from "../config.js";
4
+ import * as ui from "../ui/display.js";
5
+ export function buildProjectChoices(projects, existing) {
6
+ const nameWidth = Math.min(Math.max(...projects.map((p) => p.name.length), 8), 40);
7
+ // Default is over-capture: everything not explicitly excluded starts
8
+ // CHECKED. Transcripts prune at ~30 days, so an accidental enter that
9
+ // includes too much costs cents; one that excludes too much loses the
10
+ // sessions forever.
11
+ return projects.map((p) => ({
12
+ name: `${p.name.padEnd(nameWidth)} ${String(p.sessionCount).padStart(3)} session${p.sessionCount === 1 ? " " : "s"} ~${ui.formatUsd(p.estCost)}`,
13
+ value: p.name,
14
+ checked: existing[p.name] !== "exclude",
15
+ }));
16
+ }
17
+ // Selected = include, unselected = exclude. Every project SHOWN gets decided —
18
+ // that is the point of the triage pass; only projects never shown stay
19
+ // undecided.
20
+ export function decisionsFromSelection(projects, selected) {
21
+ const chosen = new Set(selected);
22
+ const out = {};
23
+ for (const p of projects) {
24
+ out[p.name] = chosen.has(p.name) ? "include" : "exclude";
25
+ }
26
+ return out;
27
+ }
28
+ export async function promptProjectDecisions(projects, existing, message) {
29
+ const selected = await checkbox({
30
+ message,
31
+ choices: buildProjectChoices(projects, existing),
32
+ pageSize: 15,
33
+ });
34
+ return decisionsFromSelection(projects, selected);
35
+ }
36
+ // Persist decisions by patching the RAW config file, not the loaded Config —
37
+ // loadConfig expands ~ paths, and writing those back would bake absolute
38
+ // paths into the user's config. Validated through the schema before saving.
39
+ export function persistProjectDecisions(decisions) {
40
+ const raw = JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
41
+ raw.projects = {
42
+ ...(raw.projects ?? {}),
43
+ ...decisions,
44
+ };
45
+ const parsed = ConfigSchema.safeParse(raw);
46
+ if (!parsed.success) {
47
+ throw new Error(`refusing to save invalid config: ${parsed.error.issues[0]?.message ?? "validation failed"}`);
48
+ }
49
+ saveConfig(raw);
50
+ }
51
+ //# sourceMappingURL=projectSelect.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"projectSelect.js","sourceRoot":"","sources":["../../src/cli/projectSelect.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,119 @@
1
+ import { classifyTranscript, estimateSessionCost, groupByProject, readTranscriptHead, sniffAgentEntrypoint, } from "../pipeline/projects.js";
2
+ import { normalizeModelName } from "../pipeline/distiller.js";
3
+ import { scanSessions } from "../pipeline/scanner.js";
4
+ import { StateDb } from "../state/db.js";
5
+ // Aggregate the scan (every project seen on disk) against DB state into the
6
+ // `vir projects` table. Pure — the command wires in scan/DB/config and an
7
+ // estimator; classification per session:
8
+ // distilled row with content (skipped=0)
9
+ // pending recorded "project-pending", or unseen under an
10
+ // undecided project (it WILL pend on the next run)
11
+ // excluded recorded "project-excluded", or unseen under an
12
+ // excluded project
13
+ // neither heuristic-filter skips, errors, unseen under include
14
+ export function buildProjectsReport(groups, meta, decisions, estCost, nested) {
15
+ const byPath = new Map(meta.map((m) => [m.path, m]));
16
+ const rows = [];
17
+ for (const g of groups) {
18
+ const decision = decisions[g.name] ?? "undecided";
19
+ const nestedCounts = nested?.get(g.name);
20
+ const row = {
21
+ name: g.name,
22
+ decision,
23
+ sessions: g.sessions.length,
24
+ distilled: 0,
25
+ pending: 0,
26
+ excluded: 0,
27
+ estPendingCost: 0,
28
+ pendingPaths: [],
29
+ workflowSessions: nestedCounts?.workflow ?? 0,
30
+ sidechainSessions: nestedCounts?.sidechain ?? 0,
31
+ agentSessions: nestedCounts?.agent ?? 0,
32
+ };
33
+ for (const s of g.sessions) {
34
+ const m = byPath.get(s.path);
35
+ if (m && m.skipped === 0 && m.hasContent === 1) {
36
+ row.distilled += 1;
37
+ continue;
38
+ }
39
+ const reason = m?.skipReason ?? null;
40
+ if (reason === "project-pending" || (!m && decision === "undecided")) {
41
+ row.pending += 1;
42
+ row.estPendingCost += estCost(s.size);
43
+ row.pendingPaths.push(s.path);
44
+ }
45
+ else if (reason === "project-excluded" ||
46
+ (!m && decision === "exclude")) {
47
+ row.excluded += 1;
48
+ }
49
+ }
50
+ rows.push(row);
51
+ }
52
+ rows.sort((a, b) => b.estPendingCost - a.estPendingCost || a.name.localeCompare(b.name));
53
+ return rows;
54
+ }
55
+ // Side-effectful gather for `vir projects` and doctor: scan the transcripts
56
+ // dir, read DB status, apply config decisions. Opens its own StateDb unless
57
+ // one is passed in.
58
+ export function gatherProjectsReport(cfg) {
59
+ const found = scanSessions(cfg.claudeProjectsDir);
60
+ // Partition out nested agent-internal transcripts (unless the knob says
61
+ // include) so they annotate the table instead of counting as sessions.
62
+ let inScope = found;
63
+ const nested = new Map();
64
+ const filteredOut = [];
65
+ const catByPath = new Map();
66
+ if (cfg.workflowTranscripts !== "include") {
67
+ inScope = [];
68
+ for (const s of found) {
69
+ const cat = classifyTranscript(s.path, cfg.claudeProjectsDir);
70
+ if (cat === "session")
71
+ inScope.push(s);
72
+ else {
73
+ filteredOut.push(s);
74
+ catByPath.set(s.path, cat);
75
+ }
76
+ }
77
+ }
78
+ // Top-level SDK-launched agents — same partition, its own category/knob.
79
+ if (cfg.agentTranscripts !== "include") {
80
+ const stillHuman = [];
81
+ for (const s of inScope) {
82
+ if (sniffAgentEntrypoint(readTranscriptHead(s.path)) !== null) {
83
+ filteredOut.push(s);
84
+ catByPath.set(s.path, "agent");
85
+ }
86
+ else {
87
+ stillHuman.push(s);
88
+ }
89
+ }
90
+ inScope = stillHuman;
91
+ }
92
+ for (const g of groupByProject(filteredOut, cfg.claudeProjectsDir).values()) {
93
+ const counts = { workflow: 0, sidechain: 0, agent: 0 };
94
+ for (const s of g.sessions) {
95
+ counts[catByPath.get(s.path) ?? "sidechain"] += 1;
96
+ }
97
+ nested.set(g.name, counts);
98
+ }
99
+ const groups = [...groupByProject(inScope, cfg.claudeProjectsDir).values()];
100
+ // A project whose transcripts are ALL nested would vanish from the table —
101
+ // keep it visible with an empty session group.
102
+ for (const name of nested.keys()) {
103
+ if (!groups.some((g) => g.name === name)) {
104
+ groups.push({ name, sessions: [], totalBytes: 0 });
105
+ }
106
+ }
107
+ const db = new StateDb();
108
+ let meta;
109
+ try {
110
+ meta = db.listSessionMeta();
111
+ }
112
+ finally {
113
+ db.close();
114
+ }
115
+ const classifyId = normalizeModelName(cfg.models.classify, cfg.provider);
116
+ const distillId = normalizeModelName(cfg.models.distill, cfg.provider);
117
+ return buildProjectsReport(groups, meta, cfg.projects, (size) => estimateSessionCost(cfg.provider, classifyId, distillId, size, cfg.pricing, cfg.kieTopUpTier), nested);
118
+ }
119
+ //# sourceMappingURL=projects.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"projects.js","sourceRoot":"","sources":["../../src/cli/projects.ts"],"names":[],"mappings":"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"}
package/dist/cli.js CHANGED
@@ -10,12 +10,15 @@ import { existsSync, mkdirSync, readFileSync } from "node:fs";
10
10
  import { homedir } from "node:os";
11
11
  import { basename, dirname, join } from "node:path";
12
12
  import { fileURLToPath } from "node:url";
13
- import { CONFIG_PATH, ConfigSchema, configExists, ensureVirDir, expandHome, loadConfig, saveConfig, } from "./config.js";
13
+ import { CONFIG_PATH, maskSecret, ConfigSchema, configExists, ensureVirDir, expandHome, loadConfig, saveConfig, } from "./config.js";
14
14
  import { applyPlan, planUpdates } from "./claude/updater.js";
15
15
  import { detectDuplicates } from "./dedupe/detector.js";
16
16
  import { mergeNotes } from "./dedupe/merger.js";
17
17
  import { contradictionCheck, orphanCheck, stalenessCheck, } from "./lint/linter.js";
18
18
  import { runPipeline } from "./pipeline/run.js";
19
+ import { categorizeTranscriptHead, classifyTranscript, estimateSessionCost, groupByProject, readTranscriptHead, } from "./pipeline/projects.js";
20
+ import { persistProjectDecisions, promptProjectDecisions, } from "./cli/projectSelect.js";
21
+ import { gatherProjectsReport } from "./cli/projects.js";
19
22
  import { scanSessions } from "./pipeline/scanner.js";
20
23
  import { parseSession } from "./pipeline/parser.js";
21
24
  import { scoreSession } from "./pipeline/filter.js";
@@ -70,6 +73,8 @@ program
70
73
  .option("--yes", "Skip the cost confirmation prompt")
71
74
  .option("--force-model <model>", "Override the distill model for this run only: haiku | sonnet")
72
75
  .option("--dry-run", "Estimate per-session cost after filtering, then exit before any LLM call")
76
+ .option("--only <project>", "Restrict this run to a project (repeatable, never persisted)", (v, acc) => acc.concat(v), [])
77
+ .option("--exclude-project <project>", "Skip a project for this run only (repeatable, never persisted)", (v, acc) => acc.concat(v), [])
73
78
  .action(runAction(async (opts) => {
74
79
  const cfg = loadConfig();
75
80
  const daemon = opts.daemon === true;
@@ -104,17 +109,33 @@ program
104
109
  throw err;
105
110
  }
106
111
  }
112
+ // Interactivity for the undecided-projects triage is a TTY question,
113
+ // not a --yes question: --yes skips the COST prompt but a human at a
114
+ // terminal can still answer include/exclude. launchd/cron have no
115
+ // TTY, so the daemon path can never reach the prompt.
116
+ const canPromptProjects = process.stdin.isTTY === true && !daemon && !dryRun;
107
117
  let summary;
108
118
  try {
109
119
  summary = await runPipeline(cfg, {
110
120
  full: opts.full,
111
121
  quiet: daemon,
112
- logToFile: daemon,
113
122
  rewriteOnly,
114
123
  articlesOnly,
115
124
  pdfsOnly,
116
125
  forceDistillModel: opts.forceModel,
117
126
  dryRun,
127
+ onlyProjects: opts.only?.length ? opts.only : undefined,
128
+ excludeProjects: opts.excludeProject?.length
129
+ ? opts.excludeProject
130
+ : undefined,
131
+ onUndecidedProjects: canPromptProjects
132
+ ? async (pending) => {
133
+ const answers = await promptProjectDecisions(pending, cfg.projects, `${pending.length} project(s) have sessions awaiting a decision — select the ones vir should track`);
134
+ persistProjectDecisions(answers);
135
+ ui.row(ui.success(ui.CHECK), ui.text(`saved ${Object.keys(answers).length} project decision(s)`));
136
+ return answers;
137
+ }
138
+ : undefined,
118
139
  onConfirm: skipPrompt
119
140
  ? undefined
120
141
  : async (newCount) => confirmCostIfNeeded(cfg, newCount),
@@ -939,6 +960,70 @@ program
939
960
  db.close();
940
961
  }
941
962
  }));
963
+ const projectsCmd = program
964
+ .command("projects")
965
+ .description("Per-project distillation decisions: table + include/exclude");
966
+ projectsCmd
967
+ .option("--json", "Machine-readable output")
968
+ .action(runAction(async (opts) => {
969
+ const cfg = loadConfig();
970
+ const rows = gatherProjectsReport(cfg);
971
+ if (opts.json === true) {
972
+ process.stdout.write(JSON.stringify(rows.map(({ pendingPaths: _pendingPaths, ...r }) => r), null, 2) + "\n");
973
+ return;
974
+ }
975
+ ui.header("projects");
976
+ ui.blank();
977
+ if (rows.length === 0) {
978
+ ui.line(ui.dim(" no projects found under the Claude projects dir"));
979
+ return;
980
+ }
981
+ const nameW = Math.min(Math.max(...rows.map((r) => r.name.length), 7), 44);
982
+ ui.line(ui.dim(` ${"project".padEnd(nameW)} decision sessions distilled pending excluded est. pending`));
983
+ for (const r of rows) {
984
+ const decision = r.decision === "include"
985
+ ? ui.success("include ")
986
+ : r.decision === "exclude"
987
+ ? ui.dim("exclude ")
988
+ : ui.warn("undecided");
989
+ const nestedParts = [
990
+ ...(r.workflowSessions > 0 ? [`+${r.workflowSessions} workflow`] : []),
991
+ ...(r.sidechainSessions > 0
992
+ ? [`+${r.sidechainSessions} sidechain`]
993
+ : []),
994
+ ...(r.agentSessions > 0 ? [`+${r.agentSessions} agent`] : []),
995
+ ];
996
+ const nestedNote = nestedParts.length > 0 ? ui.dim(` (${nestedParts.join(", ")})`) : "";
997
+ ui.line(` ${ui.text(r.name.padEnd(nameW))} ${decision} ${String(r.sessions).padStart(8)} ${String(r.distilled).padStart(9)} ${String(r.pending).padStart(7)} ${String(r.excluded).padStart(8)} ${r.pending > 0 ? ui.warn(ui.formatUsd(r.estPendingCost).padStart(12)) : ui.dim("—".padStart(12))}${nestedNote}`);
998
+ }
999
+ const pending = rows.filter((r) => r.decision === "undecided" && r.pending > 0);
1000
+ if (pending.length > 0) {
1001
+ ui.blank();
1002
+ ui.line(ui.dim(` ${pending.length} project(s) undecided — vir projects include|exclude <name>, or answer the prompt on the next interactive vir run`));
1003
+ }
1004
+ }));
1005
+ async function cmdProjectDecision(name, decision) {
1006
+ const cfg = loadConfig();
1007
+ const rows = gatherProjectsReport(cfg);
1008
+ const known = rows.find((r) => r.name === name);
1009
+ if (!known) {
1010
+ console.error(chalk.red(`unknown project '${name}' — seen projects: ${rows.map((r) => r.name).join(", ") || "(none)"}`));
1011
+ process.exitCode = 1;
1012
+ return;
1013
+ }
1014
+ persistProjectDecisions({ [name]: decision });
1015
+ ui.row(ui.success(ui.CHECK), ui.text(decision === "include"
1016
+ ? `${name} will be distilled from the next run (${known.pending} session(s) pending)`
1017
+ : `${name} excluded — forward-looking only; already-distilled notes stay in the vault`));
1018
+ }
1019
+ projectsCmd
1020
+ .command("include <name>")
1021
+ .description("Track a project's sessions from now on")
1022
+ .action(runAction(async (name) => cmdProjectDecision(name, "include")));
1023
+ projectsCmd
1024
+ .command("exclude <name>")
1025
+ .description("Stop tracking a project (existing notes are untouched)")
1026
+ .action(runAction(async (name) => cmdProjectDecision(name, "exclude")));
942
1027
  program
943
1028
  .command("status")
944
1029
  .description("Show processing status + knowledge base breakdown")
@@ -1265,30 +1350,36 @@ async function cmdInit() {
1265
1350
  default: existing?.provider ?? "anthropic",
1266
1351
  choices: [
1267
1352
  {
1268
- name: "Anthropic (direct, official pricing)",
1353
+ name: "Anthropic (direct + reliable, official pricing — recommended)",
1269
1354
  value: "anthropic",
1270
1355
  },
1271
1356
  {
1272
- name: "Kie.ai (same Claude models, ~72% cheaper)",
1357
+ name: "Kie.ai (~72% cheaper, third-party proxy — you trade reliability for cost)",
1273
1358
  value: "kie",
1274
1359
  },
1275
1360
  ],
1276
1361
  }));
1362
+ // Secret prompt discipline: mask while typing, echo only a masked
1363
+ // confirmation (maskSecret), and NEVER render the existing key as the
1364
+ // visible inquirer default — that prints the full secret to the terminal
1365
+ // before a single keystroke. Empty input keeps the existing/env key.
1366
+ const promptSecret = async (message, keep, validate) => {
1367
+ const value = await input({
1368
+ message: keep
1369
+ ? `${message} (Enter keeps ${maskSecret(keep)})`
1370
+ : message,
1371
+ transformer: (v, { isFinal }) => isFinal ? maskSecret(v || keep || "") : "•".repeat(v.length),
1372
+ validate: (v) => (v === "" && keep ? true : validate(v)),
1373
+ });
1374
+ return value === "" && keep ? keep : value;
1375
+ };
1277
1376
  let anthropicApiKey;
1278
1377
  let kieApiKey;
1279
1378
  if (provider === "anthropic") {
1280
- anthropicApiKey = await input({
1281
- message: "Anthropic API key",
1282
- default: existing?.anthropicApiKey ?? process.env.ANTHROPIC_API_KEY ?? "",
1283
- validate: (v) => v.startsWith("sk-ant-") ? true : "key should start with sk-ant-",
1284
- });
1379
+ 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-"));
1285
1380
  }
1286
1381
  else {
1287
- kieApiKey = await input({
1288
- message: "Kie.ai API key",
1289
- default: existing?.kieApiKey ?? process.env.KIE_API_KEY ?? "",
1290
- validate: (v) => v.length > 10 ? true : "enter a valid Kie.ai API key",
1291
- });
1382
+ 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"));
1292
1383
  }
1293
1384
  // ── model pickers (provider-aware) ──────────────────────────────────────
1294
1385
  const classifyChoices = provider === "anthropic"
@@ -1313,9 +1404,10 @@ async function cmdInit() {
1313
1404
  const distillChoices = provider === "anthropic"
1314
1405
  ? [
1315
1406
  {
1316
- name: "claude-sonnet-4-6 (recommended)",
1317
- value: "claude-sonnet-4-6",
1407
+ name: "claude-sonnet-5 (recommended)",
1408
+ value: "claude-sonnet-5",
1318
1409
  },
1410
+ { name: "claude-sonnet-4-6", value: "claude-sonnet-4-6" },
1319
1411
  {
1320
1412
  name: "claude-haiku-4-5-20251001 (faster, cheaper)",
1321
1413
  value: "claude-haiku-4-5-20251001",
@@ -1345,6 +1437,67 @@ async function cmdInit() {
1345
1437
  : "must be between 0 and 1";
1346
1438
  },
1347
1439
  }));
1440
+ // ── project triage (the primary decision point) ─────────────────────────
1441
+ // Every project found under the Claude projects dir is shown once with its
1442
+ // session count and rough cost; selected = include, unselected = exclude.
1443
+ // Projects that appear later start undecided and trigger the run prompt.
1444
+ let projectDecisions = {};
1445
+ let agentTranscriptsAnswer;
1446
+ try {
1447
+ const projectsDirX = expandHome(claudeProjectsDir);
1448
+ const allFound = scanSessions(projectsDirX);
1449
+ // Triage counts + the agent-transcript question. Only top-level
1450
+ // transcripts count (nested workflow/sidechain have their own filter),
1451
+ // and agent transcripts are excluded from the per-project multi-select
1452
+ // numbers so the costs shown reflect what would actually distill.
1453
+ const topLevel = allFound.filter((s) => classifyTranscript(s.path, projectsDirX) === "session");
1454
+ const counts = { interactive: 0, agent: 0, stub: 0 };
1455
+ const agentPaths = new Set();
1456
+ for (const s of topLevel) {
1457
+ const cat = categorizeTranscriptHead(readTranscriptHead(s.path), s.size);
1458
+ counts[cat] += 1;
1459
+ if (cat === "agent")
1460
+ agentPaths.add(s.path);
1461
+ }
1462
+ if (counts.agent > 0) {
1463
+ ui.blank();
1464
+ ui.line(ui.dim(` ${counts.interactive} interactive · ${counts.agent} SDK-launched · ${counts.stub} stubs`));
1465
+ agentTranscriptsAnswer = (await select({
1466
+ message: "SDK-launched agent transcripts (review/verify harness output) — distill them?",
1467
+ default: existing?.agentTranscripts ?? "exclude",
1468
+ choices: [
1469
+ {
1470
+ name: "Exclude (recommended — agent-internal execution, not your decisions)",
1471
+ value: "exclude",
1472
+ },
1473
+ { name: "Include (distill them like any session)", value: "include" },
1474
+ ],
1475
+ }));
1476
+ }
1477
+ const found = agentTranscriptsAnswer !== "include"
1478
+ ? topLevel.filter((s) => !agentPaths.has(s.path))
1479
+ : topLevel;
1480
+ const groups = [...groupByProject(found, projectsDirX).values()];
1481
+ if (groups.length > 0) {
1482
+ const classifyId = normalizeModelName(classifyModel, provider);
1483
+ const distillId = normalizeModelName(distillModel, provider);
1484
+ const infos = groups
1485
+ .map((g) => ({
1486
+ name: g.name,
1487
+ sessionCount: g.sessions.length,
1488
+ totalBytes: g.totalBytes,
1489
+ estCost: g.sessions.reduce((sum, s) => sum +
1490
+ estimateSessionCost(provider, classifyId, distillId, s.size), 0),
1491
+ }))
1492
+ .sort((a, b) => b.estCost - a.estCost);
1493
+ ui.blank();
1494
+ ui.line(ui.dim(" Which projects should vir distill? Costs are rough upper bounds."));
1495
+ projectDecisions = await promptProjectDecisions(infos, existing?.projects ?? {}, "Projects to track");
1496
+ }
1497
+ }
1498
+ catch (err) {
1499
+ console.warn(chalk.yellow(`project scan failed (${err.message}) — you can decide later with vir projects`));
1500
+ }
1348
1501
  const parsed = ConfigSchema.safeParse(buildInitConfig(existing, {
1349
1502
  vaultPath,
1350
1503
  outputDir,
@@ -1358,6 +1511,8 @@ async function cmdInit() {
1358
1511
  pdfsDir,
1359
1512
  classifyModel,
1360
1513
  distillModel,
1514
+ projects: projectDecisions,
1515
+ agentTranscripts: agentTranscriptsAnswer,
1361
1516
  }));
1362
1517
  if (!parsed.success) {
1363
1518
  console.error(chalk.red("invalid config:"));