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.
- package/dist/agent-config.js +3454 -602
- package/dist/agent-consent.js +111 -1
- package/dist/cli.js +99 -62
- package/dist/devmind.js +269 -0
- package/dist/doctor.js +1068 -89
- package/dist/git-worktree.js +136 -0
- package/dist/hook-scripts.js +271 -0
- package/dist/license-activate.js +33 -0
- package/dist/serve.js +7 -2
- package/mcp/mcp-server.cjs +52 -51
- package/package.json +7 -7
package/dist/agent-consent.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
793
|
-
|
|
794
|
-
.
|
|
795
|
-
.
|
|
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", "
|
|
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
|
-
|
|
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(`
|
|
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
|
|
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
|
|
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
|
|
1697
|
-
|
|
1698
|
-
|
|
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
|
-
|
|
1939
|
-
|
|
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
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
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(`
|
|
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()})`
|