vexp-cli 2.0.11 → 2.0.13
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 +154 -75
- package/dist/autostart.js +375 -0
- package/dist/binary.js +63 -0
- package/dist/cli.js +258 -105
- package/dist/mcp-supervisor.js +235 -0
- package/dist/serve.js +160 -0
- package/dist/trace.js +80 -0
- package/mcp/mcp-server.cjs +38 -38
- package/package.json +6 -6
package/dist/cli.js
CHANGED
|
@@ -12,11 +12,21 @@ import { detectAgents, getAgentList, configureSelectedAgents } from "./agent-con
|
|
|
12
12
|
import { CLI_VERSION } from "./version.js";
|
|
13
13
|
import { activateLicense, deactivateLicense, readLicenseLimits, readDeviceBlocked, } from "./license.js";
|
|
14
14
|
import { checkForUpdate } from "./update-check.js";
|
|
15
|
+
import { ensureMcpHttpServer, mcpHttpStatus } from "./mcp-supervisor.js";
|
|
16
|
+
import { installAutostart, installAutostartIfNeeded, uninstallAutostart, autostartStatus } from "./autostart.js";
|
|
17
|
+
import { runServe } from "./serve.js";
|
|
18
|
+
import { isTraceEnabled } from "./trace.js";
|
|
15
19
|
const program = new Command();
|
|
16
20
|
program
|
|
17
21
|
.name("vexp")
|
|
18
22
|
.description("Vexp — Graph-RAG context engine for AI coding agents")
|
|
19
|
-
.version(CLI_VERSION)
|
|
23
|
+
.version(CLI_VERSION)
|
|
24
|
+
.option("-i, --trace", "Verbose trace mode: print every step with timing (IPs/tokens redacted)");
|
|
25
|
+
// Propagate -i / --trace to spawned children via env var so subprocess
|
|
26
|
+
// (daemon, MCP, serve) inherits the trace flag.
|
|
27
|
+
if (isTraceEnabled()) {
|
|
28
|
+
process.env.VEXP_TRACE = "1";
|
|
29
|
+
}
|
|
20
30
|
// ────────────────────────────────────────────────────
|
|
21
31
|
// Auto-start daemon before any action that needs it
|
|
22
32
|
// ────────────────────────────────────────────────────
|
|
@@ -27,6 +37,7 @@ program
|
|
|
27
37
|
// workspace was already configured (marker: .vexp/manifest.json).
|
|
28
38
|
const AUTOSTART_SKIP = new Set([
|
|
29
39
|
"setup", "daemon-cmd", "activate", "deactivate", "license", "version",
|
|
40
|
+
"serve", "autostart", "use",
|
|
30
41
|
]);
|
|
31
42
|
program.hook("preAction", async (_thisCmd, actionCmd) => {
|
|
32
43
|
if (process.env.VEXP_NO_AUTOSTART === "1")
|
|
@@ -43,7 +54,9 @@ program.hook("preAction", async (_thisCmd, actionCmd) => {
|
|
|
43
54
|
catch {
|
|
44
55
|
return;
|
|
45
56
|
}
|
|
46
|
-
|
|
57
|
+
// Full bootstrap: daemon + MCP + autostart-if-needed. All three steps are
|
|
58
|
+
// internally idempotent — no duplicate processes across invocations.
|
|
59
|
+
await ensureBootstrap(workspaceRoot, binaryPath);
|
|
47
60
|
});
|
|
48
61
|
// ────────────────────────────────────────────────────
|
|
49
62
|
// Resolve the platform binary (no network — bundled via npm)
|
|
@@ -133,15 +146,6 @@ function findConfiguredWorkspace(startDir = process.cwd()) {
|
|
|
133
146
|
current = parent;
|
|
134
147
|
}
|
|
135
148
|
}
|
|
136
|
-
/** Quick TCP probe — true if something is already listening on 127.0.0.1:port. */
|
|
137
|
-
function isPortInUse(port) {
|
|
138
|
-
return new Promise((resolve) => {
|
|
139
|
-
const sock = net.createConnection({ port, host: "127.0.0.1" });
|
|
140
|
-
const timer = setTimeout(() => { sock.destroy(); resolve(false); }, 300);
|
|
141
|
-
sock.on("connect", () => { clearTimeout(timer); sock.destroy(); resolve(true); });
|
|
142
|
-
sock.on("error", () => { clearTimeout(timer); resolve(false); });
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
149
|
/** True if the Unix socket at `socketPath` accepts a connection within `timeoutMs`. */
|
|
146
150
|
function isSocketAlive(socketPath, timeoutMs = 300) {
|
|
147
151
|
return new Promise((resolve) => {
|
|
@@ -199,6 +203,33 @@ async function ensureDaemonRunning(workspaceRoot, binaryPath) {
|
|
|
199
203
|
console.error(chalk.yellow(` ⚠ Could not auto-start vexp daemon — see ${logPath}`));
|
|
200
204
|
}
|
|
201
205
|
}
|
|
206
|
+
/**
|
|
207
|
+
* Full lazy bootstrap for any `vexp` invocation. Each step is already
|
|
208
|
+
* idempotent on its own — calling this from multiple places (preAction,
|
|
209
|
+
* REPL, setup) can never spawn duplicates:
|
|
210
|
+
* - `ensureDaemonRunning` → socket probe, skips spawn if alive
|
|
211
|
+
* - `ensureMcpHttpServer` → ~/.vexp/mcp.pid + port probe, no-op if live
|
|
212
|
+
* - `installAutostartIfNeeded` → marker ~/.vexp/autostart-installed
|
|
213
|
+
*
|
|
214
|
+
* The point of this wrapper is to ensure users who upgraded via
|
|
215
|
+
* `npm update` and then just typed `vexp` (never re-running `setup`) still
|
|
216
|
+
* get: daemon up, MCP :7821 up, OS autostart installed for future reboots.
|
|
217
|
+
*
|
|
218
|
+
* Honors VEXP_NO_AUTOSTART=1 as a global bypass.
|
|
219
|
+
*/
|
|
220
|
+
async function ensureBootstrap(workspaceRoot, binaryPath) {
|
|
221
|
+
if (process.env.VEXP_NO_AUTOSTART === "1")
|
|
222
|
+
return;
|
|
223
|
+
await ensureDaemonRunning(workspaceRoot, binaryPath);
|
|
224
|
+
try {
|
|
225
|
+
await ensureMcpHttpServer({ owner: "cli" });
|
|
226
|
+
}
|
|
227
|
+
catch { /* non-fatal */ }
|
|
228
|
+
try {
|
|
229
|
+
installAutostartIfNeeded();
|
|
230
|
+
}
|
|
231
|
+
catch { /* non-fatal */ }
|
|
232
|
+
}
|
|
202
233
|
/**
|
|
203
234
|
* Start the vexp daemon and MCP HTTP server in the background.
|
|
204
235
|
* Used by both the non-interactive `setup` command and the REPL setup flow.
|
|
@@ -222,52 +253,28 @@ async function startBackgroundServices(binaryPath, workspaceRoot) {
|
|
|
222
253
|
catch {
|
|
223
254
|
spinnerDaemon.warn("Daemon already running or failed to start (non-fatal)");
|
|
224
255
|
}
|
|
225
|
-
// Start MCP HTTP server
|
|
226
|
-
//
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
256
|
+
// Start MCP HTTP server via the shared supervisor — singleton across
|
|
257
|
+
// CLI, VS Code plugin, and `vexp serve` (tracked by ~/.vexp/mcp.pid).
|
|
258
|
+
// Workspace-agnostic: routing is done via URL path /ws/<hash>/mcp.
|
|
259
|
+
const spinnerMcp = ora("Starting MCP HTTP server...").start();
|
|
260
|
+
try {
|
|
261
|
+
const result = await ensureMcpHttpServer({ owner: "cli" });
|
|
262
|
+
if (!result) {
|
|
263
|
+
spinnerMcp.warn("MCP HTTP server not available (missing mcp-server bundle)");
|
|
264
|
+
}
|
|
265
|
+
else if (result.pid === -1) {
|
|
266
|
+
spinnerMcp.warn(`Port ${result.port} already bound by an unknown process`);
|
|
267
|
+
}
|
|
268
|
+
else if (result.started) {
|
|
269
|
+
spinnerMcp.succeed(`MCP HTTP server started (pid=${result.pid}, port=${result.port})`);
|
|
232
270
|
}
|
|
233
271
|
else {
|
|
234
|
-
|
|
235
|
-
const { randomUUID } = await import("crypto");
|
|
236
|
-
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
237
|
-
let mcpToken = "";
|
|
238
|
-
const tokenPath = home ? path.join(home, ".vexp", "mcp.token") : "";
|
|
239
|
-
if (tokenPath) {
|
|
240
|
-
try {
|
|
241
|
-
mcpToken = fs.readFileSync(tokenPath, "utf-8").trim();
|
|
242
|
-
}
|
|
243
|
-
catch { }
|
|
244
|
-
}
|
|
245
|
-
if (!mcpToken) {
|
|
246
|
-
mcpToken = randomUUID();
|
|
247
|
-
if (tokenPath) {
|
|
248
|
-
fs.mkdirSync(path.dirname(tokenPath), { recursive: true });
|
|
249
|
-
fs.writeFileSync(tokenPath, mcpToken, { mode: 0o600 });
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
const mcpChild = spawn("node", [mcpPath, "--http", "--port=7821"], {
|
|
253
|
-
detached: true,
|
|
254
|
-
stdio: "ignore",
|
|
255
|
-
env: {
|
|
256
|
-
...process.env,
|
|
257
|
-
VEXP_WORKSPACE: workspaceRoot,
|
|
258
|
-
VEXP_HTTP: "1",
|
|
259
|
-
VEXP_PORT: "7821",
|
|
260
|
-
VEXP_MCP_TOKEN: mcpToken,
|
|
261
|
-
},
|
|
262
|
-
});
|
|
263
|
-
mcpChild.unref();
|
|
264
|
-
spinnerMcp.succeed("MCP HTTP server started (port 7821)");
|
|
265
|
-
}
|
|
266
|
-
catch {
|
|
267
|
-
spinnerMcp.warn("MCP HTTP server failed to start (non-fatal)");
|
|
268
|
-
}
|
|
272
|
+
spinnerMcp.succeed(`MCP HTTP server already running (pid=${result.pid}, port=${result.port})`);
|
|
269
273
|
}
|
|
270
274
|
}
|
|
275
|
+
catch (err) {
|
|
276
|
+
spinnerMcp.warn(`MCP HTTP server failed to start: ${err instanceof Error ? err.message : err}`);
|
|
277
|
+
}
|
|
271
278
|
}
|
|
272
279
|
// ────────────────────────────────────────────────────
|
|
273
280
|
// Commands
|
|
@@ -602,6 +609,22 @@ program
|
|
|
602
609
|
}
|
|
603
610
|
}
|
|
604
611
|
}
|
|
612
|
+
// Step 5: Install login autostart (per-user, no admin required)
|
|
613
|
+
if (process.env.VEXP_NO_AUTOSTART_INSTALL !== "1") {
|
|
614
|
+
const spinnerAuto = ora("Installing login autostart...").start();
|
|
615
|
+
try {
|
|
616
|
+
const r = installAutostart();
|
|
617
|
+
if (r.installed) {
|
|
618
|
+
spinnerAuto.succeed(`Login autostart installed (${r.mechanism})`);
|
|
619
|
+
}
|
|
620
|
+
else {
|
|
621
|
+
spinnerAuto.warn("Autostart not installed on this platform");
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
catch (err) {
|
|
625
|
+
spinnerAuto.warn(`Autostart install failed: ${err instanceof Error ? err.message : err}`);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
605
628
|
// Final summary
|
|
606
629
|
console.log("");
|
|
607
630
|
console.log(chalk.green.bold("✓ vexp setup complete!"));
|
|
@@ -743,6 +766,15 @@ async function runSetupInteractive(rl) {
|
|
|
743
766
|
}
|
|
744
767
|
console.log(chalk.green(`\n ✓ Setup complete — ${result.agents.length} agent(s) configured\n`));
|
|
745
768
|
}
|
|
769
|
+
// Login autostart — opt-out via env
|
|
770
|
+
if (process.env.VEXP_NO_AUTOSTART_INSTALL !== "1") {
|
|
771
|
+
try {
|
|
772
|
+
const r = installAutostart();
|
|
773
|
+
if (r.installed)
|
|
774
|
+
console.log(chalk.green(` ✓ Login autostart installed (${r.mechanism})`));
|
|
775
|
+
}
|
|
776
|
+
catch { /* non-fatal */ }
|
|
777
|
+
}
|
|
746
778
|
}
|
|
747
779
|
// ────────────────────────────────────────────────────
|
|
748
780
|
// License management
|
|
@@ -804,6 +836,80 @@ program
|
|
|
804
836
|
console.log("");
|
|
805
837
|
});
|
|
806
838
|
// ────────────────────────────────────────────────────
|
|
839
|
+
// Background supervisor + OS autostart
|
|
840
|
+
// ────────────────────────────────────────────────────
|
|
841
|
+
program
|
|
842
|
+
.command("serve")
|
|
843
|
+
.description("Run the login supervisor: resurrect known daemons + keep MCP HTTP alive (used by autostart)")
|
|
844
|
+
.action(async () => {
|
|
845
|
+
await runServe();
|
|
846
|
+
});
|
|
847
|
+
program
|
|
848
|
+
.command("autostart <action>")
|
|
849
|
+
.description("Manage login autostart: install | uninstall | status")
|
|
850
|
+
.action(async (action) => {
|
|
851
|
+
if (action === "install") {
|
|
852
|
+
const r = installAutostart();
|
|
853
|
+
if (r.installed) {
|
|
854
|
+
console.log(chalk.green(`✓ Autostart installed via ${r.mechanism}`));
|
|
855
|
+
if (r.path)
|
|
856
|
+
console.log(chalk.dim(` ${r.path}`));
|
|
857
|
+
}
|
|
858
|
+
else {
|
|
859
|
+
console.log(chalk.yellow("⚠ Autostart not installed (unsupported platform or insufficient permissions)"));
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
else if (action === "uninstall") {
|
|
863
|
+
uninstallAutostart();
|
|
864
|
+
console.log(chalk.yellow("✓ Autostart removed (daemons already running are not killed)"));
|
|
865
|
+
}
|
|
866
|
+
else if (action === "status") {
|
|
867
|
+
const s = autostartStatus();
|
|
868
|
+
const mcp = mcpHttpStatus();
|
|
869
|
+
console.log(chalk.bold("\nvexp Autostart Status\n"));
|
|
870
|
+
if (s.installed) {
|
|
871
|
+
console.log(` Installed: ${chalk.green("yes")} (${s.mechanism})`);
|
|
872
|
+
if (s.path)
|
|
873
|
+
console.log(` Path: ${s.path}`);
|
|
874
|
+
}
|
|
875
|
+
else {
|
|
876
|
+
console.log(` Installed: ${chalk.dim("no")}`);
|
|
877
|
+
}
|
|
878
|
+
if (mcp.running) {
|
|
879
|
+
console.log(` MCP: ${chalk.green(`running pid=${mcp.pid} port=${mcp.port}`)} (${mcp.owner})`);
|
|
880
|
+
}
|
|
881
|
+
else {
|
|
882
|
+
console.log(` MCP: ${chalk.dim("not running")}`);
|
|
883
|
+
}
|
|
884
|
+
console.log("");
|
|
885
|
+
}
|
|
886
|
+
else {
|
|
887
|
+
console.error(chalk.red(`Unknown action: ${action}. Use install | uninstall | status`));
|
|
888
|
+
process.exit(1);
|
|
889
|
+
}
|
|
890
|
+
});
|
|
891
|
+
program
|
|
892
|
+
.command("use <workspacePath>")
|
|
893
|
+
.description("Switch codex (global MCP config) to point at a different configured workspace")
|
|
894
|
+
.action(async (workspacePath) => {
|
|
895
|
+
const resolved = path.resolve(workspacePath);
|
|
896
|
+
const manifest = path.join(resolved, ".vexp", "manifest.json");
|
|
897
|
+
if (!fs.existsSync(manifest)) {
|
|
898
|
+
console.error(chalk.red(`No .vexp/manifest.json at ${resolved}. Run 'vexp setup' there first.`));
|
|
899
|
+
process.exit(1);
|
|
900
|
+
}
|
|
901
|
+
const binaryPath = ensureBinary();
|
|
902
|
+
const mcpServerPath = getMcpServerPath();
|
|
903
|
+
// Import lazily so configureCodexGlobal's side effects (TOML rewrite + legacy cleanup) only fire here.
|
|
904
|
+
const { configureSelectedAgents } = await import("./agent-config.js");
|
|
905
|
+
const version = (await getInstalledVersion()) ?? CLI_VERSION;
|
|
906
|
+
// "Codex" is a no-op file-wise in non-detect mode but the shared flow
|
|
907
|
+
// also writes codex global config via configureCodexGlobal — pass an
|
|
908
|
+
// empty agent list so only the MCP side runs.
|
|
909
|
+
configureSelectedAgents(resolved, binaryPath, version, ["Codex"], mcpServerPath);
|
|
910
|
+
console.log(chalk.green(`✓ codex now points at ${resolved}`));
|
|
911
|
+
});
|
|
912
|
+
// ────────────────────────────────────────────────────
|
|
807
913
|
// Version
|
|
808
914
|
// ────────────────────────────────────────────────────
|
|
809
915
|
program
|
|
@@ -857,7 +963,7 @@ function shortPath(p) {
|
|
|
857
963
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
858
964
|
return home && p.startsWith(home) ? "~" + p.slice(home.length) : p;
|
|
859
965
|
}
|
|
860
|
-
function printBanner() {
|
|
966
|
+
async function printBanner() {
|
|
861
967
|
const version = CLI_VERSION;
|
|
862
968
|
console.log("");
|
|
863
969
|
console.log(chalk.cyan.bold(" ██╗ ██╗███████╗██╗ ██╗██████╗"));
|
|
@@ -869,23 +975,33 @@ function printBanner() {
|
|
|
869
975
|
console.log("");
|
|
870
976
|
console.log(chalk.dim(" Context Engine for AI Coding Agents"));
|
|
871
977
|
console.log("");
|
|
872
|
-
// ── Daemon status line
|
|
978
|
+
// ── Daemon status line ──
|
|
979
|
+
//
|
|
980
|
+
// Source of truth is the socket (same signal as `daemon-cmd status`).
|
|
981
|
+
// Falling back to `.vexp/daemon.pid` + `kill(pid,0)` produces inconsistent
|
|
982
|
+
// UX after reboot: the file persists with a dead pid, `kill` fails, the
|
|
983
|
+
// banner says "not running", but the Rust daemon-cmd status probes the
|
|
984
|
+
// socket and reports "running". Using the socket here makes the banner
|
|
985
|
+
// agree with status.
|
|
873
986
|
const ws = findConfiguredWorkspace();
|
|
874
987
|
if (!ws) {
|
|
875
988
|
console.log(chalk.dim(" ○ Daemon: no workspace configured here"));
|
|
876
989
|
console.log(chalk.dim(" → type '1 > 3' to run full setup, or `vexp setup`"));
|
|
877
990
|
}
|
|
878
991
|
else {
|
|
879
|
-
const
|
|
880
|
-
|
|
881
|
-
|
|
992
|
+
const socketPath = path.join(ws, ".vexp", "daemon.sock");
|
|
993
|
+
const socketAlive = await isSocketAlive(socketPath);
|
|
994
|
+
if (socketAlive) {
|
|
995
|
+
// Display PID + uptime when we can read them; otherwise a plain "running".
|
|
996
|
+
const { pid } = isDaemonPidAlive(ws);
|
|
882
997
|
let uptime = "";
|
|
883
998
|
try {
|
|
884
999
|
const st = fs.statSync(path.join(ws, ".vexp", "daemon.pid"));
|
|
885
1000
|
uptime = `, uptime ${formatUptime((Date.now() - st.mtimeMs) / 1000)}`;
|
|
886
1001
|
}
|
|
887
1002
|
catch { /* non-fatal */ }
|
|
888
|
-
|
|
1003
|
+
const pidPart = pid ? `PID ${pid}${uptime}` : "socket alive";
|
|
1004
|
+
console.log(chalk.green(` ● Daemon: running (${pidPart}) — ${shortPath(ws)}`));
|
|
889
1005
|
}
|
|
890
1006
|
else {
|
|
891
1007
|
console.log(chalk.yellow(" ○ Daemon: not running"));
|
|
@@ -944,18 +1060,19 @@ function ask(rl, prompt) {
|
|
|
944
1060
|
}
|
|
945
1061
|
async function interactiveMode() {
|
|
946
1062
|
const readline = await import("readline");
|
|
947
|
-
//
|
|
948
|
-
//
|
|
949
|
-
//
|
|
1063
|
+
// Full bootstrap so users who upgraded the CLI without re-running
|
|
1064
|
+
// `vexp setup` still get a working state (daemon + MCP :7821 up, OS
|
|
1065
|
+
// autostart installed for the next reboot). All steps are idempotent so
|
|
1066
|
+
// running this alongside `vexp setup` never creates duplicates.
|
|
950
1067
|
const workspaceRoot = findConfiguredWorkspace();
|
|
951
1068
|
if (workspaceRoot) {
|
|
952
1069
|
try {
|
|
953
1070
|
const binaryPath = getBinaryPath();
|
|
954
|
-
await
|
|
1071
|
+
await ensureBootstrap(workspaceRoot, binaryPath);
|
|
955
1072
|
}
|
|
956
1073
|
catch { /* banner will reflect actual state */ }
|
|
957
1074
|
}
|
|
958
|
-
printBanner();
|
|
1075
|
+
await printBanner();
|
|
959
1076
|
printMainMenu();
|
|
960
1077
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
961
1078
|
rl.on("close", () => { console.log(""); process.exit(0); });
|
|
@@ -1006,34 +1123,39 @@ async function interactiveMode() {
|
|
|
1006
1123
|
const direct = allItems.find((m) => m.label === input);
|
|
1007
1124
|
if (direct) {
|
|
1008
1125
|
await executeCommand(direct.label, rl);
|
|
1126
|
+
// After a direct command, reprint the main menu so options stay visible.
|
|
1127
|
+
printMainMenu();
|
|
1009
1128
|
continue;
|
|
1010
1129
|
}
|
|
1011
1130
|
console.log(chalk.dim(" Choose 1-4 or type a command name. 'q' to exit.\n"));
|
|
1012
1131
|
continue;
|
|
1013
1132
|
}
|
|
1014
1133
|
// ── Sub-menus ──
|
|
1134
|
+
const menuMap = { config: MENU_CONFIG, explore: MENU_EXPLORE, savings: MENU_SAVINGS, license: MENU_LICENSE };
|
|
1135
|
+
const titles = { config: "Setup & Config", explore: "Explore & Analyze", savings: "Token Savings", license: "License" };
|
|
1015
1136
|
if (input === "b" || input === "back") {
|
|
1016
1137
|
currentMenu = "main";
|
|
1017
1138
|
printMainMenu();
|
|
1018
1139
|
continue;
|
|
1019
1140
|
}
|
|
1020
1141
|
if (input === "?" || input === "help" || input === "menu") {
|
|
1021
|
-
|
|
1022
|
-
const titles = { config: "Setup & Config", explore: "Explore & Analyze", savings: "Token Savings", license: "License" };
|
|
1023
|
-
printSubMenu(titles[currentMenu], menus[currentMenu]);
|
|
1142
|
+
printSubMenu(titles[currentMenu], menuMap[currentMenu]);
|
|
1024
1143
|
continue;
|
|
1025
1144
|
}
|
|
1026
|
-
const menuMap = { config: MENU_CONFIG, explore: MENU_EXPLORE, savings: MENU_SAVINGS, license: MENU_LICENSE };
|
|
1027
1145
|
const items = menuMap[currentMenu];
|
|
1028
1146
|
const item = items.find((m) => m.key === input || m.label === input);
|
|
1029
1147
|
if (!item) {
|
|
1030
1148
|
console.log(chalk.dim(` Unknown option. Type a number (1-${items.length}), 'b' to go back, or 'q' to exit.\n`));
|
|
1149
|
+
printSubMenu(titles[currentMenu], items);
|
|
1031
1150
|
continue;
|
|
1032
1151
|
}
|
|
1033
1152
|
// Show description before executing
|
|
1034
1153
|
console.log(chalk.dim(`\n ${item.description}\n`));
|
|
1035
1154
|
await executeCommand(item.label, rl);
|
|
1036
1155
|
console.log("");
|
|
1156
|
+
// Sticky sub-menu: reprint the current sub-menu after every command so
|
|
1157
|
+
// the user always sees the available options without typing '?'.
|
|
1158
|
+
printSubMenu(titles[currentMenu], items);
|
|
1037
1159
|
}
|
|
1038
1160
|
}
|
|
1039
1161
|
async function executeCommand(label, rl) {
|
|
@@ -1059,49 +1181,80 @@ async function executeCommand(label, rl) {
|
|
|
1059
1181
|
break;
|
|
1060
1182
|
}
|
|
1061
1183
|
case "daemon-cmd": {
|
|
1062
|
-
|
|
1063
|
-
const
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
console.log(chalk.
|
|
1184
|
+
// Sticky sub-sub-menu: stay here until user types 'b' (back).
|
|
1185
|
+
const binaryPath = ensureBinary();
|
|
1186
|
+
const valid = new Set(["start", "stop", "status", "logs"]);
|
|
1187
|
+
const printDaemonMenu = () => {
|
|
1188
|
+
console.log("");
|
|
1189
|
+
console.log(chalk.cyan(" ── Daemon ──"));
|
|
1190
|
+
console.log(` ${chalk.bold("start")} ${chalk.dim("Launch the daemon for this workspace")}`);
|
|
1191
|
+
console.log(` ${chalk.bold("stop")} ${chalk.dim("Stop the running daemon")}`);
|
|
1192
|
+
console.log(` ${chalk.bold("status")} ${chalk.dim("Show node/edge/file counts and uptime")}`);
|
|
1193
|
+
console.log(` ${chalk.bold("logs")} ${chalk.dim("Tail .vexp/vexp.log")}`);
|
|
1194
|
+
console.log(` ${chalk.dim("b")} ${chalk.dim("Back to Setup & Config")}`);
|
|
1195
|
+
console.log("");
|
|
1196
|
+
};
|
|
1197
|
+
printDaemonMenu();
|
|
1198
|
+
while (true) {
|
|
1199
|
+
const action = (await ask(rl, chalk.cyan(" daemon> "))).trim().toLowerCase();
|
|
1200
|
+
if (!action)
|
|
1201
|
+
continue;
|
|
1202
|
+
if (action === "b" || action === "back")
|
|
1203
|
+
break;
|
|
1204
|
+
if (valid.has(action)) {
|
|
1205
|
+
runBinary(binaryPath, ["daemon-cmd", action]);
|
|
1206
|
+
printDaemonMenu();
|
|
1207
|
+
continue;
|
|
1208
|
+
}
|
|
1209
|
+
console.log(chalk.dim(" Unknown action. Use start, stop, status, logs, or 'b' to go back."));
|
|
1070
1210
|
}
|
|
1071
1211
|
break;
|
|
1072
1212
|
}
|
|
1073
1213
|
case "setup-llm": {
|
|
1074
|
-
|
|
1075
|
-
console.log(chalk.cyan(" Local LLM compression (vexp-devmind)"));
|
|
1076
|
-
console.log(chalk.dim(" All processing runs locally — no data leaves your machine.\n"));
|
|
1077
|
-
console.log(` ${chalk.bold("1")} Install / Reinstall (~3.5 GB download)`);
|
|
1078
|
-
console.log(` ${chalk.bold("2")} Start (enable without re-downloading)`);
|
|
1079
|
-
console.log(` ${chalk.bold("3")} Check for updates (no download if up to date)`);
|
|
1080
|
-
console.log(` ${chalk.bold("4")} Status (show current config + hardware)`);
|
|
1081
|
-
console.log(` ${chalk.bold("5")} Disable (keep files, stop using LLM)`);
|
|
1082
|
-
console.log(` ${chalk.dim("b")} ${chalk.dim("Cancel")}`);
|
|
1083
|
-
console.log("");
|
|
1084
|
-
const llmChoice = await ask(rl, chalk.cyan(" Choose: "));
|
|
1214
|
+
// Sticky sub-sub-menu.
|
|
1085
1215
|
const llmBinary = ensureBinary();
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1216
|
+
const printLlmMenu = () => {
|
|
1217
|
+
console.log("");
|
|
1218
|
+
console.log(chalk.cyan(" ── Local LLM (vexp-devmind) ──"));
|
|
1219
|
+
console.log(chalk.dim(" All processing runs locally — no data leaves your machine."));
|
|
1220
|
+
console.log("");
|
|
1221
|
+
console.log(` ${chalk.bold("1")} Install / Reinstall ${chalk.dim("(~3.5 GB download)")}`);
|
|
1222
|
+
console.log(` ${chalk.bold("2")} Start ${chalk.dim("(enable without re-downloading)")}`);
|
|
1223
|
+
console.log(` ${chalk.bold("3")} Check for updates ${chalk.dim("(no download if up to date)")}`);
|
|
1224
|
+
console.log(` ${chalk.bold("4")} Status ${chalk.dim("(current config + hardware)")}`);
|
|
1225
|
+
console.log(` ${chalk.bold("5")} Disable ${chalk.dim("(keep files, stop using LLM)")}`);
|
|
1226
|
+
console.log(` ${chalk.dim("b")} ${chalk.dim("Back to Setup & Config")}`);
|
|
1227
|
+
console.log("");
|
|
1228
|
+
};
|
|
1229
|
+
printLlmMenu();
|
|
1230
|
+
while (true) {
|
|
1231
|
+
const c = (await ask(rl, chalk.cyan(" llm> "))).trim().toLowerCase();
|
|
1232
|
+
if (!c)
|
|
1233
|
+
continue;
|
|
1234
|
+
if (c === "b" || c === "back")
|
|
1102
1235
|
break;
|
|
1103
|
-
|
|
1104
|
-
|
|
1236
|
+
switch (c) {
|
|
1237
|
+
case "1":
|
|
1238
|
+
console.log(chalk.dim(" Starting LLM setup (this may take several minutes)…\n"));
|
|
1239
|
+
runBinary(llmBinary, ["setup-llm", "--install"]);
|
|
1240
|
+
break;
|
|
1241
|
+
case "2":
|
|
1242
|
+
runBinary(llmBinary, ["setup-llm", "--enable"]);
|
|
1243
|
+
break;
|
|
1244
|
+
case "3":
|
|
1245
|
+
runBinary(llmBinary, ["setup-llm", "--check-updates"]);
|
|
1246
|
+
break;
|
|
1247
|
+
case "4":
|
|
1248
|
+
runBinary(llmBinary, ["setup-llm", "--status"]);
|
|
1249
|
+
break;
|
|
1250
|
+
case "5":
|
|
1251
|
+
runBinary(llmBinary, ["setup-llm", "--disable"]);
|
|
1252
|
+
break;
|
|
1253
|
+
default:
|
|
1254
|
+
console.log(chalk.dim(" Unknown option. Use 1-5 or 'b' to go back."));
|
|
1255
|
+
continue;
|
|
1256
|
+
}
|
|
1257
|
+
printLlmMenu();
|
|
1105
1258
|
}
|
|
1106
1259
|
break;
|
|
1107
1260
|
}
|