vexp-cli 2.0.3 → 2.0.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.
@@ -186,13 +186,13 @@ export function configureAgents(workspaceRoot, binaryPath, version, agentFilter,
186
186
  if (detector.mcpConfigFile) {
187
187
  const mcpConfigPath = path.join(workspaceRoot, detector.mcpConfigFile);
188
188
  const alwaysAllow = detector.agent === "Windsurf" ? VEXP_TOOLS : undefined;
189
- if (writeMcpConfig(mcpConfigPath, binaryPath, alwaysAllow, mcpServerPath)) {
189
+ if (writeMcpConfig(mcpConfigPath, binaryPath, alwaysAllow, mcpServerPath, workspaceRoot)) {
190
190
  mcpConfigs.push(detector.mcpConfigFile);
191
191
  }
192
192
  }
193
193
  // Claude Code: configure MCP in ~/.claude.json (user-scope, stdio)
194
194
  if (detector.agent === "Claude Code") {
195
- const wrote = configureClaudeCodeGlobal(binaryPath, mcpServerPath);
195
+ const wrote = configureClaudeCodeGlobal(binaryPath, mcpServerPath, workspaceRoot);
196
196
  if (wrote)
197
197
  mcpConfigs.push("~/.claude.json");
198
198
  // Install PreToolUse hook: blocks Grep/Glob when vexp daemon is available
@@ -216,14 +216,14 @@ export function configureAgents(workspaceRoot, binaryPath, version, agentFilter,
216
216
  // GitHub Copilot: VS Code format (.vscode/mcp.json with "servers" key)
217
217
  if (detector.agent === "GitHub Copilot") {
218
218
  const mcpPath = path.join(workspaceRoot, ".vscode/mcp.json");
219
- if (writeVsCodeMcpConfig(mcpPath, binaryPath, mcpServerPath)) {
219
+ if (writeVsCodeMcpConfig(mcpPath, binaryPath, mcpServerPath, workspaceRoot)) {
220
220
  mcpConfigs.push(".vscode/mcp.json");
221
221
  }
222
222
  }
223
223
  // Zed: context_servers in .zed/settings.json
224
224
  if (detector.agent === "Zed") {
225
225
  const zedPath = path.join(workspaceRoot, ".zed/settings.json");
226
- if (writeZedMcpConfig(zedPath, binaryPath, mcpServerPath)) {
226
+ if (writeZedMcpConfig(zedPath, binaryPath, mcpServerPath, workspaceRoot)) {
227
227
  mcpConfigs.push(".zed/settings.json");
228
228
  }
229
229
  }
@@ -306,11 +306,11 @@ export function configureSelectedAgents(workspaceRoot, binaryPath, version, sele
306
306
  if (detector.mcpConfigFile) {
307
307
  const mcpConfigPath = path.join(workspaceRoot, detector.mcpConfigFile);
308
308
  const alwaysAllow = detector.agent === "Windsurf" ? VEXP_TOOLS : undefined;
309
- if (writeMcpConfig(mcpConfigPath, binaryPath, alwaysAllow, mcpServerPath))
309
+ if (writeMcpConfig(mcpConfigPath, binaryPath, alwaysAllow, mcpServerPath, workspaceRoot))
310
310
  mcpConfigs.push(detector.mcpConfigFile);
311
311
  }
312
312
  if (detector.agent === "Claude Code") {
313
- const wrote = configureClaudeCodeGlobal(binaryPath, mcpServerPath);
313
+ const wrote = configureClaudeCodeGlobal(binaryPath, mcpServerPath, workspaceRoot);
314
314
  if (wrote)
315
315
  mcpConfigs.push("~/.claude.json");
316
316
  installClaudeCodeHook(workspaceRoot);
@@ -322,12 +322,12 @@ export function configureSelectedAgents(workspaceRoot, binaryPath, version, sele
322
322
  }
323
323
  if (detector.agent === "GitHub Copilot") {
324
324
  const mcpPath = path.join(workspaceRoot, ".vscode/mcp.json");
325
- if (writeVsCodeMcpConfig(mcpPath, binaryPath, mcpServerPath))
325
+ if (writeVsCodeMcpConfig(mcpPath, binaryPath, mcpServerPath, workspaceRoot))
326
326
  mcpConfigs.push(".vscode/mcp.json");
327
327
  }
328
328
  if (detector.agent === "Zed") {
329
329
  const zedPath = path.join(workspaceRoot, ".zed/settings.json");
330
- if (writeZedMcpConfig(zedPath, binaryPath, mcpServerPath))
330
+ if (writeZedMcpConfig(zedPath, binaryPath, mcpServerPath, workspaceRoot))
331
331
  mcpConfigs.push(".zed/settings.json");
332
332
  }
333
333
  results.push({ agent: detector.agent, configFile: detector.configFile, content, alreadyExists, action });
@@ -416,7 +416,7 @@ function mergeJsonConfig(filePath, mergePayload, version) {
416
416
  * Write MCP config JSON for agents that use mcpServers format (Cursor, Windsurf).
417
417
  * Returns true if a new entry was written.
418
418
  */
419
- function writeMcpConfig(mcpConfigPath, binaryPath, alwaysAllow, mcpServerPath) {
419
+ function writeMcpConfig(mcpConfigPath, binaryPath, alwaysAllow, mcpServerPath, workspaceRoot) {
420
420
  let existing = {};
421
421
  if (fs.existsSync(mcpConfigPath)) {
422
422
  try {
@@ -430,11 +430,13 @@ function writeMcpConfig(mcpConfigPath, binaryPath, alwaysAllow, mcpServerPath) {
430
430
  const useNode = mcpServerPath && fs.existsSync(mcpServerPath);
431
431
  const targetCmd = useNode ? "node" : binaryPath;
432
432
  const targetArgs = useNode ? [mcpServerPath] : ["mcp"];
433
+ const targetEnv = workspaceRoot ? { VEXP_WORKSPACE: workspaceRoot } : undefined;
433
434
  if (servers && "vexp" in servers) {
434
435
  const cur = servers["vexp"];
435
436
  if (cur?.["command"] === targetCmd &&
436
437
  Array.isArray(cur?.["args"]) &&
437
- JSON.stringify(cur["args"]) === JSON.stringify(targetArgs)) {
438
+ JSON.stringify(cur["args"]) === JSON.stringify(targetArgs) &&
439
+ JSON.stringify(cur["env"]) === JSON.stringify(targetEnv)) {
438
440
  return false; // Already up to date
439
441
  }
440
442
  }
@@ -443,6 +445,7 @@ function writeMcpConfig(mcpConfigPath, binaryPath, alwaysAllow, mcpServerPath) {
443
445
  vexp: {
444
446
  command: targetCmd,
445
447
  args: targetArgs,
448
+ ...(targetEnv ? { env: targetEnv } : {}),
446
449
  ...(alwaysAllow && alwaysAllow.length > 0 ? { alwaysAllow } : {}),
447
450
  },
448
451
  };
@@ -527,7 +530,7 @@ function configureCodexGlobal(binaryPath, mcpServerPath, workspaceRoot) {
527
530
  * Write .vscode/mcp.json for GitHub Copilot.
528
531
  * VS Code uses "servers" (not "mcpServers") + "type": "stdio".
529
532
  */
530
- function writeVsCodeMcpConfig(p, binaryPath, mcpServerPath) {
533
+ function writeVsCodeMcpConfig(p, binaryPath, mcpServerPath, workspaceRoot) {
531
534
  let existing = {};
532
535
  if (fs.existsSync(p)) {
533
536
  try {
@@ -541,17 +544,24 @@ function writeVsCodeMcpConfig(p, binaryPath, mcpServerPath) {
541
544
  const useNode = mcpServerPath && fs.existsSync(mcpServerPath);
542
545
  const targetCmd = useNode ? "node" : binaryPath;
543
546
  const targetArgs = useNode ? [mcpServerPath] : ["mcp"];
547
+ const targetEnv = workspaceRoot ? { VEXP_WORKSPACE: workspaceRoot } : undefined;
544
548
  if (servers && "vexp" in servers) {
545
549
  const cur = servers["vexp"];
546
550
  if (cur?.["command"] === targetCmd &&
547
551
  Array.isArray(cur?.["args"]) &&
548
- JSON.stringify(cur["args"]) === JSON.stringify(targetArgs)) {
552
+ JSON.stringify(cur["args"]) === JSON.stringify(targetArgs) &&
553
+ JSON.stringify(cur["env"]) === JSON.stringify(targetEnv)) {
549
554
  return false; // Already up to date
550
555
  }
551
556
  }
552
557
  existing.servers = {
553
558
  ...(servers ?? {}),
554
- vexp: { type: "stdio", command: targetCmd, args: targetArgs },
559
+ vexp: {
560
+ type: "stdio",
561
+ command: targetCmd,
562
+ args: targetArgs,
563
+ ...(targetEnv ? { env: targetEnv } : {}),
564
+ },
555
565
  };
556
566
  fs.mkdirSync(path.dirname(p), { recursive: true });
557
567
  fs.writeFileSync(p, JSON.stringify(existing, null, 2), "utf-8");
@@ -560,7 +570,7 @@ function writeVsCodeMcpConfig(p, binaryPath, mcpServerPath) {
560
570
  /**
561
571
  * Write context_servers in .zed/settings.json for Zed.
562
572
  */
563
- function writeZedMcpConfig(p, binaryPath, mcpServerPath) {
573
+ function writeZedMcpConfig(p, binaryPath, mcpServerPath, workspaceRoot) {
564
574
  let settings = {};
565
575
  if (fs.existsSync(p)) {
566
576
  try {
@@ -574,18 +584,26 @@ function writeZedMcpConfig(p, binaryPath, mcpServerPath) {
574
584
  const useNode = mcpServerPath && fs.existsSync(mcpServerPath);
575
585
  const targetCmd = useNode ? "node" : binaryPath;
576
586
  const targetArgs = useNode ? [mcpServerPath] : ["mcp"];
587
+ const targetEnv = workspaceRoot ? { VEXP_WORKSPACE: workspaceRoot } : undefined;
577
588
  if (cs && "vexp" in cs) {
578
589
  const cur = cs["vexp"];
579
590
  const curCmd = cur?.["command"];
580
591
  if (curCmd?.["path"] === targetCmd &&
581
592
  Array.isArray(curCmd?.["args"]) &&
582
- JSON.stringify(curCmd["args"]) === JSON.stringify(targetArgs)) {
593
+ JSON.stringify(curCmd["args"]) === JSON.stringify(targetArgs) &&
594
+ JSON.stringify(curCmd["env"]) === JSON.stringify(targetEnv)) {
583
595
  return false; // Already up to date
584
596
  }
585
597
  }
586
598
  settings.context_servers = {
587
599
  ...(cs ?? {}),
588
- vexp: { command: { path: targetCmd, args: targetArgs } },
600
+ vexp: {
601
+ command: {
602
+ path: targetCmd,
603
+ args: targetArgs,
604
+ ...(targetEnv ? { env: targetEnv } : {}),
605
+ },
606
+ },
589
607
  };
590
608
  // Pre-approve all vexp tools
591
609
  const agent = (settings.agent ?? {});
@@ -603,7 +621,7 @@ function writeZedMcpConfig(p, binaryPath, mcpServerPath) {
603
621
  * Configure vexp MCP in ~/.claude.json (user-scope) using the Rust binary.
604
622
  * Returns true if config was written/updated.
605
623
  */
606
- function configureClaudeCodeGlobal(binaryPath, mcpServerPath) {
624
+ function configureClaudeCodeGlobal(binaryPath, mcpServerPath, workspaceRoot) {
607
625
  const home = os.homedir();
608
626
  if (!home)
609
627
  return false;
@@ -619,18 +637,23 @@ function configureClaudeCodeGlobal(binaryPath, mcpServerPath) {
619
637
  }
620
638
  const servers = config.mcpServers;
621
639
  const existing = servers?.["vexp"];
622
- // Skip if already configured with this exact binary path
623
- if (existing?.["command"] === binaryPath &&
640
+ const useNode = mcpServerPath && fs.existsSync(mcpServerPath);
641
+ const desiredCommand = useNode ? "node" : binaryPath;
642
+ const desiredArgs = useNode ? [mcpServerPath] : ["mcp"];
643
+ const desiredEnv = workspaceRoot ? { VEXP_WORKSPACE: workspaceRoot } : undefined;
644
+ // Skip if already configured with identical command+args+env
645
+ if (existing?.["command"] === desiredCommand &&
624
646
  Array.isArray(existing?.["args"]) &&
625
- existing["args"].includes("mcp")) {
647
+ JSON.stringify(existing["args"]) === JSON.stringify(desiredArgs) &&
648
+ JSON.stringify(existing["env"]) === JSON.stringify(desiredEnv)) {
626
649
  return false;
627
650
  }
628
- const useNode = mcpServerPath && fs.existsSync(mcpServerPath);
629
651
  config.mcpServers = {
630
652
  ...(servers ?? {}),
631
653
  vexp: {
632
- command: useNode ? "node" : binaryPath,
633
- args: useNode ? [mcpServerPath] : ["mcp"],
654
+ command: desiredCommand,
655
+ args: desiredArgs,
656
+ ...(desiredEnv ? { env: desiredEnv } : {}),
634
657
  },
635
658
  };
636
659
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
package/dist/binary.js CHANGED
@@ -28,6 +28,120 @@ function gpuRequested() {
28
28
  export function preferGpuEnv() {
29
29
  return gpuRequested() ? { VEXP_PREFER_GPU: "1" } : {};
30
30
  }
31
+ /**
32
+ * Build an env object that prepends the binary's directory to LD_LIBRARY_PATH
33
+ * (DYLD_* on macOS). Defensive fallback for binaries whose RPATH=$ORIGIN was
34
+ * not embedded at build time (older VSIX / npm packages).
35
+ */
36
+ export function binaryEnv(binaryPath, extra = {}) {
37
+ const binaryDir = path.dirname(binaryPath);
38
+ const env = { ...process.env, ...extra };
39
+ if (process.platform === "linux") {
40
+ env.LD_LIBRARY_PATH = env.LD_LIBRARY_PATH ? `${binaryDir}:${env.LD_LIBRARY_PATH}` : binaryDir;
41
+ }
42
+ else if (process.platform === "darwin") {
43
+ env.DYLD_LIBRARY_PATH = env.DYLD_LIBRARY_PATH ? `${binaryDir}:${env.DYLD_LIBRARY_PATH}` : binaryDir;
44
+ env.DYLD_FALLBACK_LIBRARY_PATH = env.DYLD_FALLBACK_LIBRARY_PATH ? `${binaryDir}:${env.DYLD_FALLBACK_LIBRARY_PATH}` : binaryDir;
45
+ }
46
+ return env;
47
+ }
48
+ /**
49
+ * Local dev builds: `cargo build --release` drops unversioned `libggml.so`
50
+ * next to vexp-core, but the binary's DT_NEEDED references the soname
51
+ * `libggml.so.0` (+ `libllama.so.0`). Production packaging (CI) uses
52
+ * `cp -P` to copy the full symlink chain; dev builds don't. With RPATH=$ORIGIN,
53
+ * the loader needs the sonames next to the binary. Self-heal by creating
54
+ * the symlink chain on first CLI invocation.
55
+ */
56
+ function ensureDevSonames(releaseDir) {
57
+ if (process.platform !== "linux" && process.platform !== "darwin")
58
+ return;
59
+ const ext = process.platform === "darwin" ? ".dylib" : ".so";
60
+ const names = ["libggml", "libggml-base", "libllama"];
61
+ for (const name of names) {
62
+ const soname = process.platform === "darwin"
63
+ ? `${name}.0${ext}` // libggml.0.dylib
64
+ : `${name}${ext}.0`; // libggml.so.0
65
+ const sonamePath = path.join(releaseDir, soname);
66
+ if (fs.existsSync(sonamePath) || isSymlink(sonamePath))
67
+ continue;
68
+ // Prefer the deepest-versioned file from the llama-cpp-sys build output.
69
+ const versioned = findVersionedLib(releaseDir, name, ext);
70
+ const unversioned = path.join(releaseDir, `${name}${ext}`);
71
+ try {
72
+ if (versioned) {
73
+ // Copy the versioned file (e.g. libggml.so.0.9.11) into releaseDir
74
+ // if not already there, then symlink soname → versioned.
75
+ const versionedName = path.basename(versioned);
76
+ const versionedDest = path.join(releaseDir, versionedName);
77
+ if (!fs.existsSync(versionedDest)) {
78
+ fs.copyFileSync(versioned, versionedDest);
79
+ }
80
+ fs.symlinkSync(versionedName, sonamePath);
81
+ }
82
+ else if (fs.existsSync(unversioned)) {
83
+ // Fallback: symlink soname → unversioned .so
84
+ fs.symlinkSync(path.basename(unversioned), sonamePath);
85
+ }
86
+ }
87
+ catch {
88
+ // Non-fatal: user will get a clearer error from the binary itself.
89
+ }
90
+ }
91
+ }
92
+ function isSymlink(p) {
93
+ try {
94
+ return fs.lstatSync(p).isSymbolicLink();
95
+ }
96
+ catch {
97
+ return false;
98
+ }
99
+ }
100
+ /** Find the deepest-versioned matching lib in target/release/build/.../out/lib/. */
101
+ function findVersionedLib(releaseDir, name, ext) {
102
+ const buildDir = path.join(releaseDir, "build");
103
+ if (!fs.existsSync(buildDir))
104
+ return null;
105
+ const pattern = process.platform === "darwin"
106
+ ? new RegExp(`^${escapeRe(name)}\\.(\\d+(?:\\.\\d+)*)${escapeRe(ext)}$`)
107
+ : new RegExp(`^${escapeRe(name)}${escapeRe(ext)}\\.(\\d+(?:\\.\\d+)*)$`);
108
+ const candidates = [];
109
+ try {
110
+ for (const entry of fs.readdirSync(buildDir)) {
111
+ if (!entry.startsWith("llama-cpp-sys-"))
112
+ continue;
113
+ const libDir = path.join(buildDir, entry, "out", "lib");
114
+ if (!fs.existsSync(libDir))
115
+ continue;
116
+ for (const f of fs.readdirSync(libDir)) {
117
+ const m = f.match(pattern);
118
+ if (m) {
119
+ candidates.push({
120
+ file: path.join(libDir, f),
121
+ version: m[1].split(".").map((n) => parseInt(n, 10)),
122
+ });
123
+ }
124
+ }
125
+ }
126
+ }
127
+ catch {
128
+ return null;
129
+ }
130
+ if (candidates.length === 0)
131
+ return null;
132
+ candidates.sort((a, b) => {
133
+ for (let i = 0; i < Math.max(a.version.length, b.version.length); i++) {
134
+ const d = (b.version[i] ?? 0) - (a.version[i] ?? 0);
135
+ if (d !== 0)
136
+ return d;
137
+ }
138
+ return 0;
139
+ });
140
+ return candidates[0].file;
141
+ }
142
+ function escapeRe(s) {
143
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
144
+ }
31
145
  export function getBinaryPath() {
32
146
  // Dev override: use VEXP_CORE_PATH env var to point to a local binary
33
147
  const envPath = process.env.VEXP_CORE_PATH;
@@ -39,6 +153,7 @@ export function getBinaryPath() {
39
153
  const scriptDir = path.dirname(new URL(import.meta.url).pathname);
40
154
  const monorepoRelease = path.resolve(scriptDir, "..", "..", "..", "target", "release", `vexp-core${ext}`);
41
155
  if (fs.existsSync(monorepoRelease)) {
156
+ ensureDevSonames(path.dirname(monorepoRelease));
42
157
  return monorepoRelease;
43
158
  }
44
159
  const key = `${process.platform}-${process.arch}`;
@@ -97,7 +212,7 @@ export async function getInstalledVersion() {
97
212
  try {
98
213
  const { execFile } = await import("child_process");
99
214
  return new Promise((resolve) => {
100
- execFile(binaryPath, ["--version"], (err, stdout) => {
215
+ execFile(binaryPath, ["--version"], { env: binaryEnv(binaryPath) }, (err, stdout) => {
101
216
  if (err) {
102
217
  resolve(null);
103
218
  return;