vexp-cli 3.2.4 → 3.2.5
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 +4 -1
- package/dist/doctor.js +7 -5
- package/dist/forget.js +3 -1
- package/dist/serve.js +3 -2
- package/dist/socket-path.js +25 -1
- package/mcp/mcp-server.cjs +44 -44
- package/package.json +6 -6
package/dist/agent-config.js
CHANGED
|
@@ -11,6 +11,7 @@ import * as path from "path";
|
|
|
11
11
|
import * as os from "os";
|
|
12
12
|
import { spawnSync } from "child_process";
|
|
13
13
|
import * as crypto from "crypto";
|
|
14
|
+
import { canonicalWorkspaceRoot } from "./socket-path.js";
|
|
14
15
|
import { VEXP_GUARD_HOOK, VEXP_OPENCODE_GUARD, VEXP_CURSOR_GUARD, vexpHintHookScript, vexpSearchHookScript, vexpHintHookCmdScript, vexpStopGateHookScript, vexpSessionContextHookScript, vexpOpencodeHintPlugin, vexpOpencodeCompressPlugin, bakeEditHintHook, bakeReadHintHook, bakeBashCapHook } from "./hook-template.js";
|
|
15
16
|
// ---------------------------------------------------------------------------
|
|
16
17
|
// Constants
|
|
@@ -39,7 +40,9 @@ const CANONICAL_VEXP_TOML_SECTION_RE = /\n?\[mcp_servers\.vexp(?:\.[a-z_]+)?\][\
|
|
|
39
40
|
* MUST stay in lockstep with that function.
|
|
40
41
|
*/
|
|
41
42
|
function workspaceHash(workspaceRoot) {
|
|
42
|
-
|
|
43
|
+
// The route hash must name the row the daemon registers: its canonical
|
|
44
|
+
// root, which on a mapped drive is the share, not the drive letter.
|
|
45
|
+
const input = canonicalWorkspaceRoot(workspaceRoot).toLowerCase();
|
|
43
46
|
let hash = BigInt("0xcbf29ce484222325");
|
|
44
47
|
const prime = BigInt("0x100000001b3");
|
|
45
48
|
const mask = BigInt("0xffffffffffffffff");
|
package/dist/doctor.js
CHANGED
|
@@ -11,6 +11,7 @@ import { CLI_VERSION } from "./version.js";
|
|
|
11
11
|
import { parentPid } from "./mcp-supervisor.js";
|
|
12
12
|
import { slowMountNotice } from "./slow-mount.js";
|
|
13
13
|
import { vexpHome, isStateHome } from "./state-home.js";
|
|
14
|
+
import { canonicalWorkspaceRoot } from "./socket-path.js";
|
|
14
15
|
// `vexp doctor` — audit the vexp MCP/daemon state WITHOUT connecting to a daemon.
|
|
15
16
|
// Surfaces the failure modes behind the Codex drift report: stale daemons.json
|
|
16
17
|
// entries, wrong-workspace resolution, mixed Codex transport (url+stdio),
|
|
@@ -292,7 +293,7 @@ export function vsCodeMcpVerdict(cfg, wsRoot, exists = (p) => fs.existsSync(p))
|
|
|
292
293
|
if (script && !exists(script))
|
|
293
294
|
return { level: WARN, message: `.vscode/mcp.json vexp server bundle is missing: ${script} (an editor upgrade removed the old extension folder?) — re-run 'vexp setup'` };
|
|
294
295
|
const pinned = typeof vexp.env?.VEXP_WORKSPACE === "string" ? vexp.env.VEXP_WORKSPACE : undefined;
|
|
295
|
-
if (pinned &&
|
|
296
|
+
if (pinned && !samePath(pinned, wsRoot)) {
|
|
296
297
|
return { level: WARN, message: `.vscode/mcp.json vexp server is pinned to ${pinned}, but this workspace is ${wsRoot} — re-run 'vexp setup' here` };
|
|
297
298
|
}
|
|
298
299
|
return { level: OK, message: `.vscode/mcp.json vexp server: ${command} ${script ?? args.join(" ")}` };
|
|
@@ -314,8 +315,9 @@ export function codexHookSpawnSpec(cmdLine, platform = process.platform, comspec
|
|
|
314
315
|
return { file: "sh", args: ["-c", cmdLine], windowsVerbatimArguments: false };
|
|
315
316
|
}
|
|
316
317
|
function samePath(a, b) {
|
|
317
|
-
// The daemon registry lower-cases Windows roots
|
|
318
|
-
|
|
318
|
+
// The daemon registry lower-cases Windows roots and spells a mapped drive
|
|
319
|
+
// as its share; compare accordingly.
|
|
320
|
+
return canonicalWorkspaceRoot(path.resolve(a)).toLowerCase() === canonicalWorkspaceRoot(path.resolve(b)).toLowerCase();
|
|
319
321
|
}
|
|
320
322
|
/**
|
|
321
323
|
* A connected repo of a multi-repo workspace has no daemon of its own: the
|
|
@@ -633,7 +635,7 @@ async function doctorChecks(onWorkspace) {
|
|
|
633
635
|
// disagrees with this directory's natural target.
|
|
634
636
|
if (ws.source === "VEXP_WORKSPACE") {
|
|
635
637
|
const discovered = discoverWorkspaceRoot(process.cwd());
|
|
636
|
-
if (discovered
|
|
638
|
+
if (!samePath(discovered, ws.root)) {
|
|
637
639
|
line(WARN, `global VEXP_WORKSPACE env var pins this session to ${ws.root}, but this directory's natural target is ${discovered} → agents here get [] / wrong-repo results. Unset the VEXP_WORKSPACE OS/shell env var for per-session targeting.`);
|
|
638
640
|
}
|
|
639
641
|
}
|
|
@@ -776,7 +778,7 @@ async function doctorChecks(onWorkspace) {
|
|
|
776
778
|
line(BAD, `${root} is your home directory — never a workspace (its .vexp is vexp's state dir). A daemon here indexes everything under home. Run: vexp forget "${root}" (stops it, drops this row, deletes only its index files — licence, config and registry stay)`);
|
|
777
779
|
}
|
|
778
780
|
}
|
|
779
|
-
const inReg = entries.some(([r]) => r
|
|
781
|
+
const inReg = entries.some(([r]) => samePath(r, ws.root));
|
|
780
782
|
if (!inReg)
|
|
781
783
|
line(WARN, `current workspace (${ws.root}) is NOT registered — a child here could mis-route`);
|
|
782
784
|
// Multi-daemon + global pin: catches the case the section-1 check misses
|
package/dist/forget.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import { vexpHome } from "./state-home.js";
|
|
4
|
+
import { canonicalWorkspaceRoot } from "./socket-path.js";
|
|
4
5
|
/**
|
|
5
6
|
* `vexp forget`: the pieces that make a workspace stay forgotten.
|
|
6
7
|
*
|
|
@@ -85,7 +86,8 @@ function registryFile() {
|
|
|
85
86
|
* socket (a connected repo mapped to this workspace's socket is dangling
|
|
86
87
|
* once the workspace is gone). Windows keys are lowercased by the daemon. */
|
|
87
88
|
export function registryRowsWithout(reg, target, targetSocket) {
|
|
88
|
-
|
|
89
|
+
// A mapped drive is registered as its share; canonicalize before comparing.
|
|
90
|
+
const canon = (p) => (process.platform === "win32" ? canonicalWorkspaceRoot(path.resolve(p)).toLowerCase() : path.resolve(p));
|
|
89
91
|
const t = canon(target);
|
|
90
92
|
const kept = {};
|
|
91
93
|
const dropped = [];
|
package/dist/serve.js
CHANGED
|
@@ -6,6 +6,7 @@ import { spawn } from "child_process";
|
|
|
6
6
|
import { getBinaryPath, binaryEnv } from "./binary.js";
|
|
7
7
|
import { ensureMcpHttpServer } from "./mcp-supervisor.js";
|
|
8
8
|
import { isStateHome } from "./state-home.js";
|
|
9
|
+
import { canonicalWorkspaceRoot } from "./socket-path.js";
|
|
9
10
|
const HEALTH_INTERVAL_MS = 60_000;
|
|
10
11
|
const DEFAULT_MCP_PORT = 7821;
|
|
11
12
|
function registryPath() {
|
|
@@ -241,7 +242,7 @@ async function resurrectAll() {
|
|
|
241
242
|
// rewrite here would otherwise immortalize the phantom twin.
|
|
242
243
|
const seenKeys = new Set();
|
|
243
244
|
for (const [ws, sock] of Object.entries(reg)) {
|
|
244
|
-
const canonical = process.platform === "win32" ? ws.toLowerCase() : ws;
|
|
245
|
+
const canonical = process.platform === "win32" ? canonicalWorkspaceRoot(ws).toLowerCase() : ws;
|
|
245
246
|
if (seenKeys.has(canonical)) {
|
|
246
247
|
appendLog(`registry prune: ${ws} (case-variant duplicate)`);
|
|
247
248
|
continue;
|
|
@@ -299,7 +300,7 @@ async function resurrectAll() {
|
|
|
299
300
|
* report what it could not find, never delete what it does not own.
|
|
300
301
|
*/
|
|
301
302
|
export async function survivingRegistryRows(before, resurrected, alive, log = () => { }) {
|
|
302
|
-
const canonical = (ws) => (process.platform === "win32" ? ws.toLowerCase() : ws);
|
|
303
|
+
const canonical = (ws) => (process.platform === "win32" ? canonicalWorkspaceRoot(ws).toLowerCase() : ws);
|
|
303
304
|
const out = { ...resurrected };
|
|
304
305
|
const kept = new Set(Object.keys(out).map(canonical));
|
|
305
306
|
for (const [ws, sock] of Object.entries(before)) {
|
package/dist/socket-path.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
1
2
|
import * as path from "path";
|
|
2
3
|
/**
|
|
3
4
|
* Where the daemon listens for a given workspace root.
|
|
@@ -18,7 +19,7 @@ import * as path from "path";
|
|
|
18
19
|
*/
|
|
19
20
|
export function socketPathFor(workspaceRoot) {
|
|
20
21
|
if (process.platform === "win32") {
|
|
21
|
-
return `\\\\.\\pipe\\vexp-${fnvHash(workspaceRoot.toLowerCase()).slice(0, 8)}`;
|
|
22
|
+
return `\\\\.\\pipe\\vexp-${fnvHash(canonicalWorkspaceRoot(workspaceRoot).toLowerCase()).slice(0, 8)}`;
|
|
22
23
|
}
|
|
23
24
|
const candidate = path.join(workspaceRoot, ".vexp", "daemon.sock");
|
|
24
25
|
// macOS/BSD sockaddr_un caps the path at 104 bytes; vexp-core falls back to
|
|
@@ -27,6 +28,29 @@ export function socketPathFor(workspaceRoot) {
|
|
|
27
28
|
return candidate;
|
|
28
29
|
return `/tmp/vexp-${fnvHash(workspaceRoot).slice(0, 12)}.sock`;
|
|
29
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* The workspace root as the daemon spells it. vexp-core canonicalizes the
|
|
33
|
+
* root before deriving the pipe name and the daemons.json key, and on
|
|
34
|
+
* Windows a folder on a mapped drive canonicalizes to its share:
|
|
35
|
+
* `X:\proj` becomes `\\server\share\proj`. Every hash computed from the
|
|
36
|
+
* drive-letter spelling named a pipe the daemon never bound, so `vexp
|
|
37
|
+
* status` on a share reported the daemon as down and started a second one,
|
|
38
|
+
* and the MCP bridge could not reach it (field report, Samba share from
|
|
39
|
+
* Windows 11, 2026-09-20). Node's native realpath resolves the same way; a
|
|
40
|
+
* local path only gets its case corrected, which the lowercase hash never
|
|
41
|
+
* saw. Falls back to the spelling given when the folder cannot be resolved
|
|
42
|
+
* (a stale registry row, a path that is not there yet).
|
|
43
|
+
*/
|
|
44
|
+
export function canonicalWorkspaceRoot(root) {
|
|
45
|
+
if (process.platform !== "win32")
|
|
46
|
+
return root;
|
|
47
|
+
try {
|
|
48
|
+
return fs.realpathSync.native(root);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return root;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
30
54
|
/** Longest `<root>/.vexp/daemon.sock` vexp binds in place; past it the socket lives in /tmp. */
|
|
31
55
|
export const UNIX_SOCKET_PATH_LIMIT = 100;
|
|
32
56
|
/**
|