vexp-cli 2.0.4 → 2.0.6
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 +46 -23
- package/dist/binary.js +116 -1
- package/dist/cli.js +254 -56
- package/mcp/mcp-server.cjs +127 -7
- package/package.json +6 -6
package/dist/cli.js
CHANGED
|
@@ -5,8 +5,9 @@ import ora from "ora";
|
|
|
5
5
|
import { spawn, spawnSync, execFileSync } from "child_process";
|
|
6
6
|
import * as path from "path";
|
|
7
7
|
import * as fs from "fs";
|
|
8
|
+
import * as net from "net";
|
|
8
9
|
import { checkbox } from "@inquirer/prompts";
|
|
9
|
-
import { getBinaryPath, getInstalledVersion, getMcpServerPath } from "./binary.js";
|
|
10
|
+
import { getBinaryPath, getInstalledVersion, getMcpServerPath, binaryEnv } from "./binary.js";
|
|
10
11
|
import { detectAgents, getAgentList, configureSelectedAgents } from "./agent-config.js";
|
|
11
12
|
import { CLI_VERSION } from "./version.js";
|
|
12
13
|
import { activateLicense, deactivateLicense, readLicenseLimits, readDeviceBlocked, } from "./license.js";
|
|
@@ -17,6 +18,34 @@ program
|
|
|
17
18
|
.description("Vexp — Graph-RAG context engine for AI coding agents")
|
|
18
19
|
.version(CLI_VERSION);
|
|
19
20
|
// ────────────────────────────────────────────────────
|
|
21
|
+
// Auto-start daemon before any action that needs it
|
|
22
|
+
// ────────────────────────────────────────────────────
|
|
23
|
+
//
|
|
24
|
+
// Commands that either manage the daemon themselves (setup, daemon-cmd)
|
|
25
|
+
// or don't need it (license, version, activate, deactivate) are skipped.
|
|
26
|
+
// Everything else triggers a lightweight probe + silent spawn if the
|
|
27
|
+
// workspace was already configured (marker: .vexp/manifest.json).
|
|
28
|
+
const AUTOSTART_SKIP = new Set([
|
|
29
|
+
"setup", "daemon-cmd", "activate", "deactivate", "license", "version",
|
|
30
|
+
]);
|
|
31
|
+
program.hook("preAction", async (_thisCmd, actionCmd) => {
|
|
32
|
+
if (process.env.VEXP_NO_AUTOSTART === "1")
|
|
33
|
+
return;
|
|
34
|
+
if (AUTOSTART_SKIP.has(actionCmd.name()))
|
|
35
|
+
return;
|
|
36
|
+
const workspaceRoot = findConfiguredWorkspace();
|
|
37
|
+
if (!workspaceRoot)
|
|
38
|
+
return;
|
|
39
|
+
let binaryPath;
|
|
40
|
+
try {
|
|
41
|
+
binaryPath = getBinaryPath();
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
await ensureDaemonRunning(workspaceRoot, binaryPath);
|
|
47
|
+
});
|
|
48
|
+
// ────────────────────────────────────────────────────
|
|
20
49
|
// Resolve the platform binary (no network — bundled via npm)
|
|
21
50
|
// ────────────────────────────────────────────────────
|
|
22
51
|
function ensureBinary() {
|
|
@@ -34,7 +63,7 @@ let replMode = false;
|
|
|
34
63
|
function runBinary(binaryPath, args) {
|
|
35
64
|
if (replMode) {
|
|
36
65
|
// In REPL mode, run synchronously and don't exit the process
|
|
37
|
-
const result = spawnSync(binaryPath, args, { stdio: "inherit" });
|
|
66
|
+
const result = spawnSync(binaryPath, args, { stdio: "inherit", env: binaryEnv(binaryPath) });
|
|
38
67
|
if (result.error) {
|
|
39
68
|
console.error(chalk.red(`Error: ${result.error.message}`));
|
|
40
69
|
}
|
|
@@ -42,6 +71,7 @@ function runBinary(binaryPath, args) {
|
|
|
42
71
|
}
|
|
43
72
|
const child = spawn(binaryPath, args, {
|
|
44
73
|
stdio: "inherit",
|
|
74
|
+
env: binaryEnv(binaryPath),
|
|
45
75
|
});
|
|
46
76
|
child.on("exit", (code) => {
|
|
47
77
|
process.exit(code ?? 0);
|
|
@@ -56,6 +86,8 @@ function runBinaryAsync(binaryPath, args, options) {
|
|
|
56
86
|
return new Promise((resolve, reject) => {
|
|
57
87
|
const child = spawn(binaryPath, args, {
|
|
58
88
|
stdio: options?.silent ? "pipe" : "inherit",
|
|
89
|
+
cwd: options?.cwd,
|
|
90
|
+
env: binaryEnv(binaryPath),
|
|
59
91
|
});
|
|
60
92
|
child.on("exit", (code) => {
|
|
61
93
|
if (code === 0)
|
|
@@ -67,6 +99,160 @@ function runBinaryAsync(binaryPath, args, options) {
|
|
|
67
99
|
});
|
|
68
100
|
}
|
|
69
101
|
// ────────────────────────────────────────────────────
|
|
102
|
+
// Background services (daemon + MCP HTTP bridge)
|
|
103
|
+
// ────────────────────────────────────────────────────
|
|
104
|
+
/**
|
|
105
|
+
* Walk up from cwd looking for `.vexp/manifest.json` — the marker that
|
|
106
|
+
* `vexp setup` has been run on this tree. Returns null if none found.
|
|
107
|
+
*/
|
|
108
|
+
function findConfiguredWorkspace(startDir = process.cwd()) {
|
|
109
|
+
let current = path.resolve(startDir);
|
|
110
|
+
while (true) {
|
|
111
|
+
if (fs.existsSync(path.join(current, ".vexp", "manifest.json")))
|
|
112
|
+
return current;
|
|
113
|
+
const parent = path.dirname(current);
|
|
114
|
+
if (parent === current)
|
|
115
|
+
return null;
|
|
116
|
+
current = parent;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/** Quick TCP probe — true if something is already listening on 127.0.0.1:port. */
|
|
120
|
+
function isPortInUse(port) {
|
|
121
|
+
return new Promise((resolve) => {
|
|
122
|
+
const sock = net.createConnection({ port, host: "127.0.0.1" });
|
|
123
|
+
const timer = setTimeout(() => { sock.destroy(); resolve(false); }, 300);
|
|
124
|
+
sock.on("connect", () => { clearTimeout(timer); sock.destroy(); resolve(true); });
|
|
125
|
+
sock.on("error", () => { clearTimeout(timer); resolve(false); });
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
/** True if the Unix socket at `socketPath` accepts a connection within `timeoutMs`. */
|
|
129
|
+
function isSocketAlive(socketPath, timeoutMs = 300) {
|
|
130
|
+
return new Promise((resolve) => {
|
|
131
|
+
if (!fs.existsSync(socketPath)) {
|
|
132
|
+
resolve(false);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const sock = net.createConnection({ path: socketPath });
|
|
136
|
+
const timer = setTimeout(() => { sock.destroy(); resolve(false); }, timeoutMs);
|
|
137
|
+
sock.on("connect", () => { clearTimeout(timer); sock.destroy(); resolve(true); });
|
|
138
|
+
sock.on("error", () => { clearTimeout(timer); resolve(false); });
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
/** Check if the PID recorded in .vexp/daemon.pid is a live process. */
|
|
142
|
+
function isDaemonPidAlive(workspaceRoot) {
|
|
143
|
+
const pidFile = path.join(workspaceRoot, ".vexp", "daemon.pid");
|
|
144
|
+
try {
|
|
145
|
+
const pid = parseInt(fs.readFileSync(pidFile, "utf-8").trim(), 10);
|
|
146
|
+
if (!pid || isNaN(pid))
|
|
147
|
+
return { alive: false };
|
|
148
|
+
try {
|
|
149
|
+
process.kill(pid, 0); // signal 0 = existence check, no actual signal
|
|
150
|
+
return { alive: true, pid };
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return { alive: false };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return { alive: false };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Ensure the vexp-core daemon is running for the given workspace. Fast when
|
|
162
|
+
* it's already up (single socket probe). Idempotent lato Rust (PID file
|
|
163
|
+
* check + `kill -0`), so re-invoking when up is a no-op.
|
|
164
|
+
*
|
|
165
|
+
* Silent on success. On failure prints a one-liner pointing at the log.
|
|
166
|
+
* Honors VEXP_NO_AUTOSTART=1 as opt-out.
|
|
167
|
+
*/
|
|
168
|
+
async function ensureDaemonRunning(workspaceRoot, binaryPath) {
|
|
169
|
+
if (process.env.VEXP_NO_AUTOSTART === "1")
|
|
170
|
+
return;
|
|
171
|
+
const socketPath = path.join(workspaceRoot, ".vexp", "daemon.sock");
|
|
172
|
+
if (await isSocketAlive(socketPath))
|
|
173
|
+
return;
|
|
174
|
+
try {
|
|
175
|
+
await runBinaryAsync(binaryPath, ["daemon-cmd", "start"], {
|
|
176
|
+
silent: true,
|
|
177
|
+
cwd: workspaceRoot,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
const logPath = path.join(workspaceRoot, ".vexp", "daemon.log");
|
|
182
|
+
console.error(chalk.yellow(` ⚠ Could not auto-start vexp daemon — see ${logPath}`));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Start the vexp daemon and MCP HTTP server in the background.
|
|
187
|
+
* Used by both the non-interactive `setup` command and the REPL setup flow.
|
|
188
|
+
* Both services detach via setsid (Rust) / Node detached+unref, so they
|
|
189
|
+
* survive terminal close and VSCode restart.
|
|
190
|
+
*/
|
|
191
|
+
async function startBackgroundServices(binaryPath, workspaceRoot) {
|
|
192
|
+
// Start daemon
|
|
193
|
+
const spinnerDaemon = ora("Starting vexp daemon...").start();
|
|
194
|
+
try {
|
|
195
|
+
await runBinaryAsync(binaryPath, ["daemon-cmd", "start"], { silent: true, cwd: workspaceRoot });
|
|
196
|
+
const socketPath = path.join(workspaceRoot, ".vexp", "daemon.sock");
|
|
197
|
+
if (fs.existsSync(socketPath)) {
|
|
198
|
+
spinnerDaemon.succeed("vexp daemon started");
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
const logPath = path.join(workspaceRoot, ".vexp", "daemon.log");
|
|
202
|
+
spinnerDaemon.warn(`Daemon socket not ready — check ${logPath}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
spinnerDaemon.warn("Daemon already running or failed to start (non-fatal)");
|
|
207
|
+
}
|
|
208
|
+
// Start MCP HTTP server (needed for Codex and HTTP-based agents).
|
|
209
|
+
// Detect collisions: VS Code plugin may have already bound :7821.
|
|
210
|
+
const mcpPath = getMcpServerPath();
|
|
211
|
+
if (mcpPath) {
|
|
212
|
+
const spinnerMcp = ora("Starting MCP HTTP server...").start();
|
|
213
|
+
if (await isPortInUse(7821)) {
|
|
214
|
+
spinnerMcp.succeed("MCP HTTP server already running on port 7821");
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
try {
|
|
218
|
+
const { randomUUID } = await import("crypto");
|
|
219
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
220
|
+
let mcpToken = "";
|
|
221
|
+
const tokenPath = home ? path.join(home, ".vexp", "mcp.token") : "";
|
|
222
|
+
if (tokenPath) {
|
|
223
|
+
try {
|
|
224
|
+
mcpToken = fs.readFileSync(tokenPath, "utf-8").trim();
|
|
225
|
+
}
|
|
226
|
+
catch { }
|
|
227
|
+
}
|
|
228
|
+
if (!mcpToken) {
|
|
229
|
+
mcpToken = randomUUID();
|
|
230
|
+
if (tokenPath) {
|
|
231
|
+
fs.mkdirSync(path.dirname(tokenPath), { recursive: true });
|
|
232
|
+
fs.writeFileSync(tokenPath, mcpToken, { mode: 0o600 });
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
const mcpChild = spawn("node", [mcpPath, "--http", "--port=7821"], {
|
|
236
|
+
detached: true,
|
|
237
|
+
stdio: "ignore",
|
|
238
|
+
env: {
|
|
239
|
+
...process.env,
|
|
240
|
+
VEXP_WORKSPACE: workspaceRoot,
|
|
241
|
+
VEXP_HTTP: "1",
|
|
242
|
+
VEXP_PORT: "7821",
|
|
243
|
+
VEXP_MCP_TOKEN: mcpToken,
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
mcpChild.unref();
|
|
247
|
+
spinnerMcp.succeed("MCP HTTP server started (port 7821)");
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
spinnerMcp.warn("MCP HTTP server failed to start (non-fatal)");
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// ────────────────────────────────────────────────────
|
|
70
256
|
// Commands
|
|
71
257
|
// ────────────────────────────────────────────────────
|
|
72
258
|
program
|
|
@@ -185,7 +371,7 @@ program
|
|
|
185
371
|
});
|
|
186
372
|
program
|
|
187
373
|
.command("compress <file>")
|
|
188
|
-
.description("Compress a markdown/text file using
|
|
374
|
+
.description("Compress a markdown/text file using prose compression (backup to <name>.original.md)")
|
|
189
375
|
.option("--level <lvl>", "Compression level: off | lite | full | ultra", "full")
|
|
190
376
|
.action(async (file, opts) => {
|
|
191
377
|
const binaryPath = ensureBinary();
|
|
@@ -307,55 +493,8 @@ program
|
|
|
307
493
|
else {
|
|
308
494
|
console.log(chalk.dim(" Skipping indexing (--no-index)"));
|
|
309
495
|
}
|
|
310
|
-
// Step 2.5: Start daemon
|
|
311
|
-
|
|
312
|
-
try {
|
|
313
|
-
await runBinaryAsync(binaryPath, ["daemon-cmd", "start"], { silent: true });
|
|
314
|
-
spinnerDaemon.succeed("vexp daemon started");
|
|
315
|
-
}
|
|
316
|
-
catch {
|
|
317
|
-
spinnerDaemon.warn("Daemon already running or failed to start (non-fatal)");
|
|
318
|
-
}
|
|
319
|
-
// Step 2.6: Start MCP HTTP server (needed for Codex and HTTP-based agents)
|
|
320
|
-
const mcpPath = getMcpServerPath();
|
|
321
|
-
if (mcpPath) {
|
|
322
|
-
const spinnerMcp = ora("Starting MCP HTTP server...").start();
|
|
323
|
-
try {
|
|
324
|
-
const { randomUUID } = await import("crypto");
|
|
325
|
-
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
326
|
-
let mcpToken = "";
|
|
327
|
-
const tokenPath = home ? path.join(home, ".vexp", "mcp.token") : "";
|
|
328
|
-
if (tokenPath) {
|
|
329
|
-
try {
|
|
330
|
-
mcpToken = fs.readFileSync(tokenPath, "utf-8").trim();
|
|
331
|
-
}
|
|
332
|
-
catch { }
|
|
333
|
-
}
|
|
334
|
-
if (!mcpToken) {
|
|
335
|
-
mcpToken = randomUUID();
|
|
336
|
-
if (tokenPath) {
|
|
337
|
-
fs.mkdirSync(path.dirname(tokenPath), { recursive: true });
|
|
338
|
-
fs.writeFileSync(tokenPath, mcpToken, { mode: 0o600 });
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
const mcpChild = spawn("node", [mcpPath, "--http", "--port=7821"], {
|
|
342
|
-
detached: true,
|
|
343
|
-
stdio: "ignore",
|
|
344
|
-
env: {
|
|
345
|
-
...process.env,
|
|
346
|
-
VEXP_WORKSPACE: workspaceRoot,
|
|
347
|
-
VEXP_HTTP: "1",
|
|
348
|
-
VEXP_PORT: "7821",
|
|
349
|
-
VEXP_MCP_TOKEN: mcpToken,
|
|
350
|
-
},
|
|
351
|
-
});
|
|
352
|
-
mcpChild.unref();
|
|
353
|
-
spinnerMcp.succeed("MCP HTTP server started (port 7821)");
|
|
354
|
-
}
|
|
355
|
-
catch {
|
|
356
|
-
spinnerMcp.warn("MCP HTTP server failed to start (non-fatal)");
|
|
357
|
-
}
|
|
358
|
-
}
|
|
496
|
+
// Step 2.5: Start daemon + MCP HTTP server (survive terminal close)
|
|
497
|
+
await startBackgroundServices(binaryPath, workspaceRoot);
|
|
359
498
|
// In personal mode, make .vexp/ fully gitignored
|
|
360
499
|
if (isPersonal) {
|
|
361
500
|
const vexpGitignore = path.join(workspaceRoot, ".vexp", ".gitignore");
|
|
@@ -530,6 +669,8 @@ async function runSetupInteractive(rl) {
|
|
|
530
669
|
// Step 1: Index
|
|
531
670
|
console.log(chalk.dim(" Indexing codebase...\n"));
|
|
532
671
|
runBinary(binaryPath, ["init", workspaceRoot]);
|
|
672
|
+
// Step 1.5: Start daemon + MCP HTTP server (parity with non-interactive setup)
|
|
673
|
+
await startBackgroundServices(binaryPath, workspaceRoot);
|
|
533
674
|
// Step 2: Select agents
|
|
534
675
|
const allAgents = getAgentList();
|
|
535
676
|
const detected = detectAgents(workspaceRoot);
|
|
@@ -671,6 +812,21 @@ const MENU_LICENSE = [
|
|
|
671
812
|
{ key: "2", label: "activate", hint: "Activate a Pro/Team license key", description: "Enter a license key to unlock Pro or Team features.\n Get your key at https://vexp.dev/#pricing" },
|
|
672
813
|
{ key: "3", label: "deactivate", hint: "Remove current license", description: "Removes the active license. Free plan limits will apply." },
|
|
673
814
|
];
|
|
815
|
+
/** Format a duration as a compact uptime string (e.g. "2h 14m", "47s"). */
|
|
816
|
+
function formatUptime(seconds) {
|
|
817
|
+
if (seconds < 60)
|
|
818
|
+
return `${Math.floor(seconds)}s`;
|
|
819
|
+
if (seconds < 3600)
|
|
820
|
+
return `${Math.floor(seconds / 60)}m`;
|
|
821
|
+
if (seconds < 86400)
|
|
822
|
+
return `${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}m`;
|
|
823
|
+
return `${Math.floor(seconds / 86400)}d ${Math.floor((seconds % 86400) / 3600)}h`;
|
|
824
|
+
}
|
|
825
|
+
/** Format a workspace path for display: shorten $HOME → ~. */
|
|
826
|
+
function shortPath(p) {
|
|
827
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
828
|
+
return home && p.startsWith(home) ? "~" + p.slice(home.length) : p;
|
|
829
|
+
}
|
|
674
830
|
function printBanner() {
|
|
675
831
|
const version = CLI_VERSION;
|
|
676
832
|
console.log("");
|
|
@@ -682,21 +838,52 @@ function printBanner() {
|
|
|
682
838
|
console.log(chalk.dim(" ╚═══╝ ╚══════╝╚═╝ ╚═╝╚═╝") + chalk.dim(` v${version}`));
|
|
683
839
|
console.log("");
|
|
684
840
|
console.log(chalk.dim(" Context Engine for AI Coding Agents"));
|
|
685
|
-
|
|
841
|
+
console.log("");
|
|
842
|
+
// ── Daemon status line (3 states, symmetric to LLM) ──
|
|
843
|
+
const ws = findConfiguredWorkspace();
|
|
844
|
+
if (!ws) {
|
|
845
|
+
console.log(chalk.dim(" ○ Daemon: no workspace configured here"));
|
|
846
|
+
console.log(chalk.dim(" → type '1 > 3' to run full setup, or `vexp setup`"));
|
|
847
|
+
}
|
|
848
|
+
else {
|
|
849
|
+
const { alive, pid } = isDaemonPidAlive(ws);
|
|
850
|
+
if (alive && pid) {
|
|
851
|
+
// Derive uptime from daemon.pid mtime (portable, no /proc parsing).
|
|
852
|
+
let uptime = "";
|
|
853
|
+
try {
|
|
854
|
+
const st = fs.statSync(path.join(ws, ".vexp", "daemon.pid"));
|
|
855
|
+
uptime = `, uptime ${formatUptime((Date.now() - st.mtimeMs) / 1000)}`;
|
|
856
|
+
}
|
|
857
|
+
catch { /* non-fatal */ }
|
|
858
|
+
console.log(chalk.green(` ● Daemon: running (PID ${pid}${uptime}) — ${shortPath(ws)}`));
|
|
859
|
+
}
|
|
860
|
+
else {
|
|
861
|
+
console.log(chalk.yellow(" ○ Daemon: not running"));
|
|
862
|
+
console.log(chalk.dim(" → type '1 > 5 > start' to launch it, or `vexp daemon-cmd start`"));
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
// ── LLM status line (3 states: active / installed-not-active / not installed) ──
|
|
686
866
|
try {
|
|
687
867
|
const llmBin = ensureBinary();
|
|
688
868
|
const stdout = execFileSync(llmBin, ["setup-llm", "--status"], {
|
|
689
869
|
timeout: 5000,
|
|
690
870
|
encoding: "utf-8",
|
|
691
871
|
stdio: ["pipe", "pipe", "pipe"],
|
|
872
|
+
env: binaryEnv(llmBin),
|
|
692
873
|
});
|
|
693
|
-
const operational = /Operational:\s*true
|
|
874
|
+
const operational = /Operational:\s*true/i.test(stdout);
|
|
875
|
+
const installed = /Installed:\s*true/i.test(stdout);
|
|
694
876
|
const model = stdout.match(/Model:\s*(.+)/)?.[1]?.trim() ?? "";
|
|
695
877
|
if (operational) {
|
|
696
|
-
console.log(chalk.green(` ● LLM: ${model} (active)`));
|
|
878
|
+
console.log(chalk.green(` ● LLM: ${model || "local"} (active)`));
|
|
879
|
+
}
|
|
880
|
+
else if (installed) {
|
|
881
|
+
console.log(chalk.yellow(" ○ LLM: installed but not active"));
|
|
882
|
+
console.log(chalk.dim(" → type '1 > 6 > start' to start it, or `vexp setup-llm --start`"));
|
|
697
883
|
}
|
|
698
884
|
else {
|
|
699
|
-
console.log(chalk.dim(" ○ LLM: not
|
|
885
|
+
console.log(chalk.dim(" ○ LLM: not installed"));
|
|
886
|
+
console.log(chalk.dim(" → type '1 > 6 > install' to download (~3.5 GB), or `vexp setup-llm --install`"));
|
|
700
887
|
}
|
|
701
888
|
}
|
|
702
889
|
catch {
|
|
@@ -727,6 +914,17 @@ function ask(rl, prompt) {
|
|
|
727
914
|
}
|
|
728
915
|
async function interactiveMode() {
|
|
729
916
|
const readline = await import("readline");
|
|
917
|
+
// Auto-start the daemon (if workspace configured) before printing the
|
|
918
|
+
// banner, so the banner's Daemon line reflects the correct final state.
|
|
919
|
+
// Silent when already running or no workspace configured.
|
|
920
|
+
const workspaceRoot = findConfiguredWorkspace();
|
|
921
|
+
if (workspaceRoot) {
|
|
922
|
+
try {
|
|
923
|
+
const binaryPath = getBinaryPath();
|
|
924
|
+
await ensureDaemonRunning(workspaceRoot, binaryPath);
|
|
925
|
+
}
|
|
926
|
+
catch { /* banner will reflect actual state */ }
|
|
927
|
+
}
|
|
730
928
|
printBanner();
|
|
731
929
|
printMainMenu();
|
|
732
930
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|