vexp-cli 3.3.9 → 3.4.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.
@@ -3,6 +3,7 @@ import * as path from "path";
3
3
  import { TextDecoder } from "util";
4
4
  import { vexpHome } from "./state-home.js";
5
5
  import { canonicalWorkspaceRoot } from "./socket-path.js";
6
+ import { linkedWorktreeMain, worktreeConsentSource } from "./git-worktree.js";
6
7
  export function agentConsentPath() {
7
8
  return path.join(vexpHome(), ".vexp", "agent-consent.json");
8
9
  }
@@ -153,8 +154,52 @@ export function decisionForKey(consent, key, agent) {
153
154
  return "no";
154
155
  return undefined;
155
156
  }
157
+ /**
158
+ * The answer for `agent` in the project at `root`. A linked git worktree
159
+ * with no answer of its own for the agent takes its main checkout's (the
160
+ * rule vexp-core's worktree_consent_source and both MCP servers apply to a
161
+ * worktree's index: a main checkout set up for vexp). Each `git worktree
162
+ * add` gave a fresh key with no answers, so a worktree asked again about
163
+ * every tool the main checkout had answered for (field report,
164
+ * 2026-11-07). An answer given IN the worktree is keyed by the worktree, as
165
+ * before, and wins. "Never" is machine-wide and needs no inheriting.
166
+ * Lockstep with the extension's decisionFor.
167
+ */
156
168
  export function decisionFor(consent, root, agent, opts) {
157
- return decisionForKey(consent, consentKey(root, opts), agent);
169
+ return decisionsFor(consent, root, opts)(agent);
170
+ }
171
+ /** An answer recorded for a project itself ("yes" or "no"), not through "never". */
172
+ function ownAnswer(consent, key, agent) {
173
+ const here = consent.workspaces[key]?.[agent];
174
+ return here === "yes" || here === "no";
175
+ }
176
+ /**
177
+ * decisionFor for every agent of one project, with the worktree's main
178
+ * checkout looked up once: planAgentSetup asks about every agent.
179
+ */
180
+ export function decisionsFor(consent, root, opts) {
181
+ const key = consentKey(root, opts);
182
+ const main = worktreeConsentSource(root);
183
+ const mainKey = main ? consentKey(main, opts) : undefined;
184
+ return (agent) => mainKey !== undefined && !ownAnswer(consent, key, agent)
185
+ ? decisionForKey(consent, mainKey, agent)
186
+ : decisionForKey(consent, key, agent);
187
+ }
188
+ /**
189
+ * The agents whose answer in the linked worktree at `root` comes from its
190
+ * main checkout (decisionFor): the worktree has none of its own for them
191
+ * and the main checkout has a "yes" or a "no". undefined when `root` takes
192
+ * nothing from another checkout. What `vexp doctor` says the answers come
193
+ * from.
194
+ */
195
+ export function inheritedAnswers(consent, root, opts) {
196
+ const main = worktreeConsentSource(root);
197
+ if (!main)
198
+ return undefined;
199
+ const key = consentKey(root, opts);
200
+ const mainKey = consentKey(main, opts);
201
+ const agents = Object.keys(consent.workspaces[mainKey] ?? {}).filter((agent) => ownAnswer(consent, mainKey, agent) && !ownAnswer(consent, key, agent));
202
+ return agents.length > 0 ? { main, agents } : undefined;
158
203
  }
159
204
  /** Temp file + rename, so a reader never sees half a file. */
160
205
  function writeAtomic(file, content) {
@@ -236,9 +281,74 @@ export function recordAgentConsent(root, agent, decision, opts) {
236
281
  else
237
282
  delete workspaces[key];
238
283
  const out = { ...raw, version: typeof raw.version === "number" ? raw.version : 1, workspaces, never };
284
+ pruneGoneWorktrees(out, workspaces, key, linkedWorktreeMain(root));
239
285
  writeAtomic(file, JSON.stringify(out, null, 2) + "\n");
240
286
  }
241
287
  catch {
242
288
  /* see above */
243
289
  }
244
290
  }
