@djolex999/vir-cli 0.13.0 → 0.14.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.
@@ -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,6 +109,11 @@ 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, {
@@ -115,6 +125,18 @@ program
115
125
  pdfsOnly,
116
126
  forceDistillModel: opts.forceModel,
117
127
  dryRun,
128
+ onlyProjects: opts.only?.length ? opts.only : undefined,
129
+ excludeProjects: opts.excludeProject?.length
130
+ ? opts.excludeProject
131
+ : undefined,
132
+ onUndecidedProjects: canPromptProjects
133
+ ? async (pending) => {
134
+ const answers = await promptProjectDecisions(pending, cfg.projects, `${pending.length} project(s) have sessions awaiting a decision — select the ones vir should track`);
135
+ persistProjectDecisions(answers);
136
+ ui.row(ui.success(ui.CHECK), ui.text(`saved ${Object.keys(answers).length} project decision(s)`));
137
+ return answers;
138
+ }
139
+ : undefined,
118
140
  onConfirm: skipPrompt
119
141
  ? undefined
120
142
  : async (newCount) => confirmCostIfNeeded(cfg, newCount),
@@ -939,6 +961,70 @@ program
939
961
  db.close();
940
962
  }
941
963
  }));
964
+ const projectsCmd = program
965
+ .command("projects")
966
+ .description("Per-project distillation decisions: table + include/exclude");
967
+ projectsCmd
968
+ .option("--json", "Machine-readable output")
969
+ .action(runAction(async (opts) => {
970
+ const cfg = loadConfig();
971
+ const rows = gatherProjectsReport(cfg);
972
+ if (opts.json === true) {
973
+ process.stdout.write(JSON.stringify(rows.map(({ pendingPaths: _pendingPaths, ...r }) => r), null, 2) + "\n");
974
+ return;
975
+ }
976
+ ui.header("projects");
977
+ ui.blank();
978
+ if (rows.length === 0) {
979
+ ui.line(ui.dim(" no projects found under the Claude projects dir"));
980
+ return;
981
+ }
982
+ const nameW = Math.min(Math.max(...rows.map((r) => r.name.length), 7), 44);
983
+ ui.line(ui.dim(` ${"project".padEnd(nameW)} decision sessions distilled pending excluded est. pending`));
984
+ for (const r of rows) {
985
+ const decision = r.decision === "include"
986
+ ? ui.success("include ")
987
+ : r.decision === "exclude"
988
+ ? ui.dim("exclude ")
989
+ : ui.warn("undecided");
990
+ const nestedParts = [
991
+ ...(r.workflowSessions > 0 ? [`+${r.workflowSessions} workflow`] : []),
992
+ ...(r.sidechainSessions > 0
993
+ ? [`+${r.sidechainSessions} sidechain`]
994
+ : []),
995
+ ...(r.agentSessions > 0 ? [`+${r.agentSessions} agent`] : []),
996
+ ];
997
+ const nestedNote = nestedParts.length > 0 ? ui.dim(` (${nestedParts.join(", ")})`) : "";
998
+ 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}`);
999
+ }
1000
+ const pending = rows.filter((r) => r.decision === "undecided" && r.pending > 0);
1001
+ if (pending.length > 0) {
1002
+ ui.blank();
1003
+ ui.line(ui.dim(` ${pending.length} project(s) undecided — vir projects include|exclude <name>, or answer the prompt on the next interactive vir run`));
1004
+ }
1005
+ }));
1006
+ async function cmdProjectDecision(name, decision) {
1007
+ const cfg = loadConfig();
1008
+ const rows = gatherProjectsReport(cfg);
1009
+ const known = rows.find((r) => r.name === name);
1010
+ if (!known) {
1011
+ console.error(chalk.red(`unknown project '${name}' — seen projects: ${rows.map((r) => r.name).join(", ") || "(none)"}`));
1012
+ process.exitCode = 1;
1013
+ return;
1014
+ }
1015
+ persistProjectDecisions({ [name]: decision });
1016
+ ui.row(ui.success(ui.CHECK), ui.text(decision === "include"
1017
+ ? `${name} will be distilled from the next run (${known.pending} session(s) pending)`
1018
+ : `${name} excluded — forward-looking only; already-distilled notes stay in the vault`));
1019
+ }
1020
+ projectsCmd
1021
+ .command("include <name>")
1022
+ .description("Track a project's sessions from now on")
1023
+ .action(runAction(async (name) => cmdProjectDecision(name, "include")));
1024
+ projectsCmd
1025
+ .command("exclude <name>")
1026
+ .description("Stop tracking a project (existing notes are untouched)")
1027
+ .action(runAction(async (name) => cmdProjectDecision(name, "exclude")));
942
1028
  program
943
1029
  .command("status")
944
1030
  .description("Show processing status + knowledge base breakdown")
@@ -1265,30 +1351,36 @@ async function cmdInit() {
1265
1351
  default: existing?.provider ?? "anthropic",
1266
1352
  choices: [
1267
1353
  {
1268
- name: "Anthropic (direct, official pricing)",
1354
+ name: "Anthropic (direct + reliable, official pricing — recommended)",
1269
1355
  value: "anthropic",
1270
1356
  },
1271
1357
  {
1272
- name: "Kie.ai (same Claude models, ~72% cheaper)",
1358
+ name: "Kie.ai (~72% cheaper, third-party proxy — you trade reliability for cost)",
1273
1359
  value: "kie",
1274
1360
  },
1275
1361
  ],
1276
1362
  }));
1363
+ // Secret prompt discipline: mask while typing, echo only a masked
1364
+ // confirmation (maskSecret), and NEVER render the existing key as the
1365
+ // visible inquirer default — that prints the full secret to the terminal
1366
+ // before a single keystroke. Empty input keeps the existing/env key.
1367
+ const promptSecret = async (message, keep, validate) => {
1368
+ const value = await input({
1369
+ message: keep
1370
+ ? `${message} (Enter keeps ${maskSecret(keep)})`
1371
+ : message,
1372
+ transformer: (v, { isFinal }) => isFinal ? maskSecret(v || keep || "") : "•".repeat(v.length),
1373
+ validate: (v) => (v === "" && keep ? true : validate(v)),
1374
+ });
1375
+ return value === "" && keep ? keep : value;
1376
+ };
1277
1377
  let anthropicApiKey;
1278
1378
  let kieApiKey;
1279
1379
  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
- });
1380
+ 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
1381
  }
1286
1382
  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
- });
1383
+ 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
1384
  }
1293
1385
  // ── model pickers (provider-aware) ──────────────────────────────────────
1294
1386
  const classifyChoices = provider === "anthropic"
@@ -1313,9 +1405,10 @@ async function cmdInit() {
1313
1405
  const distillChoices = provider === "anthropic"
1314
1406
  ? [
1315
1407
  {
1316
- name: "claude-sonnet-4-6 (recommended)",
1317
- value: "claude-sonnet-4-6",
1408
+ name: "claude-sonnet-5 (recommended)",
1409
+ value: "claude-sonnet-5",
1318
1410
  },
1411
+ { name: "claude-sonnet-4-6", value: "claude-sonnet-4-6" },
1319
1412
  {
1320
1413
  name: "claude-haiku-4-5-20251001 (faster, cheaper)",
1321
1414
  value: "claude-haiku-4-5-20251001",
@@ -1345,6 +1438,67 @@ async function cmdInit() {
1345
1438
  : "must be between 0 and 1";
1346
1439
  },
1347
1440
  }));
1441
+ // ── project triage (the primary decision point) ─────────────────────────
1442
+ // Every project found under the Claude projects dir is shown once with its
1443
+ // session count and rough cost; selected = include, unselected = exclude.
1444
+ // Projects that appear later start undecided and trigger the run prompt.
1445
+ let projectDecisions = {};
1446
+ let agentTranscriptsAnswer;
1447
+ try {
1448
+ const projectsDirX = expandHome(claudeProjectsDir);
1449
+ const allFound = scanSessions(projectsDirX);
1450
+ // Triage counts + the agent-transcript question. Only top-level
1451
+ // transcripts count (nested workflow/sidechain have their own filter),
1452
+ // and agent transcripts are excluded from the per-project multi-select
1453
+ // numbers so the costs shown reflect what would actually distill.
1454
+ const topLevel = allFound.filter((s) => classifyTranscript(s.path, projectsDirX) === "session");
1455
+ const counts = { interactive: 0, agent: 0, stub: 0 };
1456
+ const agentPaths = new Set();
1457
+ for (const s of topLevel) {
1458
+ const cat = categorizeTranscriptHead(readTranscriptHead(s.path), s.size);
1459
+ counts[cat] += 1;
1460
+ if (cat === "agent")
1461
+ agentPaths.add(s.path);
1462
+ }
1463
+ if (counts.agent > 0) {
1464
+ ui.blank();
1465
+ ui.line(ui.dim(` ${counts.interactive} interactive · ${counts.agent} SDK-launched · ${counts.stub} stubs`));
1466
+ agentTranscriptsAnswer = (await select({
1467
+ message: "SDK-launched agent transcripts (review/verify harness output) — distill them?",
1468
+ default: existing?.agentTranscripts ?? "exclude",
1469
+ choices: [
1470
+ {
1471
+ name: "Exclude (recommended — agent-internal execution, not your decisions)",
1472
+ value: "exclude",
1473
+ },
1474
+ { name: "Include (distill them like any session)", value: "include" },
1475
+ ],
1476
+ }));
1477
+ }
1478
+ const found = agentTranscriptsAnswer !== "include"
1479
+ ? topLevel.filter((s) => !agentPaths.has(s.path))
1480
+ : topLevel;
1481
+ const groups = [...groupByProject(found, projectsDirX).values()];
1482
+ if (groups.length > 0) {
1483
+ const classifyId = normalizeModelName(classifyModel, provider);
1484
+ const distillId = normalizeModelName(distillModel, provider);
1485
+ const infos = groups
1486
+ .map((g) => ({
1487
+ name: g.name,
1488
+ sessionCount: g.sessions.length,
1489
+ totalBytes: g.totalBytes,
1490
+ estCost: g.sessions.reduce((sum, s) => sum +
1491
+ estimateSessionCost(provider, classifyId, distillId, s.size), 0),
1492
+ }))
1493
+ .sort((a, b) => b.estCost - a.estCost);
1494
+ ui.blank();
1495
+ ui.line(ui.dim(" Which projects should vir distill? Costs are rough upper bounds."));
1496
+ projectDecisions = await promptProjectDecisions(infos, existing?.projects ?? {}, "Projects to track");
1497
+ }
1498
+ }
1499
+ catch (err) {
1500
+ console.warn(chalk.yellow(`project scan failed (${err.message}) — you can decide later with vir projects`));
1501
+ }
1348
1502
  const parsed = ConfigSchema.safeParse(buildInitConfig(existing, {
1349
1503
  vaultPath,
1350
1504
  outputDir,
@@ -1358,6 +1512,8 @@ async function cmdInit() {
1358
1512
  pdfsDir,
1359
1513
  classifyModel,
1360
1514
  distillModel,
1515
+ projects: projectDecisions,
1516
+ agentTranscripts: agentTranscriptsAnswer,
1361
1517
  }));
1362
1518
  if (!parsed.success) {
1363
1519
  console.error(chalk.red("invalid config:"));