vexp-cli 3.3.4 → 3.3.7

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 CHANGED
@@ -60,6 +60,17 @@ vexp shield scan
60
60
  vexp doctor
61
61
  ```
62
62
 
63
+ ## Daemons and memory
64
+
65
+ vexp runs one small daemon per project, started by your agent's first call. By default it runs until you stop it (`vexp stop`, or `vexp stop --all`). To have the daemons nobody is using stop on their own, add this to `~/.vexp/config.toml`:
66
+
67
+ ```toml
68
+ [daemon]
69
+ idle_timeout_minutes = 30
70
+ ```
71
+
72
+ The next tool call or prompt starts the daemon again. `VEXP_IDLE_TIMEOUT_MINUTES` sets it for one daemon. A daemon the VS Code extension started for a window stops when that window closes. `vexp daemons` lists what is running, with each daemon's memory, and `vexp doctor` shows whether the setting is on.
73
+
63
74
  ## Supported agents
64
75
 
65
76
  Claude Code · Cursor · Windsurf · GitHub Copilot · OpenAI Codex · Zed · Continue.dev · Cline · Aider · Kiro · Kilo Code · opencode · Augment · Antigravity · ZCode · Friday Code
package/dist/cli.js CHANGED
@@ -33,6 +33,23 @@ program
33
33
  .description("Vexp — Graph-RAG context engine for AI coding agents")
34
34
  .version(CLI_VERSION)
35
35
  .option("-i, --trace", "Verbose trace mode: print every step with timing (IPs/tokens redacted)");
36
+ // The idle stop was only to be found by reading the binary (field report,
37
+ // 2026-10-17). Said under the root help and under the commands that list
38
+ // and stop daemons.
39
+ const IDLE_STOP_HELP = `
40
+ Daemons:
41
+ vexp runs one daemon per project. It runs until it is stopped (vexp stop, vexp stop --all),
42
+ or, with this setting, until it has gone unused that many minutes; the next tool call or
43
+ prompt starts it again:
44
+
45
+ # ~/.vexp/config.toml
46
+ [daemon]
47
+ idle_timeout_minutes = 30
48
+
49
+ VEXP_IDLE_TIMEOUT_MINUTES sets it for one daemon. A daemon an editor window started stops
50
+ with that window instead. vexp daemons lists them; vexp doctor says whether the setting is on.
51
+ `;
52
+ program.addHelpText("after", IDLE_STOP_HELP);
36
53
  // Propagate -i / --trace to spawned children via env var so subprocess
37
54
  // (daemon, MCP, serve) inherits the trace flag.
38
55
  if (isTraceEnabled()) {
@@ -599,6 +616,7 @@ program
599
616
  .command("daemons")
600
617
  .description("List every vexp daemon on this machine: which workspace holds each concurrent-workspace slot, version, process id, memory, index size, uptime")
601
618
  .option("--json", "Emit machine-readable JSON")
619
+ .addHelpText("after", IDLE_STOP_HELP)
602
620
  .action(async (opts) => {
603
621
  const binaryPath = ensureBinary();
604
622
  const args = ["daemons"];
@@ -673,6 +691,7 @@ program
673
691
  .command("stop [workspace]")
674
692
  .description("Stop the vexp daemon of a workspace (default: current directory), or with --all every daemon on this machine. Accepts any path shown by the concurrent-workspace cap message")
675
693
  .option("--all", "Stop every live vexp daemon on this machine, one after the other")
694
+ .addHelpText("after", IDLE_STOP_HELP)
676
695
  .action(async (workspace, opts) => {
677
696
  if (opts.all && workspace !== undefined) {
678
697
  console.error(`--all stops every daemon on this machine and takes no workspace: run 'vexp stop --all', or 'vexp stop "${workspace}"' for that one.`);
@@ -832,6 +851,7 @@ program
832
851
  .command("daemon-cmd <action>")
833
852
  .description("Manage the vexp daemon (start, stop, restart, status, logs)")
834
853
  .option("--follow", "Follow log output (with 'logs' action)")
854
+ .addHelpText("after", IDLE_STOP_HELP)
835
855
  .action(async (action, opts) => {
836
856
  const binaryPath = ensureBinary();
837
857
  const args = ["daemon-cmd", action];
package/dist/doctor.js CHANGED
@@ -605,6 +605,42 @@ function withoutVexpCommands(line) {
605
605
  .filter((sentence) => !/[`']vexp /.test(sentence))
606
606
  .join(" ");
607
607
  }