291
+ /**
292
+ * The keys of linked worktrees that answers were saved for, under
293
+ * `worktrees`, each with its main checkout: a worktree is made and removed
294
+ * by the dozen (an agent harness makes one per task), and the key of each
295
+ * removed one stayed in this file for good. At each save, a worktree that
296
+ * is gone for good loses its answers (worktreeGone). One that is only out
297
+ * of reach keeps them: to fs.existsSync, an unplugged disk, an offline
298
+ * share, a dropped mount or a folder this user may not enter all looked
299
+ * like a removed worktree, and once the volume was back a "Not here" given
300
+ * in the worktree became its main checkout's "yes" (review, 2026-11-07).
301
+ * Only keys saved as a worktree's go. The list is written only while it
302
+ * names one, so a file with no worktree in it keeps its bytes. Lockstep
303
+ * with the extension.
304
+ */
305
+ function pruneGoneWorktrees(out, workspaces, key, main) {
306
+ const listed = {};
307
+ if (isPlainObject(out.worktrees)) {
308
+ for (const [k, m] of Object.entries(out.worktrees))
309
+ if (typeof m === "string")
310
+ listed[k] = m;
311
+ }
312
+ if (main !== null && workspaces[key] !== undefined)
313
+ listed[key] = main;
314
+ for (const [k, m] of Object.entries(listed)) {
315
+ if (workspaces[k] === undefined)
316
+ delete listed[k];
317
+ else if (worktreeGone(k, m)) {
318
+ delete workspaces[k];
319
+ delete listed[k];
320
+ }
321
+ }
322
+ if (Object.keys(listed).length > 0)
323
+ out.worktrees = listed;
324
+ else
325
+ delete out.worktrees;
326
+ }
327
+ /**
328
+ * Removed, not just out of reach: the worktree folder does not exist
329
+ * (ENOENT or ENOTDIR, not EACCES, EIO or ENOTCONN), while the folder that
330
+ * held it and its main checkout's .git do. An unplugged disk, an offline
331
+ * share or an unmounted volume takes the parent folder or the main checkout
332
+ * with it; a worktree beside its main checkout at the root of a Linux mount
333
+ * leaves only the empty mount point as its parent, and the main checkout's
334
+ * .git is gone with the mount. Lockstep with the extension.
335
+ */
336
+ function worktreeGone(k, main) {
337
+ try {
338
+ fs.statSync(k);
339
+ return false;
340
+ }
341
+ catch (e) {
342
+ const code = e.code;
343
+ if (code !== "ENOENT" && code !== "ENOTDIR")
344
+ return false;
345
+ }
346
+ try {
347
+ fs.statSync(path.dirname(k));
348
+ fs.statSync(path.join(main, ".git"));
349
+ return true;
350
+ }
351
+ catch {
352
+ return false;
353
+ }
354
+ }
package/dist/cli.js CHANGED
@@ -8,11 +8,11 @@ import * as fs from "fs";
8
8
  import * as net from "net";
9
9
  import { checkbox, confirm } from "@inquirer/prompts";
10
10
  import { getBinaryPath, getInstalledVersion, getMcpServerPath, binaryEnv } from "./binary.js";
11
- import { getAgentList, configureSelectedAgents, resolveAgentName, suggestAgentName, setGuardMode, setInterventionMode, plannedWrites, takeSkippedConfigs, takeUnreachableTargets, takeCodexHookApprovals, codexHookApprovalStep, planAgentSetup, nonInteractiveAgentChoice, setupAgentsCommand } from "./agent-config.js";
11
+ import { getAgentList, configureSelectedAgents, resolveAgentName, suggestAgentName, setGuardMode, setInterventionMode, plannedWrites, takeSkippedConfigs, takeUnreachableTargets, takeCodexHookApprovals, takeSetupNotes, codexHookApprovalStep, planAgentSetup, nonInteractiveAgentChoice, setupAgentsCommand, deadAntigravityEntryFiles } from "./agent-config.js";
12
12
  import { recordAgentConsent } from "./agent-consent.js";
13
13
  import { CLI_VERSION } from "./version.js";
