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/devmind.js
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
// DevMind (vexp's optional local LLM) as the CLI tells it.
|
|
2
|
+
//
|
|
3
|
+
// Since 3.3.9 DevMind runs only on a GPU. Every frontend rendered that
|
|
4
|
+
// policy as a fault: doctor counted a WARN with "restarting will NOT change
|
|
5
|
+
// this - fix the cause above", the REPL said "(active)" from the config
|
|
6
|
+
// alone, and the sidebar showed a warning banner. A user spent an hour
|
|
7
|
+
// trying to fix a machine that had nothing wrong with it (field report,
|
|
8
|
+
// 2026-11-08). The engine now names the cause (index_status.llm_off_cause,
|
|
9
|
+
// `setup-llm --status` "DevMind:" and "Off cause:" lines); this module reads
|
|
10
|
+
// it and words it, so doctor, the REPL and the report agree.
|
|
11
|
+
//
|
|
12
|
+
// The constants and the kind strings are the engine's
|
|
13
|
+
// (packages/vexp-core/src/prose_compress/llm/gate.rs). Lockstep with that
|
|
14
|
+
// file, with packages/vexp-vscode/src/ui/llm-advice.ts and with
|
|
15
|
+
// packages/vexp-mcp/src/tools/index-status.ts
|
|
16
|
+
// (test/devmind.lockstep.test.ts reads gate.rs).
|
|
17
|
+
/** The one reassurance sentence (gate.rs DEVMIND_OPTIONAL). */
|
|
18
|
+
export const DEVMIND_OPTIONAL = "DevMind is optional: everything else in vexp works the same without it.";
|
|
19
|
+
/** The requirement, said before anyone downloads (gate.rs DEVMIND_REQUIREMENT). */
|
|
20
|
+
export const DEVMIND_REQUIREMENT = "DevMind needs an NVIDIA GPU (Linux, Windows) or an Apple Silicon Mac; without one it stays off and vexp works the same.";
|
|
21
|
+
/** gate.rs `kind::ALL`, in the order the gate checks them. */
|
|
22
|
+
export const DEVMIND_OFF_KINDS = [
|
|
23
|
+
"no_gpu",
|
|
24
|
+
"container_no_gpu",
|
|
25
|
+
"intel_mac",
|
|
26
|
+
"prefer_gpu_off",
|
|
27
|
+
"gpu_failed_before",
|
|
28
|
+
"gpu_too_old",
|
|
29
|
+
"gpu_too_small",
|
|
30
|
+
"cuda_plugin_missing",
|
|
31
|
+
"cuda_plugin_broken",
|
|
32
|
+
"gpu_init_failed",
|
|
33
|
+
];
|
|
34
|
+
/** gate.rs `kind_is_fixable`: the user can change something here. Frontends read `llm_off_fixable`; this is for the lockstep test. */
|
|
35
|
+
export const DEVMIND_FIXABLE_KINDS = ["prefer_gpu_off", "gpu_failed_before", "cuda_plugin_missing", "cuda_plugin_broken", "gpu_init_failed"];
|
|
36
|
+
/**
|
|
37
|
+
* Is the rule compressor this daemon runs DevMind's policy, not a fault?
|
|
38
|
+
* A daemon from 3.4.0 always sends `llm_off_cause`: a kind string, or null
|
|
39
|
+
* for every fault (an endpoint that does not answer, a model that failed to
|
|
40
|
+
* load, the startup sync). A daemon without the field (3.3.9) said "DevMind
|
|
41
|
+
* needs a GPU and none is usable on this machine" for every policy cause,
|
|
42
|
+
* and nothing else it said contains "needs a GPU": that sentence is the
|
|
43
|
+
* policy, with no kind and nothing the user is told to fix.
|
|
44
|
+
*/
|
|
45
|
+
export function devmindOff(st) {
|
|
46
|
+
if (!st || st.compressor === "llm")
|
|
47
|
+
return null;
|
|
48
|
+
const raw = typeof st.llm_inactive_reason === "string" ? st.llm_inactive_reason : "";
|
|
49
|
+
const reason = raw.trim().replace(/\.+$/, "");
|
|
50
|
+
if ("llm_off_cause" in st) {
|
|
51
|
+
if (typeof st.llm_off_cause !== "string" || st.llm_off_cause === "")
|
|
52
|
+
return null;
|
|
53
|
+
return { kind: st.llm_off_cause, fixable: st.llm_off_fixable === true, reason: reason || "DevMind needs a GPU" };
|
|
54
|
+
}
|
|
55
|
+
if (/needs a GPU/i.test(reason))
|
|
56
|
+
return { kind: null, fixable: false, reason };
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The note doctor prints for DevMind off by policy, at INFO: nothing is
|
|
61
|
+
* broken, so it counts as no warning and brings no restart tip. Word for
|
|
62
|
+
* word the extension Doctor's line (packages/vexp-vscode doctor-lines.ts).
|
|
63
|
+
* A fixable cause adds the restart the fix needs: the daemon decides at its
|
|
64
|
+
* start.
|
|
65
|
+
*/
|
|
66
|
+
export function devmindOffNote(off) {
|
|
67
|
+
return (`DevMind (optional local LLM) is off on this machine: ${off.reason}. ${DEVMIND_OPTIONAL} ` +
|
|
68
|
+
`'vexp setup-llm --disable' hides this note; 'vexp setup-llm --uninstall' also frees the disk space.` +
|
|
69
|
+
(off.fixable ? " After the fix, restart the daemon." : ""));
|
|
70
|
+
}
|
|
71
|
+
/** An `[llm] endpoint` that did not answer when the daemon started (gate.rs endpoint_unreachable_reason). */
|
|
72
|
+
export function isEndpointUnreachable(reason) {
|
|
73
|
+
return reason !== undefined && /^endpoint \S+ unreachable \(/.test(reason);
|
|
74
|
+
}
|
|
75
|
+
const statusField = (stdout, label) => {
|
|
76
|
+
const m = new RegExp(`^[ \\t]*${label}:[ \\t]*(.*?)[ \\t]*$`, "m").exec(stdout);
|
|
77
|
+
return m ? m[1] : undefined;
|
|
78
|
+
};
|
|
79
|
+
export function parseSetupLlmStatus(stdout) {
|
|
80
|
+
const build = statusField(stdout, "Build");
|
|
81
|
+
const devmindLine = statusField(stdout, "DevMind");
|
|
82
|
+
let devmind = null;
|
|
83
|
+
if (devmindLine !== undefined) {
|
|
84
|
+
const runs = /^runs here \((.*)\)$/.exec(devmindLine);
|
|
85
|
+
const off = /^off here - (.*)$/.exec(devmindLine);
|
|
86
|
+
if (runs)
|
|
87
|
+
devmind = { runs: true, label: runs[1] };
|
|
88
|
+
else if (off)
|
|
89
|
+
devmind = { runs: false, reason: off[1].replace(/\.+$/, "") };
|
|
90
|
+
}
|
|
91
|
+
const daemonLine = statusField(stdout, "Daemon now");
|
|
92
|
+
let daemon = null;
|
|
93
|
+
if (daemonLine !== undefined) {
|
|
94
|
+
if (/^v\S* running the model/.test(daemonLine))
|
|
95
|
+
daemon = { state: "model" };
|
|
96
|
+
else if (/^no daemon answered/.test(daemonLine))
|
|
97
|
+
daemon = { state: "none" };
|
|
98
|
+
else if (/^v\S* using the /.test(daemonLine)) {
|
|
99
|
+
// The daemon's reason is the line right under it, when it gave one.
|
|
100
|
+
const lines = stdout.split(/\r?\n/);
|
|
101
|
+
const at = lines.findIndex((l) => /^[ \t]*Daemon now:/.test(l));
|
|
102
|
+
const why = /^[ \t]*reason:[ \t]*(.*?)[ \t]*$/.exec(lines[at + 1] ?? "");
|
|
103
|
+
daemon = { state: "rule", reason: why?.[1] || undefined };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const endpoint = statusField(stdout, "Endpoint");
|
|
107
|
+
return {
|
|
108
|
+
build: build === undefined ? null : /vanilla/i.test(build) ? "vanilla" : "llm",
|
|
109
|
+
enabled: /^true$/i.test(statusField(stdout, "Enabled") ?? ""),
|
|
110
|
+
installed: /^yes\b/i.test(statusField(stdout, "Installed") ?? ""),
|
|
111
|
+
operational: /^true$/i.test(statusField(stdout, "Operational") ?? ""),
|
|
112
|
+
model: statusField(stdout, "Model") ?? "",
|
|
113
|
+
devmind,
|
|
114
|
+
offCause: statusField(stdout, "Off cause") || null,
|
|
115
|
+
preferGpuOff: /^false$/i.test(statusField(stdout, "prefer_gpu") ?? ""),
|
|
116
|
+
endpoint: endpoint ? endpoint.replace(/\s*\(configured\)\s*$/, "") : null,
|
|
117
|
+
daemon,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
export const LLM_MENU = [
|
|
121
|
+
{ key: "1", label: "Install / Reinstall", note: "(~3.5 GB model; needs an NVIDIA GPU or Apple Silicon)", flag: "--install", aliases: ["install", "reinstall"] },
|
|
122
|
+
{ key: "2", label: "Start", note: "(enable without re-downloading)", flag: "--enable", aliases: ["start", "enable"] },
|
|
123
|
+
{ key: "3", label: "Check for updates", note: "(no download if up to date)", flag: "--check-updates", aliases: ["update", "updates", "check-updates"] },
|
|
124
|
+
{ key: "4", label: "Status", note: "(config, hardware, whether DevMind runs here)", flag: "--status", aliases: ["status"] },
|
|
125
|
+
{ key: "5", label: "Disable", note: "(keep files, stop using LLM)", flag: "--disable", aliases: ["disable", "stop"] },
|
|
126
|
+
{ key: "6", label: "Remove files", note: "(delete the model and CUDA files vexp downloaded)", flag: "--uninstall", aliases: ["uninstall", "remove"] },
|
|
127
|
+
];
|
|
128
|
+
/** The entry a typed answer at the llm> prompt picks: its key or one of its words. */
|
|
129
|
+
export function llmMenuChoice(input) {
|
|
130
|
+
const c = input.trim().toLowerCase();
|
|
131
|
+
return LLM_MENU.find((e) => e.key === c || e.aliases.includes(c));
|
|
132
|
+
}
|
|
133
|
+
/** What to type from the main menu to reach the entry for `flag`: Setup & Config (1), setup-llm (6), then its key. */
|
|
134
|
+
export function llmMenuPath(flag) {
|
|
135
|
+
const e = LLM_MENU.find((m) => m.flag === flag);
|
|
136
|
+
if (!e)
|
|
137
|
+
throw new Error(`no llm> entry for ${flag}`);
|
|
138
|
+
return `1 > 6 > ${e.key}`;
|
|
139
|
+
}
|
|
140
|
+
/** A banner line as the REPL prints it. */
|
|
141
|
+
export function bannerText(l) {
|
|
142
|
+
return `${l.mark === "on" ? " ● " : l.mark === "off" ? " ○ " : " → "}${l.text}`;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* The REPL banner's LLM lines. "(active)" only when the workspace daemon
|
|
146
|
+
* runs the model: "Operational: true" is the config's truth, and it was
|
|
147
|
+
* shown as active on every machine where DevMind stays off (review,
|
|
148
|
+
* 2026-11-08). Off here is said calmly, with no download offered.
|
|
149
|
+
*
|
|
150
|
+
* The local gate speaks only for the local model. An `[llm] endpoint`
|
|
151
|
+
* serves without it, and a daemon that runs a model is the answer over
|
|
152
|
+
* both: "(off here: needs a GPU)" and "'1 > 6 > 5' turns it off" were told
|
|
153
|
+
* to an NPU user whose endpoint was serving. And a CUDA plugin that is
|
|
154
|
+
* missing (on every NVIDIA machine before the first setup) or does not load
|
|
155
|
+
* is what the install fetches: "needs a GPU" was told to a fresh RTX user,
|
|
156
|
+
* with a download hint that left out the model (review, 2026-11-08).
|
|
157
|
+
* Lockstep with the sidebar's engineOff / pluginMissing (vexp-vscode
|
|
158
|
+
* sidebar-panel.ts).
|
|
159
|
+
*/
|
|
160
|
+
export function llmBannerLines(s) {
|
|
161
|
+
const name = s.model || "local";
|
|
162
|
+
if (s.build === "vanilla") {
|
|
163
|
+
const out = [{ tone: "dim", mark: "off", text: "LLM: not in this build (everything else works the same, with the rule compressor)" }];
|
|
164
|
+
if (s.enabled) {
|
|
165
|
+
out.push({ tone: "dim", mark: "hint", text: `your config still switches it on, left from a build that had one: type '${llmMenuPath("--disable")}', or \`vexp setup-llm --disable\`` });
|
|
166
|
+
}
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
169
|
+
if (s.operational && s.daemon?.state === "model") {
|
|
170
|
+
return [{ tone: "green", mark: "on", text: s.endpoint ? `LLM: active, through the [llm] endpoint ${s.endpoint}` : `LLM: ${name} (active)` }];
|
|
171
|
+
}
|
|
172
|
+
// A 3.3.9 engine has no DevMind line: its daemon's one policy sentence.
|
|
173
|
+
const offReason = s.endpoint
|
|
174
|
+
? null
|
|
175
|
+
: s.devmind && !s.devmind.runs
|
|
176
|
+
? s.devmind.reason
|
|
177
|
+
: s.devmind === null && s.daemon?.state === "rule" && /needs a GPU/i.test(s.daemon.reason ?? "")
|
|
178
|
+
? s.daemon.reason
|
|
179
|
+
: null;
|
|
180
|
+
if (offReason !== null) {
|
|
181
|
+
if (s.offCause === "cuda_plugin_missing" || s.offCause === "cuda_plugin_broken") {
|
|
182
|
+
if (!s.installed && !s.enabled) {
|
|
183
|
+
return [
|
|
184
|
+
{ tone: "dim", mark: "off", text: "LLM: not installed" },
|
|
185
|
+
{ tone: "dim", mark: "hint", text: `type '${llmMenuPath("--install")}' to download (~3.5 GB model + ~1 GB CUDA plugin), or \`vexp setup-llm --install\`` },
|
|
186
|
+
];
|
|
187
|
+
}
|
|
188
|
+
const missing = s.offCause === "cuda_plugin_missing";
|
|
189
|
+
return [
|
|
190
|
+
{ tone: "yellow", mark: "off", text: `LLM: ${name} (${missing ? "CUDA plugin not installed" : "CUDA plugin does not load"})` },
|
|
191
|
+
{
|
|
192
|
+
tone: "dim",
|
|
193
|
+
mark: "hint",
|
|
194
|
+
text: missing
|
|
195
|
+
? `type '${llmMenuPath("--install")}' to download vexp's CUDA plugin, or \`vexp setup-llm --install\``
|
|
196
|
+
: `type '${llmMenuPath("--install")}' to fetch the CUDA runtime it needs (and the plugin again), or \`vexp setup-llm --install\``,
|
|
197
|
+
},
|
|
198
|
+
];
|
|
199
|
+
}
|
|
200
|
+
if (!s.enabled && !s.installed) {
|
|
201
|
+
return [
|
|
202
|
+
{ tone: "dim", mark: "off", text: "LLM: off here (needs a GPU)" },
|
|
203
|
+
{ tone: "dim", mark: "hint", text: DEVMIND_REQUIREMENT },
|
|
204
|
+
];
|
|
205
|
+
}
|
|
206
|
+
const out = [{ tone: "dim", mark: "off", text: `LLM: ${name} (off here: needs a GPU)` }];
|
|
207
|
+
out.push({ tone: "dim", mark: "hint", text: `${DEVMIND_OPTIONAL} Why: '${llmMenuPath("--status")}'` });
|
|
208
|
+
out.push({
|
|
209
|
+
tone: "dim",
|
|
210
|
+
mark: "hint",
|
|
211
|
+
text: s.enabled
|
|
212
|
+
? `'${llmMenuPath("--disable")}' turns it off; '${llmMenuPath("--uninstall")}' also frees the disk space`
|
|
213
|
+
: `'${llmMenuPath("--uninstall")}' frees the disk space (\`vexp setup-llm --uninstall\`)`,
|
|
214
|
+
});
|
|
215
|
+
return out;
|
|
216
|
+
}
|
|
217
|
+
if (s.operational) {
|
|
218
|
+
if (s.daemon?.state === "rule") {
|
|
219
|
+
const out = [{ tone: "yellow", mark: "off", text: `LLM: ${name} (enabled, but this daemon runs the rule compressor)` }];
|
|
220
|
+
if (s.daemon.reason)
|
|
221
|
+
out.push({ tone: "dim", mark: "hint", text: `reason: ${s.daemon.reason}` });
|
|
222
|
+
out.push({ tone: "dim", mark: "hint", text: "`vexp doctor` says what to do" });
|
|
223
|
+
return out;
|
|
224
|
+
}
|
|
225
|
+
if (s.daemon?.state === "none")
|
|
226
|
+
return [{ tone: "green", mark: "on", text: `LLM: ${name} (enabled: the daemon loads it when it starts)` }];
|
|
227
|
+
return [{ tone: "green", mark: "on", text: `LLM: ${name} (enabled)` }];
|
|
228
|
+
}
|
|
229
|
+
if (s.installed) {
|
|
230
|
+
return [
|
|
231
|
+
{ tone: "yellow", mark: "off", text: "LLM: installed but not active" },
|
|
232
|
+
{ tone: "dim", mark: "hint", text: `type '${llmMenuPath("--enable")}' to start it, or \`vexp setup-llm --enable\`` },
|
|
233
|
+
];
|
|
234
|
+
}
|
|
235
|
+
return [
|
|
236
|
+
{ tone: "dim", mark: "off", text: "LLM: not installed" },
|
|
237
|
+
{ tone: "dim", mark: "hint", text: `type '${llmMenuPath("--install")}' to download (~3.5 GB), or \`vexp setup-llm --install\`` },
|
|
238
|
+
];
|
|
239
|
+
}
|
|
240
|
+
// The `setup-llm --status` lines a doctor report carries. An allow-list:
|
|
241
|
+
// the rest of the output (library inventory, file listings) is noise in a
|
|
242
|
+
// report, and nothing else can carry a secret by accident. The engine
|
|
243
|
+
// prints the endpoint's origin only and never the api key; the Endpoint
|
|
244
|
+
// line is stripped of userinfo and query here as well, and the report masks
|
|
245
|
+
// secret shapes on top.
|
|
246
|
+
const REPORT_LABELS = /^[ \t]*(Build|Enabled|Model|Installed|Operational|DevMind|Off cause|prefer_gpu|Endpoint|Model file|DevMind files|Accelerator|Plugin|GPU|GPU offload|GPU in use|Daemon now):/;
|
|
247
|
+
const REPORT_FOLLOWERS = new Set(["GPU offload", "GPU in use", "Daemon now"]);
|
|
248
|
+
/** The lines of `setup-llm --status` that the doctor report's LLM section quotes. */
|
|
249
|
+
export function llmStatusReportLines(stdout) {
|
|
250
|
+
const out = [];
|
|
251
|
+
let follows = false;
|
|
252
|
+
for (const raw of stdout.split(/\r?\n/)) {
|
|
253
|
+
const line = raw.replace(/\s+$/, "");
|
|
254
|
+
const m = REPORT_LABELS.exec(line);
|
|
255
|
+
if (m) {
|
|
256
|
+
const text = m[1] === "Endpoint" ? line.replace(/\/\/[^/@\s]*@/, "//").replace(/[?#]\S*/, "") : line;
|
|
257
|
+
out.push(text);
|
|
258
|
+
follows = REPORT_FOLLOWERS.has(m[1]);
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
// The indented reason/verdict/marker lines under the three lines that have them.
|
|
262
|
+
if (follows && /^[ \t]{6,}(reason|verdict|marker|retry|note|spent):/.test(line)) {
|
|
263
|
+
out.push(line);
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
follows = false;
|
|
267
|
+
}
|
|
268
|
+
return out;
|
|
269
|
+
}
|