vexp-cli 3.3.8 → 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/README.md +1 -1
- package/dist/agent-config.js +3471 -602
- package/dist/agent-consent.js +111 -1
- package/dist/cli.js +101 -63
- package/dist/daemon-upgrade.js +19 -6
- package/dist/devmind.js +269 -0
- package/dist/doctor.js +1068 -89
- package/dist/forget.js +1 -0
- 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 +54 -3
- 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
|
|
@@ -46,7 +47,8 @@ Daemons:
|
|
|
46
47
|
[daemon]
|
|
47
48
|
idle_timeout_minutes = 30
|
|
48
49
|
|
|
49
|
-
VEXP_IDLE_TIMEOUT_MINUTES sets it for one daemon.
|
|
50
|
+
VEXP_IDLE_TIMEOUT_MINUTES sets it for one daemon. Unset, a daemon an agent's session started
|
|
51
|
+
stops after 30 minutes without use (0 turns that off). A daemon an editor window started stops
|
|
50
52
|
with that window instead. vexp daemons lists them; vexp doctor says whether the setting is on.
|
|
51
53
|
`;
|
|
52
54
|
program.addHelpText("after", IDLE_STOP_HELP);
|
|
@@ -782,18 +784,23 @@ program
|
|
|
782
784
|
.command("compress <file>")
|
|
783
785
|
.description("Compress a markdown/text file using prose compression (backup to <name>.original.md)")
|
|
784
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")
|
|
785
788
|
.action(async (file, opts) => {
|
|
786
789
|
const binaryPath = ensureBinary();
|
|
787
|
-
runBinary(binaryPath, ["compress", file, "--level", opts.level]);
|
|
790
|
+
runBinary(binaryPath, ["compress", file, "--level", opts.level, ...(opts.llm ? ["--llm"] : [])]);
|
|
788
791
|
});
|
|
789
792
|
program
|
|
790
793
|
.command("setup-llm")
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
.
|
|
794
|
-
.
|
|
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)")
|
|
795
801
|
.option("--check-updates", "Check for model updates without downloading")
|
|
796
|
-
.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")
|
|
797
804
|
.action(async (opts) => {
|
|
798
805
|
const binaryPath = ensureBinary();
|
|
799
806
|
const args = ["setup-llm"];
|
|
@@ -807,6 +814,8 @@ program
|
|
|
807
814
|
args.push("--check-updates");
|
|
808
815
|
if (opts.enable)
|
|
809
816
|
args.push("--enable");
|
|
817
|
+
if (opts.uninstall)
|
|
818
|
+
args.push("--uninstall");
|
|
810
819
|
runBinary(binaryPath, args);
|
|
811
820
|
});
|
|
812
821
|
program
|
|
@@ -891,6 +900,20 @@ function printCodexHookApprovals() {
|
|
|
891
900
|
console.log(chalk.yellow(` ! ${codexHookApprovalStep(root)}`));
|
|
892
901
|
}
|
|
893
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
|
+
}
|
|
894
917
|
function agentMark(plan, agent) {
|
|
895
918
|
const pre = plan.configure.includes(agent);
|
|
896
919
|
const kind = plan.kinds[agent];
|
|
@@ -1067,6 +1090,14 @@ program
|
|
|
1067
1090
|
console.log(chalk.dim(` - ${line}`));
|
|
1068
1091
|
}
|
|
1069
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
|
+
}
|
|
1070
1101
|
console.log(chalk.dim("\n Nothing was written (dry run)."));
|
|
1071
1102
|
}
|
|
1072
1103
|
else {
|
|
@@ -1132,14 +1163,9 @@ program
|
|
|
1132
1163
|
}
|
|
1133
1164
|
// Agents selected whose config target does not exist on this
|
|
1134
1165
|
// machine yet: rules land, tools do not, and nothing said so.
|
|
1135
|
-
|
|
1136
|
-
if (unreachable.length > 0) {
|
|
1137
|
-
console.log("");
|
|
1138
|
-
console.log(chalk.yellow(` ⚠ ${unreachable.length} agent config(s) could not be written yet:`));
|
|
1139
|
-
for (const f of unreachable)
|
|
1140
|
-
console.log(chalk.yellow(` ${f}`));
|
|
1141
|
-
}
|
|
1166
|
+
printUnreachableTargets();
|
|
1142
1167
|
printCodexHookApprovals();
|
|
1168
|
+
printSetupNotes();
|
|
1143
1169
|
}
|
|
1144
1170
|
}
|
|
1145
1171
|
}
|
|
@@ -1275,9 +1301,32 @@ async function runSetupAgents(dir) {
|
|
|
1275
1301
|
console.log(chalk.dim(` → ${mcpFile}`));
|
|
1276
1302
|
}
|
|
1277
1303
|
}
|
|
1304
|
+
printUnreachableTargets();
|
|
1278
1305
|
printCodexHookApprovals();
|
|
1306
|
+
printSetupNotes();
|
|
1279
1307
|
console.log(chalk.green(`\n ✓ ${result.agents.length} agent(s) configured\n`));
|
|
1280
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
|
+
}
|
|
1281
1330
|
/** Full setup from REPL — reuses the existing readline to avoid stdin conflicts. */
|
|
1282
1331
|
async function runSetupInteractive(rl) {
|
|
1283
1332
|
const workspaceRoot = process.cwd();
|
|
@@ -1340,7 +1389,9 @@ async function runSetupInteractive(rl) {
|
|
|
1340
1389
|
if (result.mcpConfigs.length > 0) {
|
|
1341
1390
|
console.log(chalk.dim(`\n MCP configs: ${result.mcpConfigs.join(", ")}`));
|
|
1342
1391
|
}
|
|
1392
|
+
printUnreachableTargets();
|
|
1343
1393
|
printCodexHookApprovals();
|
|
1394
|
+
printSetupNotes();
|
|
1344
1395
|
console.log(chalk.green(`\n ✓ Setup complete — ${result.agents.length} agent(s) configured\n`));
|
|
1345
1396
|
}
|
|
1346
1397
|
// Login autostart — opt-in only. The daemon already cold-starts on demand,
|
|
@@ -1425,10 +1476,7 @@ program
|
|
|
1425
1476
|
console.log(` Email: ${limits.email}`);
|
|
1426
1477
|
console.log(` Max nodes: ${limits.maxNodes === 0 ? "unlimited" : limits.maxNodes.toLocaleString()}`);
|
|
1427
1478
|
console.log(` Max repos: ${limits.maxRepos === 0 ? "unlimited" : limits.maxRepos}`);
|
|
1428
|
-
console.log(`
|
|
1429
|
-
if (limits.allTools) {
|
|
1430
|
-
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)`);
|
|
1431
|
-
}
|
|
1479
|
+
allToolsLines(limits.allTools).forEach((l, n) => console.log(` ${n === 0 ? "" : " ".repeat(11)}${l}`));
|
|
1432
1480
|
if (limits.renewsAt) {
|
|
1433
1481
|
console.log(ltd
|
|
1434
1482
|
? ` Renewal: none — lifetime licence (local token auto-refreshes, ` +
|
|
@@ -1573,6 +1621,7 @@ program
|
|
|
1573
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`));
|
|
1574
1622
|
}
|
|
1575
1623
|
printCodexHookApprovals();
|
|
1624
|
+
printSetupNotes();
|
|
1576
1625
|
});
|
|
1577
1626
|
// ────────────────────────────────────────────────────
|
|
1578
1627
|
// Version
|
|
@@ -1593,7 +1642,7 @@ const MENU_CONFIG = [
|
|
|
1593
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." },
|
|
1594
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." },
|
|
1595
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" },
|
|
1596
|
-
{ 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." },
|
|
1597
1646
|
];
|
|
1598
1647
|
// ── Sub-menu: Explore & Analyze ──
|
|
1599
1648
|
const MENU_EXPLORE = [
|
|
@@ -1683,7 +1732,10 @@ async function printBanner() {
|
|
|
1683
1732
|
console.log(chalk.dim(" → type '1 > 5 > start' to launch it, or `vexp daemon-cmd start`"));
|
|
1684
1733
|
}
|
|
1685
1734
|
}
|
|
1686
|
-
// ── 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).
|
|
1687
1739
|
try {
|
|
1688
1740
|
const llmBin = ensureBinary();
|
|
1689
1741
|
const stdout = execFileSync(llmBin, ["setup-llm", "--status"], {
|
|
@@ -1691,20 +1743,13 @@ async function printBanner() {
|
|
|
1691
1743
|
encoding: "utf-8",
|
|
1692
1744
|
stdio: ["pipe", "pipe", "pipe"],
|
|
1693
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,
|
|
1694
1749
|
});
|
|
1695
|
-
const
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
if (operational) {
|
|
1699
|
-
console.log(chalk.green(` ● LLM: ${model || "local"} (active)`));
|
|
1700
|
-
}
|
|
1701
|
-
else if (installed) {
|
|
1702
|
-
console.log(chalk.yellow(" ○ LLM: installed but not active"));
|
|
1703
|
-
console.log(chalk.dim(" → type '1 > 6 > start' to start it, or `vexp setup-llm --enable`"));
|
|
1704
|
-
}
|
|
1705
|
-
else {
|
|
1706
|
-
console.log(chalk.dim(" ○ LLM: not installed"));
|
|
1707
|
-
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));
|
|
1708
1753
|
}
|
|
1709
1754
|
}
|
|
1710
1755
|
catch {
|
|
@@ -1929,16 +1974,17 @@ async function executeCommand(label, rl, outCtl) {
|
|
|
1929
1974
|
case "setup-llm": {
|
|
1930
1975
|
// Sticky sub-sub-menu.
|
|
1931
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).
|
|
1932
1980
|
const printLlmMenu = () => {
|
|
1933
1981
|
console.log("");
|
|
1934
1982
|
console.log(chalk.cyan(" ── Local LLM (vexp-devmind) ──"));
|
|
1935
1983
|
console.log(chalk.dim(" All processing runs locally — no data leaves your machine."));
|
|
1984
|
+
console.log(chalk.dim(` ${DEVMIND_REQUIREMENT}`));
|
|
1936
1985
|
console.log("");
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
console.log(` ${chalk.bold("3")} Check for updates ${chalk.dim("(no download if up to date)")}`);
|
|
1940
|
-
console.log(` ${chalk.bold("4")} Status ${chalk.dim("(current config + hardware)")}`);
|
|
1941
|
-
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)}`);
|
|
1942
1988
|
console.log(` ${chalk.dim("b")} ${chalk.dim("Back to Setup & Config")}`);
|
|
1943
1989
|
console.log("");
|
|
1944
1990
|
};
|
|
@@ -1949,27 +1995,22 @@ async function executeCommand(label, rl, outCtl) {
|
|
|
1949
1995
|
continue;
|
|
1950
1996
|
if (c === "b" || c === "back")
|
|
1951
1997
|
break;
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
runBinary(llmBinary, ["setup-llm", "--status"]);
|
|
1965
|
-
break;
|
|
1966
|
-
case "5":
|
|
1967
|
-
runBinary(llmBinary, ["setup-llm", "--disable"]);
|
|
1968
|
-
break;
|
|
1969
|
-
default:
|
|
1970
|
-
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();
|
|
1971
2010
|
continue;
|
|
2011
|
+
}
|
|
1972
2012
|
}
|
|
2013
|
+
runBinary(llmBinary, ["setup-llm", choice.flag]);
|
|
1973
2014
|
printLlmMenu();
|
|
1974
2015
|
}
|
|
1975
2016
|
break;
|
|
@@ -2128,10 +2169,7 @@ async function executeCommand(label, rl, outCtl) {
|
|
|
2128
2169
|
console.log(` Email: ${limits.email}`);
|
|
2129
2170
|
console.log(` Max nodes: ${limits.maxNodes === 0 ? "unlimited" : limits.maxNodes.toLocaleString()}`);
|
|
2130
2171
|
console.log(` Max repos: ${limits.maxRepos === 0 ? "unlimited" : limits.maxRepos}`);
|
|
2131
|
-
console.log(`
|
|
2132
|
-
if (limits.allTools) {
|
|
2133
|
-
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)`);
|
|
2134
|
-
}
|
|
2172
|
+
allToolsLines(limits.allTools).forEach((l, n) => console.log(` ${n === 0 ? "" : " ".repeat(11)}${l}`));
|
|
2135
2173
|
if (limits.renewsAt) {
|
|
2136
2174
|
console.log(ltd
|
|
2137
2175
|
? ` Renewal: none — lifetime licence (token auto-refreshes, valid to ${limits.renewsAt.toLocaleDateString()})`
|
package/dist/daemon-upgrade.js
CHANGED
|
@@ -187,8 +187,13 @@ export function parseEngineVersion(stdout) {
|
|
|
187
187
|
return last ? last : undefined;
|
|
188
188
|
}
|
|
189
189
|
/**
|
|
190
|
-
* The folder `daemon-cmd` works on when run in `dir
|
|
191
|
-
* `.git` at or above it,
|
|
190
|
+
* The folder `daemon-cmd` works on when run in `dir` (daemon_cmd_root in
|
|
191
|
+
* vexp-core): the nearest one with a `.git` at or above it, unless that one
|
|
192
|
+
* was never set up for vexp and a folder between it and `dir` was, which
|
|
193
|
+
* is then the nearest such folder; with no `.git` above, `dir` itself. Set
|
|
194
|
+
* up is the engine's is_workspace_marker: one of its consent files in the
|
|
195
|
+
* folder's .vexp/, and neither the home directory nor a filesystem root
|
|
196
|
+
* (forbidden_workspace_reason refuses both).
|
|
192
197
|
*/
|
|
193
198
|
export function daemonCmdRoot(dir) {
|
|
194
199
|
let start = path.resolve(dir);
|
|
@@ -198,10 +203,16 @@ export function daemonCmdRoot(dir) {
|
|
|
198
203
|
catch {
|
|
199
204
|
/* not there: the spelling given */
|
|
200
205
|
}
|
|
206
|
+
const setUp = (d) => (process.env.VEXP_ALLOW_HOME_WORKSPACE === "1" ||
|
|
207
|
+
(upgradeAttemptKey(d) !== upgradeAttemptKey(vexpHome()) && path.dirname(d) !== d)) &&
|
|
208
|
+
["manifest.json", "vexp.toml", "workspace.json", "index.db"].some((f) => fs.existsSync(path.join(d, ".vexp", f)));
|
|
209
|
+
const below = [];
|
|
201
210
|
let current = start;
|
|
202
211
|
for (;;) {
|
|
203
|
-
if (fs.existsSync(path.join(current, ".git")))
|
|
204
|
-
return current;
|
|
212
|
+
if (fs.existsSync(path.join(current, ".git"))) {
|
|
213
|
+
return setUp(current) ? current : (below.find(setUp) ?? current);
|
|
214
|
+
}
|
|
215
|
+
below.push(current);
|
|
205
216
|
const parent = path.dirname(current);
|
|
206
217
|
if (parent === current)
|
|
207
218
|
return start;
|
|
@@ -215,8 +226,10 @@ export function daemonCmdRoot(dir) {
|
|
|
215
226
|
*
|
|
216
227
|
* - the stop half reads `<dir>/.vexp/daemon.pid`; with none there it stops
|
|
217
228
|
* nothing, and the start half then races the daemon that is still up;
|
|
218
|
-
* - `daemon-cmd` works on the git root at or above its cwd
|
|
219
|
-
*
|
|
229
|
+
* - `daemon-cmd` works on the git root at or above its cwd unless a folder
|
|
230
|
+
* below an unset-up root was set up on its own (daemonCmdRoot), so for
|
|
231
|
+
* any other workspace below one it would start a daemon on the folder
|
|
232
|
+
* above;
|
|
220
233
|
* - the engine refuses to start on the home directory, so a daemon an older
|
|
221
234
|
* build left there would be stopped and not come back.
|
|
222
235
|
*/
|