14
14
  import { deactivateLicense, readLicenseLimits, readDeviceBlocked, refreshFromDisk, } from "./license.js";
15
- import { runActivation, planLabel as licensePlanLabel, isLifetimePlan, printCheckInStatus, } from "./license-activate.js";
15
+ import { runActivation, planLabel as licensePlanLabel, isLifetimePlan, printCheckInStatus, allToolsLines, } from "./license-activate.js";
16
16
  import { checkForUpdate } from "./update-check.js";
17
17
  import { ensureMcpHttpServer, mcpHttpStatus } from "./mcp-supervisor.js";
18
18
  import { installAutostart, uninstallAutostart, autostartStatus, migrateClaudeUnpinIfNeeded } from "./autostart.js";
@@ -26,6 +26,7 @@ import { parseDaemonRows, daemonsToStop, engineSaid, daemonWasStopped, stopNotes
26
26
  import { replaceOlderDaemon, parseEngineVersion, commandMayReplaceDaemon } from "./daemon-upgrade.js";
27
27
  import { mutableOutput, askSecretOn } from "./secret-prompt.js";
28
28
  import { isTraceEnabled } from "./trace.js";
29
+ import { DEVMIND_REQUIREMENT, LLM_MENU, bannerText, llmBannerLines, llmMenuChoice, parseSetupLlmStatus } from "./devmind.js";
29
30
  import { resolveParentWorkspace, listWorkspaceRepos, addRepoToWorkspace, } from "./workspace-repos.js";
30
31
  const program = new Command();
31
32
  program
@@ -783,18 +784,23 @@ program
783
784
  .command("compress <file>")
784
785
  .description("Compress a markdown/text file using prose compression (backup to <name>.original.md)")
785
786
  .option("--level <lvl>", "Compression level: off | lite | full | ultra", "full")
787
+ .option("--llm", "Use the local LLM (DevMind) for this file: needs the model installed (or an [llm] endpoint), and runs also with LLM compression switched off, for this file only. Without a usable GPU it runs on the CPU as an explicit one-off, slower; falls back to rule-based compression when no model can be used")
786
788
  .action(async (file, opts) => {
787
789
  const binaryPath = ensureBinary();
788
- runBinary(binaryPath, ["compress", file, "--level", opts.level]);
790
+ runBinary(binaryPath, ["compress", file, "--level", opts.level, ...(opts.llm ? ["--llm"] : [])]);
789
791
  });
790
792
  program
791
793
  .command("setup-llm")
792
- .description("Configure optional local LLM compression (vexp-devmind). Runs interactive wizard by default.")
793
- .option("--status", "Print current LLM config and readiness")
794
- .option("--disable", "Disable LLM compression without removing model files")
795
- .option("--install", "Non-interactive install (for CI / first-run)")
794
+ // The requirement is said before anyone downloads, and every flag says
795
+ // what it does (review, 2026-11-08). The flags are vexp-core's, passed
796
+ // through. Lockstep with packages/vexp-core/src/main.rs (SetupLlm).
797
+ .description(`Configure the optional local LLM compression (DevMind, vexp-devmind). ${DEVMIND_REQUIREMENT} Runs the interactive wizard by default.`)
798
+ .option("--status", "Print the LLM config, whether DevMind runs on this machine (and why not), the hardware, and what the workspace daemon runs")
799
+ .option("--disable", "Turn LLM compression off; the model files stay on disk (see --uninstall)")
800
+ .option("--install", "Install without prompts: checks first that DevMind can run here and downloads nothing when it cannot; on NVIDIA fetches the CUDA plugin and runtime (~1 GB) before the model (~3.5 GB)")
796
801
  .option("--check-updates", "Check for model updates without downloading")
797
- .option("--enable", "Enable a previously-installed LLM without re-downloading anything")
802
+ .option("--enable", "Turn a previously installed model back on without downloading anything; refuses where DevMind cannot run")
803
+ .option("--uninstall", "Remove what vexp downloaded for DevMind (the model, the CUDA plugin and runtime, the GPU markers) and turn it off")
798
804
  .action(async (opts) => {
799
805
  const binaryPath = ensureBinary();
800
806
  const args = ["setup-llm"];
@@ -808,6 +814,8 @@ program
808
814
  args.push("--check-updates");
809
815
  if (opts.enable)
810
816
  args.push("--enable");
817
+ if (opts.uninstall)
818
+ args.push("--uninstall");
811
819
  runBinary(binaryPath, args);
812
820
  });
813
821
  program
@@ -892,6 +900,20 @@ function printCodexHookApprovals() {
892
900
  console.log(chalk.yellow(` ! ${codexHookApprovalStep(root)}`));
893
901
  }
894
902
  }
