vexp-cli 3.3.8 → 3.3.9
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 +19 -2
- package/dist/cli.js +2 -1
- package/dist/daemon-upgrade.js +19 -6
- package/dist/doctor.js +1 -1
- package/dist/forget.js +1 -0
- package/dist/serve.js +47 -1
- package/mcp/mcp-server.cjs +46 -46
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -69,7 +69,7 @@ vexp runs one small daemon per project, started by your agent's first call. By d
|
|
|
69
69
|
idle_timeout_minutes = 30
|
|
70
70
|
```
|
|
71
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.
|
|
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. Unset, a daemon an agent's session started (any agent) stops after 30 minutes without use and frees its slot of the plan's daemon cap; `idle_timeout_minutes = 0` turns that off, and `VEXP_NO_AUTOSTART=1` keeps sessions from starting one. `vexp daemons` lists what is running, with each daemon's memory, and `vexp doctor` shows whether the setting is on.
|
|
73
73
|
|
|
74
74
|
## Supported agents
|
|
75
75
|
|
package/dist/agent-config.js
CHANGED
|
@@ -2452,6 +2452,12 @@ function isUserManagedCodexSection(section) {
|
|
|
2452
2452
|
* them. `pinTo` is `vexp use --pin`, on purpose. The http section keeps its
|
|
2453
2453
|
* project: the URL routes to one workspace's daemon.
|
|
2454
2454
|
*
|
|
2455
|
+
* `vexp-core mcp` starts the checkout's shared daemon when none runs, as the
|
|
2456
|
+
* Node server does. Unused for 30 minutes it stops and frees its slot of the
|
|
2457
|
+
* plan's daemon cap (vexp-core idle.rs; `idle_timeout_minutes` overrides it).
|
|
2458
|
+
* VEXP_NO_AUTOSTART=1, or a cap already full, leaves the session serving
|
|
2459
|
+
* in-process and taking no slot.
|
|
2460
|
+
*
|
|
2455
2461
|
* BYTE-IDENTICAL with the extension's buildCodexSection, marker lines
|
|
2456
2462
|
* included, or the two rewrite this machine-wide file in turn. */
|
|
2457
2463
|
function buildCodexSection(opts) {
|
|
@@ -2508,10 +2514,21 @@ Authorization = "Bearer ${opts.token}"
|
|
|
2508
2514
|
* buildCodexSection), so one user entry serves every project.
|
|
2509
2515
|
*
|
|
2510
2516
|
* Default transport is `direct` (stdio: `vexp-core mcp`), which works with AND
|
|
2511
|
-
* without VS Code
|
|
2512
|
-
*
|
|
2517
|
+
* without VS Code: `vexp-core mcp` proxies to the project's daemon, starting it
|
|
2518
|
+
* when none runs as the Node MCP server does, so every session on a checkout
|
|
2519
|
+
* shares one daemon. It runs an embedded in-process index where it may not
|
|
2520
|
+
* start one (VEXP_NO_AUTOSTART=1, the daily limit), where the plan's daemon cap
|
|
2521
|
+
* refuses it, or where it does not answer in time. Override with the
|
|
2513
2522
|
* VEXP_CODEX_TRANSPORT env var ("direct" | "http").
|
|
2514
2523
|
*
|
|
2524
|
+
* Every agent configured with `vexp-core mcp` directly runs that server and
|
|
2525
|
+
* starts the daemon the same way: Cline, Continue, ZCode and the entries
|
|
2526
|
+
* written where no Node runtime is found. In an editor with the vexp
|
|
2527
|
+
* extension, such a server can start the daemon before the extension does.
|
|
2528
|
+
* The extension then adopts it as a daemon it does not own, and the rule
|
|
2529
|
+
* that ends a window's daemon with its window does not apply to it: it runs
|
|
2530
|
+
* until `vexp stop`, the idle stop (`idle_timeout_minutes`) or a reboot.
|
|
2531
|
+
*
|
|
2515
2532
|
* `pin` is `vexp use <project> --pin`: the one way to name a project in the entry,
|
|
2516
2533
|
* kept by every later setup until `vexp use` changes it. `unpin` is
|
|
2517
2534
|
* `vexp use --unpin`. [mcp_servers.vexp.tools.*] tables (Codex's per-tool
|
package/dist/cli.js
CHANGED
|
@@ -46,7 +46,8 @@ Daemons:
|
|
|
46
46
|
[daemon]
|
|
47
47
|
idle_timeout_minutes = 30
|
|
48
48
|
|
|
49
|
-
VEXP_IDLE_TIMEOUT_MINUTES sets it for one daemon.
|
|
49
|
+
VEXP_IDLE_TIMEOUT_MINUTES sets it for one daemon. Unset, a daemon an agent's session started
|
|
50
|
+
stops after 30 minutes without use (0 turns that off). A daemon an editor window started stops
|
|
50
51
|
with that window instead. vexp daemons lists them; vexp doctor says whether the setting is on.
|
|
51
52
|
`;
|
|
52
53
|
program.addHelpText("after", IDLE_STOP_HELP);
|
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
|
*/
|
package/dist/doctor.js
CHANGED
|
@@ -3492,7 +3492,7 @@ async function doctorChecks(onWorkspace) {
|
|
|
3492
3492
|
// the cause can change between starts.
|
|
3493
3493
|
const why = typeof st.llm_inactive_reason === "string" ? st.llm_inactive_reason : undefined;
|
|
3494
3494
|
const restartIsPointless = why !== undefined &&
|
|
3495
|
-
/vanilla build|model load failed|incomplete|did not survive|worker did not start/i.test(why);
|
|
3495
|
+
/vanilla build|model load failed|incomplete|did not survive|worker did not start|needs a GPU/i.test(why);
|
|
3496
3496
|
// A daemon still running its startup sync has not REACHED the model
|
|
3497
3497
|
// yet. Restarting there is worse than useless: it starts the sync
|
|
3498
3498
|
// over, and the user never arrives.
|
package/dist/forget.js
CHANGED
package/dist/serve.js
CHANGED
|
@@ -125,7 +125,7 @@ async function resurrectDaemon(workspaceRoot, socketPath) {
|
|
|
125
125
|
child = spawn(binary, ["daemon", "--workspace", workspaceRoot, "--socket", socketPath], {
|
|
126
126
|
detached: true,
|
|
127
127
|
stdio: ["ignore", log, log],
|
|
128
|
-
env:
|
|
128
|
+
env: serveDaemonEnv(binary, workspaceRoot),
|
|
129
129
|
});
|
|
130
130
|
}
|
|
131
131
|
finally {
|
|
@@ -143,6 +143,20 @@ async function resurrectDaemon(workspaceRoot, socketPath) {
|
|
|
143
143
|
return false;
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
|
+
/** The environment of a daemon brought back: one that was started for an
|
|
147
|
+
* agent's session stays one, with its idle stop. Without this a crash made it
|
|
148
|
+
* permanent, holding its slot of the plan's daemon cap. The role is in the
|
|
149
|
+
* lock file it left (vexp-core index_lock.rs AGENT_DAEMON_ROLE). */
|
|
150
|
+
export function serveDaemonEnv(binary, workspaceRoot) {
|
|
151
|
+
const env = binaryEnv(binary);
|
|
152
|
+
try {
|
|
153
|
+
const lock = JSON.parse(fs.readFileSync(path.join(workspaceRoot, ".vexp", "daemon.lock"), "utf-8"));
|
|
154
|
+
if (lock?.role === "agent daemon")
|
|
155
|
+
env.VEXP_DAEMON_OWNER = "agent";
|
|
156
|
+
}
|
|
157
|
+
catch { /* no lock, or held and unreadable: no owner */ }
|
|
158
|
+
return env;
|
|
159
|
+
}
|
|
146
160
|
/** True when `sock` is the canonical socket for `ws` itself (unix path or the
|
|
147
161
|
* win32 named pipe derived from `ws`). A registry row that FAILS this is a
|
|
148
162
|
* ROUTING entry: a connected secondary repo mapped to its parent workspace's
|
|
@@ -203,8 +217,40 @@ export function resurrectionVeto(ws) {
|
|
|
203
217
|
return "no manifest";
|
|
204
218
|
if (isLinkedWorktree(ws))
|
|
205
219
|
return "linked git worktree — on-demand only, never resurrected";
|
|
220
|
+
if (indexesNothingUnsetUp(ws)) {
|
|
221
|
+
return "its manifest lists no file and it was never set up — the daemon refuses it unless it now holds sources, so on-demand only";
|
|
222
|
+
}
|
|
206
223
|
return null;
|
|
207
224
|
}
|
|
225
|
+
/** The files a person writes to set a folder up on purpose
|
|
226
|
+
* (vexp-core utils::SET_UP_ON_PURPOSE, held equal by a test there). */
|
|
227
|
+
export const SET_UP_ON_PURPOSE = ["vexp.toml", "workspace.json"];
|
|
228
|
+
/** A workspace whose manifest lists no file and that nobody set up on
|
|
229
|
+
* purpose: what older versions of `vexp index` left in folders with no
|
|
230
|
+
* source. The daemon refuses such a folder unless a walk finds a source
|
|
231
|
+
* file in it (vexp-core utils::nothing_to_serve); a resurrection would be
|
|
232
|
+
* a refused spawn a minute. No walk here: a row whose folder gained
|
|
233
|
+
* sources is started again by the next launcher that needs it, and its
|
|
234
|
+
* daemon then writes a manifest that lists them. A manifest that cannot
|
|
235
|
+
* be read, or has no file_hashes at all (every manifest vexp writes has
|
|
236
|
+
* one), is not "no file". */
|
|
237
|
+
function indexesNothingUnsetUp(ws) {
|
|
238
|
+
if (SET_UP_ON_PURPOSE.some((f) => fs.existsSync(path.join(ws, ".vexp", f))))
|
|
239
|
+
return false;
|
|
240
|
+
try {
|
|
241
|
+
const manifest = path.join(ws, ".vexp", "manifest.json");
|
|
242
|
+
// One that lists nothing is a few hundred bytes; a real one is not
|
|
243
|
+
// worth parsing every minute to learn that it lists files.
|
|
244
|
+
if (fs.statSync(manifest).size > 64 * 1024)
|
|
245
|
+
return false;
|
|
246
|
+
const parsed = JSON.parse(fs.readFileSync(manifest, "utf-8"));
|
|
247
|
+
const files = parsed?.file_hashes;
|
|
248
|
+
return typeof files === "object" && files !== null && Object.keys(files).length === 0;
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
208
254
|
export function resurrectionPlan(reg) {
|
|
209
255
|
const bySock = new Map();
|
|
210
256
|
for (const [ws, sock] of Object.entries(reg)) {
|