@djolex999/vir-cli 0.12.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.
- package/dist/cli/initConfig.js +4 -0
- package/dist/cli/initConfig.js.map +1 -1
- package/dist/cli/projectSelect.js +51 -0
- package/dist/cli/projectSelect.js.map +1 -0
- package/dist/cli/projects.js +119 -0
- package/dist/cli/projects.js.map +1 -0
- package/dist/cli/reconcile.js +44 -4
- package/dist/cli/reconcile.js.map +1 -1
- package/dist/cli.js +235 -41
- package/dist/cli.js.map +1 -1
- package/dist/config.js +40 -2
- package/dist/config.js.map +1 -1
- package/dist/cost/pricing.js +5 -0
- package/dist/cost/pricing.js.map +1 -1
- package/dist/daemon/index.js +10 -3
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/launchd.js +65 -6
- package/dist/daemon/launchd.js.map +1 -1
- package/dist/diagnostics/doctor.js +151 -14
- package/dist/diagnostics/doctor.js.map +1 -1
- package/dist/pipeline/distiller.js +29 -0
- package/dist/pipeline/distiller.js.map +1 -1
- package/dist/pipeline/lock.js +57 -0
- package/dist/pipeline/lock.js.map +1 -0
- package/dist/pipeline/parser.js +11 -0
- package/dist/pipeline/parser.js.map +1 -1
- package/dist/pipeline/projects.js +189 -0
- package/dist/pipeline/projects.js.map +1 -0
- package/dist/pipeline/run.js +354 -23
- package/dist/pipeline/run.js.map +1 -1
- package/dist/search/embedder.js +13 -0
- package/dist/search/embedder.js.map +1 -1
- package/dist/search/retriever.js +35 -12
- package/dist/search/retriever.js.map +1 -1
- package/dist/state/db.js +128 -13
- package/dist/state/db.js.map +1 -1
- package/package.json +2 -1
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";
|
|
@@ -27,7 +30,8 @@ import { computeCost } from "./cost/pricing.js";
|
|
|
27
30
|
import { countByCategory, summarizeAll, summarizeProject, } from "./pipeline/summarizer.js";
|
|
28
31
|
import { buildPeriodPrompt, estimatePeriodCostTokens, periodLabel, periodRange, selectNotesInPeriod, summarizePeriod, } from "./pipeline/periodSummary.js";
|
|
29
32
|
import { embeddingForNote, isOllamaAvailable, } from "./search/embedder.js";
|
|
30
|
-
import {
|
|
33
|
+
import { acquireLock, LockHeldError, releaseLock } from "./pipeline/lock.js";
|
|
34
|
+
import { search, searchWithOutcome, vaultRoot } from "./search/retriever.js";
|
|
31
35
|
import { buildQueryResults, errorPayload } from "./output/json.js";
|
|
32
36
|
import { synthesize } from "./search/synthesizer.js";
|
|
33
37
|
import { runMcpServer } from "./mcp/server.js";
|
|
@@ -69,6 +73,8 @@ program
|
|
|
69
73
|
.option("--yes", "Skip the cost confirmation prompt")
|
|
70
74
|
.option("--force-model <model>", "Override the distill model for this run only: haiku | sonnet")
|
|
71
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), [])
|
|
72
78
|
.action(runAction(async (opts) => {
|
|
73
79
|
const cfg = loadConfig();
|
|
74
80
|
const daemon = opts.daemon === true;
|
|
@@ -87,19 +93,59 @@ program
|
|
|
87
93
|
articlesOnly ||
|
|
88
94
|
pdfsOnly ||
|
|
89
95
|
dryRun;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
96
|
+
// Dry-run and rewrite-only never call the distiller — no lock needed,
|
|
97
|
+
// and they must not be blocked by (or block) a running pipeline.
|
|
98
|
+
const needsLock = !dryRun && !rewriteOnly;
|
|
99
|
+
if (needsLock) {
|
|
100
|
+
try {
|
|
101
|
+
acquireLock();
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
if (err instanceof LockHeldError) {
|
|
105
|
+
console.error(chalk.yellow(err.message));
|
|
106
|
+
process.exitCode = 1;
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
throw err;
|
|
110
|
+
}
|
|
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;
|
|
117
|
+
let summary;
|
|
118
|
+
try {
|
|
119
|
+
summary = await runPipeline(cfg, {
|
|
120
|
+
full: opts.full,
|
|
121
|
+
quiet: daemon,
|
|
122
|
+
logToFile: daemon,
|
|
123
|
+
rewriteOnly,
|
|
124
|
+
articlesOnly,
|
|
125
|
+
pdfsOnly,
|
|
126
|
+
forceDistillModel: opts.forceModel,
|
|
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,
|
|
140
|
+
onConfirm: skipPrompt
|
|
141
|
+
? undefined
|
|
142
|
+
: async (newCount) => confirmCostIfNeeded(cfg, newCount),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
finally {
|
|
146
|
+
if (needsLock)
|
|
147
|
+
releaseLock();
|
|
148
|
+
}
|
|
103
149
|
// Surface per-item distill failures via a non-zero exit so external
|
|
104
150
|
// callers (and the user) don't get false "success" — the silent-success
|
|
105
151
|
// bug that hid Kie's 200-with-error responses pre-0.7.2.
|
|
@@ -222,10 +268,11 @@ const schedule = program
|
|
|
222
268
|
.description("Manage the background daemon (launchd / systemd / cron)");
|
|
223
269
|
schedule
|
|
224
270
|
.command("install")
|
|
225
|
-
.description("Install
|
|
226
|
-
.
|
|
271
|
+
.description("Install the scheduled daemon (first run at the next interval)")
|
|
272
|
+
.option("--run-now", "Also kick off one run immediately after installing")
|
|
273
|
+
.action(runAction(async (opts) => {
|
|
227
274
|
const cfg = loadConfig();
|
|
228
|
-
await installDaemon(cfg);
|
|
275
|
+
await installDaemon(cfg, { runNow: opts.runNow });
|
|
229
276
|
const ds = await daemonStatus();
|
|
230
277
|
console.log(chalk.green(`installed via ${ds.method}: ${ds.configPath ?? "(no path)"} (active=${ds.active})`));
|
|
231
278
|
}));
|
|
@@ -777,19 +824,24 @@ program
|
|
|
777
824
|
console.log(ui.text(question));
|
|
778
825
|
ui.divider();
|
|
779
826
|
ui.blank();
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
827
|
+
// The label must reflect what actually served the results, so it can
|
|
828
|
+
// only be printed AFTER the search — the old up-front isOllamaAvailable
|
|
829
|
+
// label claimed "embeddings" even when the embed call failed and TF-IDF
|
|
830
|
+
// served the hits.
|
|
831
|
+
const sp = ui.spinner("searching vault").start();
|
|
832
|
+
let outcome;
|
|
785
833
|
try {
|
|
786
|
-
|
|
834
|
+
outcome = await searchWithOutcome(cfg, db, question, 8);
|
|
787
835
|
sp.stop();
|
|
788
836
|
}
|
|
789
837
|
catch (err) {
|
|
790
838
|
sp.fail(ui.errorColor(err.message));
|
|
791
839
|
return;
|
|
792
840
|
}
|
|
841
|
+
const hits = outcome.hits;
|
|
842
|
+
if (outcome.degraded) {
|
|
843
|
+
ui.row(ui.warn(ui.WARN_GLYPH), ui.text(`embedding search failed — results are TF-IDF fallback (${outcome.embedError})`));
|
|
844
|
+
}
|
|
793
845
|
if (hits.length === 0) {
|
|
794
846
|
ui.row(ui.warn(ui.WARN_GLYPH), ui.text("no documents matched"));
|
|
795
847
|
return;
|
|
@@ -798,7 +850,7 @@ program
|
|
|
798
850
|
ui.blank();
|
|
799
851
|
console.log(ui.text(ui.wrap(answer.trim(), 60)));
|
|
800
852
|
ui.blank();
|
|
801
|
-
const method =
|
|
853
|
+
const method = outcome.method;
|
|
802
854
|
const relevant = hits.filter((h) => h.score > 0).slice(0, 3);
|
|
803
855
|
ui.divider();
|
|
804
856
|
for (const h of relevant)
|
|
@@ -809,7 +861,10 @@ program
|
|
|
809
861
|
: new VaultWriter(cfg).noteCount();
|
|
810
862
|
ui.summary({
|
|
811
863
|
sources: { value: relevant.length, color: ui.info },
|
|
812
|
-
via: {
|
|
864
|
+
via: {
|
|
865
|
+
value: outcome.degraded ? "tfidf (embeddings failed)" : method,
|
|
866
|
+
color: outcome.degraded ? ui.warn : ui.accent,
|
|
867
|
+
},
|
|
813
868
|
searched: { value: totalNotes, color: ui.muted },
|
|
814
869
|
});
|
|
815
870
|
}
|
|
@@ -906,6 +961,70 @@ program
|
|
|
906
961
|
db.close();
|
|
907
962
|
}
|
|
908
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")));
|
|
909
1028
|
program
|
|
910
1029
|
.command("status")
|
|
911
1030
|
.description("Show processing status + knowledge base breakdown")
|
|
@@ -937,9 +1056,14 @@ program
|
|
|
937
1056
|
.description("Retry sessions that silently failed pre-0.7.2 (null/empty content despite skipped=0)")
|
|
938
1057
|
.option("--dry-run", "Report recoverable count + estimated cost + false-cost collateral; exit before any LLM call")
|
|
939
1058
|
.option("--yes", "Skip the cost confirmation prompt")
|
|
1059
|
+
.option("--force", "Also retry retry-exhausted sessions (3+ consecutive failed distills)")
|
|
940
1060
|
.action(runAction(async (opts) => {
|
|
941
1061
|
const cfg = loadConfig();
|
|
942
|
-
await runReconcile(cfg, {
|
|
1062
|
+
await runReconcile(cfg, {
|
|
1063
|
+
dryRun: opts.dryRun,
|
|
1064
|
+
yes: opts.yes,
|
|
1065
|
+
force: opts.force,
|
|
1066
|
+
});
|
|
943
1067
|
}));
|
|
944
1068
|
program
|
|
945
1069
|
.command("review")
|
|
@@ -1227,30 +1351,36 @@ async function cmdInit() {
|
|
|
1227
1351
|
default: existing?.provider ?? "anthropic",
|
|
1228
1352
|
choices: [
|
|
1229
1353
|
{
|
|
1230
|
-
name: "Anthropic (direct, official pricing)",
|
|
1354
|
+
name: "Anthropic (direct + reliable, official pricing — recommended)",
|
|
1231
1355
|
value: "anthropic",
|
|
1232
1356
|
},
|
|
1233
1357
|
{
|
|
1234
|
-
name: "Kie.ai (
|
|
1358
|
+
name: "Kie.ai (~72% cheaper, third-party proxy — you trade reliability for cost)",
|
|
1235
1359
|
value: "kie",
|
|
1236
1360
|
},
|
|
1237
1361
|
],
|
|
1238
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
|
+
};
|
|
1239
1377
|
let anthropicApiKey;
|
|
1240
1378
|
let kieApiKey;
|
|
1241
1379
|
if (provider === "anthropic") {
|
|
1242
|
-
anthropicApiKey = await
|
|
1243
|
-
message: "Anthropic API key",
|
|
1244
|
-
default: existing?.anthropicApiKey ?? process.env.ANTHROPIC_API_KEY ?? "",
|
|
1245
|
-
validate: (v) => v.startsWith("sk-ant-") ? true : "key should start with sk-ant-",
|
|
1246
|
-
});
|
|
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-"));
|
|
1247
1381
|
}
|
|
1248
1382
|
else {
|
|
1249
|
-
kieApiKey = await
|
|
1250
|
-
message: "Kie.ai API key",
|
|
1251
|
-
default: existing?.kieApiKey ?? process.env.KIE_API_KEY ?? "",
|
|
1252
|
-
validate: (v) => v.length > 10 ? true : "enter a valid Kie.ai API key",
|
|
1253
|
-
});
|
|
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"));
|
|
1254
1384
|
}
|
|
1255
1385
|
// ── model pickers (provider-aware) ──────────────────────────────────────
|
|
1256
1386
|
const classifyChoices = provider === "anthropic"
|
|
@@ -1275,9 +1405,10 @@ async function cmdInit() {
|
|
|
1275
1405
|
const distillChoices = provider === "anthropic"
|
|
1276
1406
|
? [
|
|
1277
1407
|
{
|
|
1278
|
-
name: "claude-sonnet-
|
|
1279
|
-
value: "claude-sonnet-
|
|
1408
|
+
name: "claude-sonnet-5 (recommended)",
|
|
1409
|
+
value: "claude-sonnet-5",
|
|
1280
1410
|
},
|
|
1411
|
+
{ name: "claude-sonnet-4-6", value: "claude-sonnet-4-6" },
|
|
1281
1412
|
{
|
|
1282
1413
|
name: "claude-haiku-4-5-20251001 (faster, cheaper)",
|
|
1283
1414
|
value: "claude-haiku-4-5-20251001",
|
|
@@ -1307,6 +1438,67 @@ async function cmdInit() {
|
|
|
1307
1438
|
: "must be between 0 and 1";
|
|
1308
1439
|
},
|
|
1309
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
|
+
}
|
|
1310
1502
|
const parsed = ConfigSchema.safeParse(buildInitConfig(existing, {
|
|
1311
1503
|
vaultPath,
|
|
1312
1504
|
outputDir,
|
|
@@ -1320,6 +1512,8 @@ async function cmdInit() {
|
|
|
1320
1512
|
pdfsDir,
|
|
1321
1513
|
classifyModel,
|
|
1322
1514
|
distillModel,
|
|
1515
|
+
projects: projectDecisions,
|
|
1516
|
+
agentTranscripts: agentTranscriptsAnswer,
|
|
1323
1517
|
}));
|
|
1324
1518
|
if (!parsed.success) {
|
|
1325
1519
|
console.error(chalk.red("invalid config:"));
|