903
+ /**
904
+ * What this run left as it is on purpose (takeSetupNotes): a file committed
905
+ * to the repository that only this checkout's path would have changed, or
906
+ * that a cleanup would have deleted; .vscode/mcp.json left without vexp
907
+ * because .mcp.json defines it. Said in the words the extension logs.
908
+ */
909
+ function printSetupNotes() {
910
+ const notes = takeSetupNotes();
911
+ if (notes.length === 0)
912
+ return;
913
+ console.log("");
914
+ for (const note of notes)
915
+ console.log(chalk.dim(` ${note}`));
916
+ }
895
917
  function agentMark(plan, agent) {
896
918
  const pre = plan.configure.includes(agent);
897
919
  const kind = plan.kinds[agent];
@@ -1068,6 +1090,14 @@ program
1068
1090
  console.log(chalk.dim(` - ${line}`));
1069
1091
  }
1070
1092
  }
1093
+ // Every setup takes vexp's dead Antigravity entries out, whichever
1094
+ // agents it sets up; with Antigravity chosen its lines name them.
1095
+ const dead = selectedNames.includes("Antigravity") ? [] : deadAntigravityEntryFiles();
1096
+ if (dead.length > 0) {
1097
+ console.log(chalk.dim(" machine-wide:"));
1098
+ for (const file of dead)
1099
+ console.log(chalk.dim(` - ${file} (vexp's old unpinned entry removed)`));
1100
+ }
1071
1101
  console.log(chalk.dim("\n Nothing was written (dry run)."));
1072
1102
  }
1073
1103
  else {
@@ -1133,14 +1163,9 @@ program
1133
1163
  }
1134
1164
  // Agents selected whose config target does not exist on this
1135
1165
  // machine yet: rules land, tools do not, and nothing said so.
1136
- const unreachable = takeUnreachableTargets();
1137
- if (unreachable.length > 0) {
1138
- console.log("");
1139
- console.log(chalk.yellow(` ⚠ ${unreachable.length} agent config(s) could not be written yet:`));
1140
- for (const f of unreachable)
1141
- console.log(chalk.yellow(` ${f}`));
1142
- }
1166
+ printUnreachableTargets();
1143
1167
  printCodexHookApprovals();
1168
+ printSetupNotes();
1144
1169
  }
1145
1170
  }
1146
1171
  }
@@ -1276,9 +1301,32 @@ async function runSetupAgents(dir) {
1276
1301
  console.log(chalk.dim(` → ${mcpFile}`));
1277
1302
  }
1278
1303
  }
1304
+ printUnreachableTargets();
1279
1305
  printCodexHookApprovals();
1306
+ printSetupNotes();
1280
1307
  console.log(chalk.green(`\n ✓ ${result.agents.length} agent(s) configured\n`));
1281
1308
  }