608
+ /** ~/.vexp/config.toml, under VEXP_HOME when that is set: where the engine reads it. */
609
+ function configTomlPath() {
610
+ return path.join(vexpHome(), ".vexp", "config.toml");
611
+ }
612
+ /** Minutes as the setting gives them: whole, else seconds. */
613
+ function spokenMinutes(minutes) {
614
+ return Number.isInteger(minutes) ? `${minutes} min` : `${Math.round(minutes * 60)} s`;
615
+ }
616
+ /**
617
+ * The idle stop, as the daemon that answered sees it (index_status
618
+ * `idle_stop`, 3.3.6 and later). The setting was only to be found by reading
619
+ * the binary (field report, 2026-10-17): doctor names it, on, off, or passed
620
+ * over for a window's daemon. Null for a daemon from before it.
621
+ */
622
+ export function idleStopFinding(idle) {
623
+ if (!idle || typeof idle !== "object")
624
+ return null;
625
+ const st = idle;
626
+ const after = typeof st.after_minutes === "number" && st.after_minutes > 0 ? st.after_minutes : null;
627
+ if (after === null) {
628
+ return {
629
+ level: INFO,
630
+ message: `idle stop: off, so this daemon runs until it is stopped. To stop the daemons nobody uses, set [daemon] idle_timeout_minutes = 30 in ${configTomlPath()} ` +
631
+ `(or VEXP_IDLE_TIMEOUT_MINUTES for one daemon); the next tool call or prompt starts one again. A daemon an editor window started stops with that window instead.`,
632
+ };
633
+ }
634
+ if (st.editor_owned === true) {
635
+ return {
636
+ level: OK,
637
+ message: `idle stop: after ${spokenMinutes(after)} without use, but not this daemon: an editor window started it, and it stops when that window closes`,
638
+ };
639
+ }
640
+ const idleS = typeof st.idle_s === "number" ? st.idle_s : null;
641
+ const unused = idleS === null ? "" : idleS >= 60 ? ` (unused for ${Math.floor(idleS / 60)} min now)` : " (used in the last minute)";
642
+ return { level: OK, message: `idle stop: after ${spokenMinutes(after)} without use${unused}; the next tool call or prompt starts it again` };
643
+ }
608
644
  /**
609
645
  * What doctor says when no daemon answers for this workspace.
610
646
  *
@@ -654,6 +690,18 @@ export function daemonDownFindings(o) {
654
690
  },
655
691
  ];
656
692
  }
693
+ // Stopped on purpose: idle_timeout_minutes. Nothing to restart by hand,
694
+ // and the marker goes as soon as a daemon starts here again.
695
+ const idle = (o.idleExit ?? "").split(/\r?\n/)[0].trim();
696
+ if (idle) {
697
+ return [
698
+ {
699
+ level: INFO,
700
+ message: `no daemon is running for this project (${o.sock}): it ${idle}.\n` +
701
+ ` nothing to do: the next tool call or prompt starts it again. The setting is [daemon] idle_timeout_minutes in ${configTomlPath()}.`,
702
+ },
703
+ ];
704
+ }
657
705
  if (ext && !o.hasIndex) {
658
706
  return [
659
707
  {
@@ -1009,7 +1057,11 @@ export function partialParseVerdict(cov, coveragePath = ".vexp/coverage.json") {
1009
1057
  const files = list;
1010
1058
  const examples = files
1011
1059
  .slice(0, 5)
1012
- .map((f) => `${f.path} (${Number(f.lines) || 0} line(s) from line ${Number(f.first_line) || 0})`);
1060
+ .map((f) => {
1061
+ const broke = Number(f.broke_line) || 0;
1062
+ const at = broke > 0 ? `, the reading broke at line ${broke}` : "";
1063
+ return `${f.path} (${Number(f.lines) || 0} line(s) from line ${Number(f.first_line) || 0}${at})`;
1064
+ });
1013
1065
  const more = files.length - examples.length;
1014
1066
  return {
1015
1067
  level: WARN,
@@ -3257,6 +3309,11 @@ async function doctorChecks(onWorkspace) {
3257
3309
  startBlocked = fs.readFileSync(path.join(ws.root, ".vexp", "start-blocked"), "utf-8");
3258
3310
  }
3259
3311
  catch { /* the last start was not refused */ }
3312
+ let idleExit;
3313
+ try {
3314
+ idleExit = fs.readFileSync(path.join(ws.root, ".vexp", "idle-exit"), "utf-8");
3315
+ }
3316
+ catch { /* the last daemon did not stop for idleness */ }
3260
3317
  const what = notAProject(ws.root);
3261
3318
  const down = daemonDownFindings({
3262
3319
  sock,
@@ -3264,6 +3321,7 @@ async function doctorChecks(onWorkspace) {
3264
3321
  hasIndex: fs.existsSync(path.join(ws.root, ".vexp", "index.db")),
3265
3322
  startBlocked,
3266
3323
  outside: what ? { root: ws.root, what } : undefined,
3324
+ idleExit,
3267
3325
  });
3268
3326
  closedInIde = down.every((f) => f.level === INFO);
3269
3327
  for (const f of down)
@@ -3303,6 +3361,9 @@ async function doctorChecks(onWorkspace) {
3303
3361
  ? "loading (the model is still starting — this is not the rule compressor settling in)"
3304
3362
  : st.compressor ?? "unknown (older daemon)";
3305
3363
  line(OK, `index: ${st.total_files ?? "?"} files · ${st.total_nodes ?? "?"} nodes · state ${st.status ?? "?"} · compressor ${comp}`);
3364
+ const idleStop = idleStopFinding(st.idle_stop);
3365
+ if (idleStop)
3366
+ emit(idleStop);
3306
3367
  if (st.llm_configured_but_inactive === true) {
3307
3368
  // The daemon knows WHY, and doctor was throwing it away: it printed
3308
3369
  // the same "restart to load the model" the sidebar prints, to a user