vexp-cli 2.0.19 → 2.0.20
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 +144 -54
- package/package.json +7 -6
package/dist/agent-config.js
CHANGED
|
@@ -398,6 +398,102 @@ export function configureSelectedAgents(workspaceRoot, binaryPath, version, sele
|
|
|
398
398
|
// ---------------------------------------------------------------------------
|
|
399
399
|
// File helpers — ported from agent-auto-config.ts
|
|
400
400
|
// ---------------------------------------------------------------------------
|
|
401
|
+
/**
|
|
402
|
+
* Parse JSON that may use JSONC syntax — `//` and slash-star comments and
|
|
403
|
+
* trailing commas. VS Code mcp.json and Zed settings.json are JSONC, so a raw
|
|
404
|
+
* JSON.parse throws on perfectly valid user files. Strips comments and trailing
|
|
405
|
+
* commas (preserving string contents) before parsing.
|
|
406
|
+
*/
|
|
407
|
+
export function parseJsonc(text) {
|
|
408
|
+
let out = "";
|
|
409
|
+
let inString = false;
|
|
410
|
+
let inLineComment = false;
|
|
411
|
+
let inBlockComment = false;
|
|
412
|
+
for (let i = 0; i < text.length; i++) {
|
|
413
|
+
const c = text[i];
|
|
414
|
+
const next = text[i + 1];
|
|
415
|
+
if (inLineComment) {
|
|
416
|
+
if (c === "\n") {
|
|
417
|
+
inLineComment = false;
|
|
418
|
+
out += c;
|
|
419
|
+
}
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
if (inBlockComment) {
|
|
423
|
+
if (c === "*" && next === "/") {
|
|
424
|
+
inBlockComment = false;
|
|
425
|
+
i++;
|
|
426
|
+
}
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
if (inString) {
|
|
430
|
+
out += c;
|
|
431
|
+
if (c === "\\") {
|
|
432
|
+
out += next ?? "";
|
|
433
|
+
i++;
|
|
434
|
+
}
|
|
435
|
+
else if (c === '"')
|
|
436
|
+
inString = false;
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
if (c === '"') {
|
|
440
|
+
inString = true;
|
|
441
|
+
out += c;
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
if (c === "/" && next === "/") {
|
|
445
|
+
inLineComment = true;
|
|
446
|
+
i++;
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
if (c === "/" && next === "*") {
|
|
450
|
+
inBlockComment = true;
|
|
451
|
+
i++;
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
out += c;
|
|
455
|
+
}
|
|
456
|
+
out = out.replace(/,(\s*[}\]])/g, "$1");
|
|
457
|
+
return JSON.parse(out);
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Read a JSON/JSONC config file without ever risking user data. When the file
|
|
461
|
+
* exists but cannot be parsed (even as JSONC), returns ok:false so callers skip
|
|
462
|
+
* the write instead of silently clobbering the user's config.
|
|
463
|
+
*/
|
|
464
|
+
export function readJsonConfigSafe(filePath) {
|
|
465
|
+
if (!fs.existsSync(filePath))
|
|
466
|
+
return { data: {}, ok: true, existed: false };
|
|
467
|
+
let raw;
|
|
468
|
+
try {
|
|
469
|
+
raw = fs.readFileSync(filePath, "utf-8");
|
|
470
|
+
}
|
|
471
|
+
catch {
|
|
472
|
+
return { data: {}, ok: false, existed: true };
|
|
473
|
+
}
|
|
474
|
+
if (raw.trim() === "")
|
|
475
|
+
return { data: {}, ok: true, existed: true };
|
|
476
|
+
try {
|
|
477
|
+
const parsed = parseJsonc(raw);
|
|
478
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
479
|
+
return { data: parsed, ok: true, existed: true };
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
catch { /* fall through to ok:false */ }
|
|
483
|
+
return { data: {}, ok: false, existed: true };
|
|
484
|
+
}
|
|
485
|
+
/** Copy an existing config to <file>.vexp-bak before overwriting it. */
|
|
486
|
+
function backupConfig(filePath) {
|
|
487
|
+
try {
|
|
488
|
+
if (fs.existsSync(filePath))
|
|
489
|
+
fs.copyFileSync(filePath, `${filePath}.vexp-bak`);
|
|
490
|
+
}
|
|
491
|
+
catch { /* non-fatal */ }
|
|
492
|
+
}
|
|
493
|
+
/** Warn that we refused to touch an unparseable config file. */
|
|
494
|
+
function warnUnparseable(filePath) {
|
|
495
|
+
process.stderr.write(` ⚠ ${filePath} could not be parsed — leaving it untouched. Fix the file or add vexp manually, then re-run setup.\n`);
|
|
496
|
+
}
|
|
401
497
|
function appendOrCreate(filePath, content, version) {
|
|
402
498
|
if (!fs.existsSync(filePath)) {
|
|
403
499
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
@@ -433,16 +529,13 @@ function appendOrCreate(filePath, content, version) {
|
|
|
433
529
|
}
|
|
434
530
|
return "updated";
|
|
435
531
|
}
|
|
436
|
-
function mergeJsonConfig(filePath, mergePayload, version) {
|
|
437
|
-
|
|
438
|
-
if (
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
}
|
|
442
|
-
catch {
|
|
443
|
-
// Malformed JSON: overwrite
|
|
444
|
-
}
|
|
532
|
+
export function mergeJsonConfig(filePath, mergePayload, version) {
|
|
533
|
+
const read = readJsonConfigSafe(filePath);
|
|
534
|
+
if (!read.ok) {
|
|
535
|
+
warnUnparseable(filePath);
|
|
536
|
+
return "skipped";
|
|
445
537
|
}
|
|
538
|
+
const existing = read.data;
|
|
446
539
|
// Build the merged result first, then compare to avoid unnecessary writes.
|
|
447
540
|
const merged = { ...existing };
|
|
448
541
|
for (const [key, value] of Object.entries(mergePayload)) {
|
|
@@ -467,6 +560,8 @@ function mergeJsonConfig(filePath, mergePayload, version) {
|
|
|
467
560
|
if (mergedStr === existingStr)
|
|
468
561
|
return "skipped";
|
|
469
562
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
563
|
+
if (read.existed)
|
|
564
|
+
backupConfig(filePath);
|
|
470
565
|
fs.writeFileSync(filePath, mergedStr, "utf-8");
|
|
471
566
|
return "merged";
|
|
472
567
|
}
|
|
@@ -477,16 +572,13 @@ function mergeJsonConfig(filePath, mergePayload, version) {
|
|
|
477
572
|
* Write MCP config JSON for agents that use mcpServers format (Cursor, Windsurf).
|
|
478
573
|
* Returns true if a new entry was written.
|
|
479
574
|
*/
|
|
480
|
-
function writeMcpConfig(mcpConfigPath, binaryPath, alwaysAllow, mcpServerPath, workspaceRoot) {
|
|
481
|
-
|
|
482
|
-
if (
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
}
|
|
486
|
-
catch {
|
|
487
|
-
// Malformed JSON: overwrite
|
|
488
|
-
}
|
|
575
|
+
export function writeMcpConfig(mcpConfigPath, binaryPath, alwaysAllow, mcpServerPath, workspaceRoot) {
|
|
576
|
+
const read = readJsonConfigSafe(mcpConfigPath);
|
|
577
|
+
if (!read.ok) {
|
|
578
|
+
warnUnparseable(mcpConfigPath);
|
|
579
|
+
return false;
|
|
489
580
|
}
|
|
581
|
+
const existing = read.data;
|
|
490
582
|
const useNode = mcpServerPath && fs.existsSync(mcpServerPath);
|
|
491
583
|
const targetCmd = useNode ? "node" : binaryPath;
|
|
492
584
|
const targetArgs = useNode ? [mcpServerPath] : ["mcp"];
|
|
@@ -517,6 +609,8 @@ function writeMcpConfig(mcpConfigPath, binaryPath, alwaysAllow, mcpServerPath, w
|
|
|
517
609
|
};
|
|
518
610
|
existing.mcpServers = servers;
|
|
519
611
|
fs.mkdirSync(path.dirname(mcpConfigPath), { recursive: true });
|
|
612
|
+
if (read.existed)
|
|
613
|
+
backupConfig(mcpConfigPath);
|
|
520
614
|
fs.writeFileSync(mcpConfigPath, JSON.stringify(existing, null, 2), "utf-8");
|
|
521
615
|
return true;
|
|
522
616
|
}
|
|
@@ -611,16 +705,13 @@ function configureCodexGlobal(binaryPath, mcpServerPath, workspaceRoot) {
|
|
|
611
705
|
* Write .vscode/mcp.json for GitHub Copilot.
|
|
612
706
|
* VS Code uses "servers" (not "mcpServers") + "type": "stdio".
|
|
613
707
|
*/
|
|
614
|
-
function writeVsCodeMcpConfig(p, binaryPath, mcpServerPath, workspaceRoot) {
|
|
615
|
-
|
|
616
|
-
if (
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
}
|
|
620
|
-
catch {
|
|
621
|
-
// Malformed JSON: overwrite
|
|
622
|
-
}
|
|
708
|
+
export function writeVsCodeMcpConfig(p, binaryPath, mcpServerPath, workspaceRoot) {
|
|
709
|
+
const read = readJsonConfigSafe(p);
|
|
710
|
+
if (!read.ok) {
|
|
711
|
+
warnUnparseable(p);
|
|
712
|
+
return false;
|
|
623
713
|
}
|
|
714
|
+
const existing = read.data;
|
|
624
715
|
const useNode = mcpServerPath && fs.existsSync(mcpServerPath);
|
|
625
716
|
const targetCmd = useNode ? "node" : binaryPath;
|
|
626
717
|
const targetArgs = useNode ? [mcpServerPath] : ["mcp"];
|
|
@@ -645,22 +736,21 @@ function writeVsCodeMcpConfig(p, binaryPath, mcpServerPath, workspaceRoot) {
|
|
|
645
736
|
};
|
|
646
737
|
existing.servers = servers;
|
|
647
738
|
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
739
|
+
if (read.existed)
|
|
740
|
+
backupConfig(p);
|
|
648
741
|
fs.writeFileSync(p, JSON.stringify(existing, null, 2), "utf-8");
|
|
649
742
|
return true;
|
|
650
743
|
}
|
|
651
744
|
/**
|
|
652
745
|
* Write context_servers in .zed/settings.json for Zed.
|
|
653
746
|
*/
|
|
654
|
-
function writeZedMcpConfig(p, binaryPath, mcpServerPath, workspaceRoot) {
|
|
655
|
-
|
|
656
|
-
if (
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
}
|
|
660
|
-
catch {
|
|
661
|
-
// Malformed JSON: overwrite
|
|
662
|
-
}
|
|
747
|
+
export function writeZedMcpConfig(p, binaryPath, mcpServerPath, workspaceRoot) {
|
|
748
|
+
const read = readJsonConfigSafe(p);
|
|
749
|
+
if (!read.ok) {
|
|
750
|
+
warnUnparseable(p);
|
|
751
|
+
return false;
|
|
663
752
|
}
|
|
753
|
+
const settings = read.data;
|
|
664
754
|
const useNode = mcpServerPath && fs.existsSync(mcpServerPath);
|
|
665
755
|
const targetCmd = useNode ? "node" : binaryPath;
|
|
666
756
|
const targetArgs = useNode ? [mcpServerPath] : ["mcp"];
|
|
@@ -694,6 +784,8 @@ function writeZedMcpConfig(p, binaryPath, mcpServerPath, workspaceRoot) {
|
|
|
694
784
|
}
|
|
695
785
|
settings.agent = { ...agent, tool_permissions: { ...tp, tools } };
|
|
696
786
|
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
787
|
+
if (read.existed)
|
|
788
|
+
backupConfig(p);
|
|
697
789
|
fs.writeFileSync(p, JSON.stringify(settings, null, 2), "utf-8");
|
|
698
790
|
return true;
|
|
699
791
|
}
|
|
@@ -701,20 +793,17 @@ function writeZedMcpConfig(p, binaryPath, mcpServerPath, workspaceRoot) {
|
|
|
701
793
|
* Configure vexp MCP in ~/.claude.json (user-scope) using the Rust binary.
|
|
702
794
|
* Returns true if config was written/updated.
|
|
703
795
|
*/
|
|
704
|
-
function configureClaudeCodeGlobal(binaryPath, mcpServerPath, workspaceRoot) {
|
|
796
|
+
export function configureClaudeCodeGlobal(binaryPath, mcpServerPath, workspaceRoot) {
|
|
705
797
|
const home = os.homedir();
|
|
706
798
|
if (!home)
|
|
707
799
|
return false;
|
|
708
800
|
const configPath = path.join(home, ".claude.json");
|
|
709
|
-
|
|
710
|
-
if (
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
}
|
|
714
|
-
catch {
|
|
715
|
-
// Malformed JSON: overwrite
|
|
716
|
-
}
|
|
801
|
+
const read = readJsonConfigSafe(configPath);
|
|
802
|
+
if (!read.ok) {
|
|
803
|
+
warnUnparseable(configPath);
|
|
804
|
+
return false;
|
|
717
805
|
}
|
|
806
|
+
const config = read.data;
|
|
718
807
|
const beforeServers = config.mcpServers;
|
|
719
808
|
const existing = beforeServers?.["vexp"];
|
|
720
809
|
const useNode = mcpServerPath && fs.existsSync(mcpServerPath);
|
|
@@ -736,6 +825,8 @@ function configureClaudeCodeGlobal(binaryPath, mcpServerPath, workspaceRoot) {
|
|
|
736
825
|
...(desiredEnv ? { env: desiredEnv } : {}),
|
|
737
826
|
};
|
|
738
827
|
config.mcpServers = servers;
|
|
828
|
+
if (read.existed)
|
|
829
|
+
backupConfig(configPath);
|
|
739
830
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
740
831
|
return true;
|
|
741
832
|
}
|
|
@@ -747,7 +838,7 @@ function configureClaudeCodeGlobal(binaryPath, mcpServerPath, workspaceRoot) {
|
|
|
747
838
|
* Creates .claude/hooks/vexp-guard.sh and merges hook config into .claude/settings.json.
|
|
748
839
|
* Returns the action taken, or null if nothing was written.
|
|
749
840
|
*/
|
|
750
|
-
function installClaudeCodeHook(workspaceRoot) {
|
|
841
|
+
export function installClaudeCodeHook(workspaceRoot) {
|
|
751
842
|
const hookDir = path.join(workspaceRoot, ".claude", "hooks");
|
|
752
843
|
const hookPath = path.join(hookDir, "vexp-guard.sh");
|
|
753
844
|
const settingsPath = path.join(workspaceRoot, ".claude", "settings.json");
|
|
@@ -761,15 +852,12 @@ function installClaudeCodeHook(workspaceRoot) {
|
|
|
761
852
|
}
|
|
762
853
|
fs.writeFileSync(hookPath, VEXP_GUARD_HOOK, { mode: 0o755 });
|
|
763
854
|
// 2. Merge hook config into .claude/settings.json
|
|
764
|
-
|
|
765
|
-
if (
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
}
|
|
769
|
-
catch {
|
|
770
|
-
// Malformed JSON: overwrite
|
|
771
|
-
}
|
|
855
|
+
const read = readJsonConfigSafe(settingsPath);
|
|
856
|
+
if (!read.ok) {
|
|
857
|
+
warnUnparseable(settingsPath);
|
|
858
|
+
return existed ? "updated" : "created";
|
|
772
859
|
}
|
|
860
|
+
const settings = read.data;
|
|
773
861
|
const hooks = (settings.hooks ?? {});
|
|
774
862
|
const existingPreToolUse = Array.isArray(hooks.PreToolUse) ? hooks.PreToolUse : [];
|
|
775
863
|
// Aggressively remove ALL vexp-related hook entries (old format, stale matchers, malformed)
|
|
@@ -806,6 +894,8 @@ function installClaudeCodeHook(workspaceRoot) {
|
|
|
806
894
|
],
|
|
807
895
|
});
|
|
808
896
|
settings.hooks = { ...hooks, PreToolUse: filtered };
|
|
897
|
+
if (read.existed)
|
|
898
|
+
backupConfig(settingsPath);
|
|
809
899
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
810
900
|
return existed ? "updated" : "created";
|
|
811
901
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vexp-cli",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.20",
|
|
4
4
|
"description": "Vexp — Context Engine for AI Coding Agents. Pre-indexes your codebase into a dependency graph and delivers ranked context to any MCP-compatible agent. 58% lower cost per task, 90% fewer tool calls (SWE-bench Verified). Works with Claude Code, Cursor, Copilot, Windsurf, Codex, Cline, Aider, and 12+ agents. Local-first. Your code never leaves your machine.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"scripts": {
|
|
10
10
|
"build": "tsc",
|
|
11
11
|
"dev": "tsx src/cli.ts",
|
|
12
|
+
"test": "node --import tsx --test test/*.test.ts",
|
|
12
13
|
"clean": "rm -rf dist",
|
|
13
14
|
"clean:artifacts": "node -e \"const fs=require('fs'),p=require('path');function w(d){if(!fs.existsSync(d))return;for(const e of fs.readdirSync(d,{withFileTypes:true})){const f=p.join(d,e.name);if(e.isDirectory())w(f);else if(/\\.(map|d\\.ts|d\\.ts\\.map)$/.test(e.name))fs.unlinkSync(f);}}w('dist');w('mcp');\"",
|
|
14
15
|
"prepublishOnly": "npm run build && npm run clean:artifacts"
|
|
@@ -99,10 +100,10 @@
|
|
|
99
100
|
"node": ">=20.0.0"
|
|
100
101
|
},
|
|
101
102
|
"optionalDependencies": {
|
|
102
|
-
"@vexp/core-linux-x64": "2.0.
|
|
103
|
-
"@vexp/core-linux-arm64": "2.0.
|
|
104
|
-
"@vexp/core-darwin-x64": "2.0.
|
|
105
|
-
"@vexp/core-darwin-arm64": "2.0.
|
|
106
|
-
"@vexp/core-win32-x64": "2.0.
|
|
103
|
+
"@vexp/core-linux-x64": "2.0.20",
|
|
104
|
+
"@vexp/core-linux-arm64": "2.0.20",
|
|
105
|
+
"@vexp/core-darwin-x64": "2.0.20",
|
|
106
|
+
"@vexp/core-darwin-arm64": "2.0.20",
|
|
107
|
+
"@vexp/core-win32-x64": "2.0.20"
|
|
107
108
|
}
|
|
108
109
|
}
|