1309
+ /**
1310
+ * Agents selected whose config target cannot be written (none on this
1311
+ * machine yet, one setup refuses to write through, such as Antigravity's
1312
+ * .agents/mcp_config.json when it is a link to the machine-wide file, a
1313
+ * pinned project file that leads out of the project through a link or is
1314
+ * a link to nothing (projectWriteLanding), or a file whose mcpServers /
1315
+ * servers / mcp is not an object, left untouched):
1316
+ * rules land, tools do not, and nothing said so. Every setup path prints
1317
+ * it: `vexp setup-agents` and the REPL's setup ended with a check mark
1318
+ * over a refusal that only `vexp setup` showed. Not "yet": a file left
1319
+ * untouched stays so until the user fixes it.
1320
+ */
1321
+ function printUnreachableTargets() {
1322
+ const unreachable = takeUnreachableTargets();
1323
+ if (unreachable.length === 0)
1324
+ return;
1325
+ console.log("");
1326
+ console.log(chalk.yellow(` ⚠ ${unreachable.length} agent config(s) could not be written:`));
1327
+ for (const f of unreachable)
1328
+ console.log(chalk.yellow(` ${f}`));
1329
+ }
1282
1330
  /** Full setup from REPL — reuses the existing readline to avoid stdin conflicts. */
1283
1331
  async function runSetupInteractive(rl) {
1284
1332
  const workspaceRoot = process.cwd();
@@ -1341,7 +1389,9 @@ async function runSetupInteractive(rl) {
1341
1389
  if (result.mcpConfigs.length > 0) {
1342
1390
  console.log(chalk.dim(`\n MCP configs: ${result.mcpConfigs.join(", ")}`));
1343
1391
  }
1392
+ printUnreachableTargets();
1344
1393
  printCodexHookApprovals();
1394
+ printSetupNotes();
1345
1395
  console.log(chalk.green(`\n ✓ Setup complete — ${result.agents.length} agent(s) configured\n`));
1346
1396
  }
1347
1397
  // Login autostart — opt-in only. The daemon already cold-starts on demand,
@@ -1426,10 +1476,7 @@ program
1426
1476
  console.log(` Email: ${limits.email}`);
1427
1477
  console.log(` Max nodes: ${limits.maxNodes === 0 ? "unlimited" : limits.maxNodes.toLocaleString()}`);
1428
1478
  console.log(` Max repos: ${limits.maxRepos === 0 ? "unlimited" : limits.maxRepos}`);
1429
- console.log(` All tools: ${limits.allTools ? "yes" : "no (7/10)"}`);
1430
- if (limits.allTools) {
1431
- console.log(` (the MCP tool list shows 4 by default to keep the catalog small; every tool stays callable — VEXP_ALL_TOOLS=1 lists them all)`);
1432
- }
1479
+ allToolsLines(limits.allTools).forEach((l, n) => console.log(` ${n === 0 ? "" : " ".repeat(11)}${l}`));
1433
1480
  if (limits.renewsAt) {
1434
1481
  console.log(ltd
1435
1482
  ? ` Renewal: none — lifetime licence (local token auto-refreshes, ` +
@@ -1574,6 +1621,7 @@ program
1574
1621
  console.log(chalk.dim(` Codex starts vexp in each session's own folder; 'vexp use ${JSON.stringify(resolved)} --pin' would make every session use this project instead`));
1575
1622
  }
1576
1623
  printCodexHookApprovals();
1624
+ printSetupNotes();
1577
1625
  });
1578
1626
  // ────────────────────────────────────────────────────
1579
1627
  // Version
@@ -1594,7 +1642,7 @@ const MENU_CONFIG = [
1594
1642
  { key: "3", label: "setup", hint: "Full setup (index + agents + hooks)", description: "One-command bootstrap: indexes the codebase, detects AI agents,\n writes MCP configs, installs git hooks." },
1595
1643
  { key: "4", label: "setup-agents", hint: "Choose which AI agents to configure", description: "Select agents (Claude, Cursor, Windsurf, Copilot, …) to configure.\n Creates config directories and writes MCP settings for each." },
1596
1644
  { key: "5", label: "daemon-cmd", hint: "Manage daemon (start/stop/restart/status/logs)", description: "Controls the background daemon:\n start → launches the daemon (indexes + watches for changes)\n stop → stops the running daemon\n restart → stop + start (reloads workspace.json, indexes new repos)\n status → shows node/edge/file counts\n logs → prints .vexp/vexp.log" },
1597
- { key: "6", label: "setup-llm", hint: "Configure local LLM compression (vexp-devmind)", description: "Install, update, or disable the optional local LLM (vexp-devmind).\n Runs entirely on your machine (~3.5 GB download).\n Enables smart prompt preprocessing and higher-quality memory compression." },
1645
+ { key: "6", label: "setup-llm", hint: "Configure local LLM compression (vexp-devmind)", description: "Install, update, disable or remove the optional local LLM (vexp-devmind).\n Runs entirely on your machine (~3.5 GB download).\n Enables smart prompt preprocessing and higher-quality memory compression.\n DevMind needs an NVIDIA GPU (Linux, Windows) or an Apple Silicon Mac;\n without one it stays off and vexp works the same." },
1598
1646
  ];
1599
1647
  // ── Sub-menu: Explore & Analyze ──
1600
1648
  const MENU_EXPLORE = [
@@ -1684,7 +1732,10 @@ async function printBanner() {
1684
1732
  console.log(chalk.dim(" → type '1 > 5 > start' to launch it, or `vexp daemon-cmd start`"));
1685
1733
  }
1686
1734
  }
1687
- // ── LLM status line (3 states: active / installed-not-active / not installed) ──
1735
+ // ── LLM status line ──
1736
+ // From the engine's --status: "(active)" only when the workspace daemon
1737
+ // runs the model, and DevMind off on this machine said calmly, with no
1738
+ // download offered (review, 2026-11-08; see llmBannerLines).
1688
1739
  try {
1689
1740
  const llmBin = ensureBinary();
1690
1741
  const stdout = execFileSync(llmBin, ["setup-llm", "--status"], {
@@ -1692,20 +1743,13 @@ async function printBanner() {
1692
1743
  encoding: "utf-8",
1693
1744
  stdio: ["pipe", "pipe", "pipe"],
1694
1745
  env: binaryEnv(llmBin),
1746
+ // Its "Daemon now" line names the daemon of the folder it runs in:
1747
+ // the one the daemon line above reports.
1748
+ cwd: eff?.root,
1695
1749
  });
1696
- const operational = /Operational:\s*true/i.test(stdout);
1697
- const installed = /Installed:\s*true/i.test(stdout);
1698
- const model = stdout.match(/Model:\s*(.+)/)?.[1]?.trim() ?? "";
1699
- if (operational) {
1700
- console.log(chalk.green(` ● LLM: ${model || "local"} (active)`));
1701
- }
1702
- else if (installed) {
1703
- console.log(chalk.yellow(" ○ LLM: installed but not active"));
1704
- console.log(chalk.dim(" → type '1 > 6 > start' to start it, or `vexp setup-llm --enable`"));
1705
- }
1706
- else {
1707
- console.log(chalk.dim(" ○ LLM: not installed"));
1708
- console.log(chalk.dim(" → type '1 > 6 > install' to download (~3.5 GB), or `vexp setup-llm --install`"));
1750
+ for (const l of llmBannerLines(parseSetupLlmStatus(stdout))) {
1751
+ const text = bannerText(l);
1752
+ console.log(l.tone === "green" ? chalk.green(text) : l.tone === "yellow" ? chalk.yellow(text) : chalk.dim(text));
1709
1753
  }
1710
1754
  }
1711
1755
  catch {
@@ -1930,16 +1974,17 @@ async function executeCommand(label, rl, outCtl) {
1930
1974
  case "setup-llm": {
1931
1975
  // Sticky sub-sub-menu.
1932
1976
  const llmBinary = ensureBinary();
1977
+ // One table prints the menu, reads the answer and names the keys
1978
+ // in the banner's hints: those said '1 > 6 > start', which this
1979
+ // prompt refused (review, 2026-11-08).
1933
1980
  const printLlmMenu = () => {
1934
1981
  console.log("");
1935
1982
  console.log(chalk.cyan(" ── Local LLM (vexp-devmind) ──"));
1936
1983
  console.log(chalk.dim(" All processing runs locally — no data leaves your machine."));
1984
+ console.log(chalk.dim(` ${DEVMIND_REQUIREMENT}`));
1937
1985
  console.log("");
1938
- console.log(` ${chalk.bold("1")} Install / Reinstall ${chalk.dim("(~3.5 GB download)")}`);
1939
- console.log(` ${chalk.bold("2")} Start ${chalk.dim("(enable without re-downloading)")}`);
1940
- console.log(` ${chalk.bold("3")} Check for updates ${chalk.dim("(no download if up to date)")}`);
1941
- console.log(` ${chalk.bold("4")} Status ${chalk.dim("(current config + hardware)")}`);
1942
- console.log(` ${chalk.bold("5")} Disable ${chalk.dim("(keep files, stop using LLM)")}`);
1986
+ for (const e of LLM_MENU)
1987
+ console.log(` ${chalk.bold(e.key)} ${e.label.padEnd(20)} ${chalk.dim(e.note)}`);
1943
1988
  console.log(` ${chalk.dim("b")} ${chalk.dim("Back to Setup & Config")}`);
1944
1989
  console.log("");
1945
1990
  };
@@ -1950,27 +1995,22 @@ async function executeCommand(label, rl, outCtl) {
1950
1995
  continue;
1951
1996
  if (c === "b" || c === "back")
1952
1997
  break;
1953
- switch (c) {
1954
- case "1":
1955
- console.log(chalk.dim(" Starting LLM setup (this may take several minutes)…\n"));
1956
- runBinary(llmBinary, ["setup-llm", "--install"]);
1957
- break;
1958
- case "2":
1959
- runBinary(llmBinary, ["setup-llm", "--enable"]);
1960
- break;
1961
- case "3":
1962
- runBinary(llmBinary, ["setup-llm", "--check-updates"]);
1963
- break;
1964
- case "4":
1965
- runBinary(llmBinary, ["setup-llm", "--status"]);
1966
- break;
1967
- case "5":
1968
- runBinary(llmBinary, ["setup-llm", "--disable"]);
1969
- break;
1970
- default:
1971
- console.log(chalk.dim(" Unknown option. Use 1-5 or 'b' to go back."));
1998
+ const choice = llmMenuChoice(c);
1999
+ if (!choice) {
2000
+ console.log(chalk.dim(` Unknown option. Use 1-${LLM_MENU.length} or 'b' to go back.`));
2001
+ continue;
2002
+ }
2003
+ if (choice.flag === "--install") {
2004
+ console.log(chalk.dim(" Starting LLM setup (this may take several minutes)…\n"));
2005
+ }
2006
+ if (choice.flag === "--uninstall") {
2007
+ const yes = (await ask(rl, chalk.cyan(" Remove the DevMind model and the CUDA files vexp downloaded? (y/N) "))).trim().toLowerCase();
2008
+ if (yes !== "y" && yes !== "yes") {
2009
+ printLlmMenu();
1972
2010
  continue;
2011
+ }
1973
2012
  }
2013
+ runBinary(llmBinary, ["setup-llm", choice.flag]);
1974
2014
  printLlmMenu();
1975
2015
  }
1976
2016
  break;
@@ -2129,10 +2169,7 @@ async function executeCommand(label, rl, outCtl) {
2129
2169
  console.log(` Email: ${limits.email}`);
2130
2170
  console.log(` Max nodes: ${limits.maxNodes === 0 ? "unlimited" : limits.maxNodes.toLocaleString()}`);
2131
2171
  console.log(` Max repos: ${limits.maxRepos === 0 ? "unlimited" : limits.maxRepos}`);
2132
- console.log(` All tools: ${limits.allTools ? "yes" : "no (7/10)"}`);
2133
- if (limits.allTools) {
2134
- console.log(` (the MCP tool list shows 4 by default to keep the catalog small; every tool stays callable — VEXP_ALL_TOOLS=1 lists them all)`);
2135
- }
2172
+ allToolsLines(limits.allTools).forEach((l, n) => console.log(` ${n === 0 ? "" : " ".repeat(11)}${l}`));
2136
2173
  if (limits.renewsAt) {
2137
2174
  console.log(ltd
2138
2175
  ? ` Renewal: none — lifetime licence (token auto-refreshes, valid to ${limits.renewsAt.toLocaleDateString()})`