vexp-cli 2.0.18 → 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 +145 -54
- package/package.json +7 -6
package/dist/agent-config.js
CHANGED
|
@@ -89,6 +89,7 @@ const VEXP_TOOLS = [
|
|
|
89
89
|
"get_session_context",
|
|
90
90
|
"search_memory",
|
|
91
91
|
"save_observation",
|
|
92
|
+
"get_token_savings",
|
|
92
93
|
];
|
|
93
94
|
// ---------------------------------------------------------------------------
|
|
94
95
|
// Agent detectors — mirrors VS Code extension's AGENT_DETECTORS
|
|
@@ -397,6 +398,102 @@ export function configureSelectedAgents(workspaceRoot, binaryPath, version, sele
|
|
|
397
398
|
// ---------------------------------------------------------------------------
|
|
398
399
|
// File helpers — ported from agent-auto-config.ts
|
|
399
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
|
+
}
|
|
400
497
|
function appendOrCreate(filePath, content, version) {
|
|
401
498
|
if (!fs.existsSync(filePath)) {
|
|
402
499
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
@@ -432,16 +529,13 @@ function appendOrCreate(filePath, content, version) {
|
|
|
432
529
|
}
|
|
433
530
|
return "updated";
|
|
434
531
|
}
|
|
435
|
-
function mergeJsonConfig(filePath, mergePayload, version) {
|
|
436
|
-
|
|
437
|
-
if (
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
}
|
|
441
|
-
catch {
|
|
442
|
-
// Malformed JSON: overwrite
|
|
443
|
-
}
|
|
532
|
+
export function mergeJsonConfig(filePath, mergePayload, version) {
|
|
533
|
+
const read = readJsonConfigSafe(filePath);
|
|
534
|
+
if (!read.ok) {
|
|
535
|
+
warnUnparseable(filePath);
|
|
536
|
+
return "skipped";
|
|
444
537
|
}
|
|
538
|
+
const existing = read.data;
|
|
445
539
|
// Build the merged result first, then compare to avoid unnecessary writes.
|
|
446
540
|
const merged = { ...existing };
|
|
447
541
|
for (const [key, value] of Object.entries(mergePayload)) {
|
|
@@ -466,6 +560,8 @@ function mergeJsonConfig(filePath, mergePayload, version) {
|
|
|
466
560
|
if (mergedStr === existingStr)
|
|
467
561
|
return "skipped";
|
|
468
562
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
563
|
+
if (read.existed)
|
|
564
|
+
backupConfig(filePath);
|
|
469
565
|
fs.writeFileSync(filePath, mergedStr, "utf-8");
|
|
470
566
|
return "merged";
|
|
471
567
|
}
|
|
@@ -476,16 +572,13 @@ function mergeJsonConfig(filePath, mergePayload, version) {
|
|
|
476
572
|
* Write MCP config JSON for agents that use mcpServers format (Cursor, Windsurf).
|
|
477
573
|
* Returns true if a new entry was written.
|
|
478
574
|
*/
|
|
479
|
-
function writeMcpConfig(mcpConfigPath, binaryPath, alwaysAllow, mcpServerPath, workspaceRoot) {
|
|
480
|
-
|
|
481
|
-
if (
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
}
|
|
485
|
-
catch {
|
|
486
|
-
// Malformed JSON: overwrite
|
|
487
|
-
}
|
|
575
|
+
export function writeMcpConfig(mcpConfigPath, binaryPath, alwaysAllow, mcpServerPath, workspaceRoot) {
|
|
576
|
+
const read = readJsonConfigSafe(mcpConfigPath);
|
|
577
|
+
if (!read.ok) {
|
|
578
|
+
warnUnparseable(mcpConfigPath);
|
|
579
|
+
return false;
|
|
488
580
|
}
|
|
581
|
+
const existing = read.data;
|
|
489
582
|
const useNode = mcpServerPath && fs.existsSync(mcpServerPath);
|
|
490
583
|
const targetCmd = useNode ? "node" : binaryPath;
|
|
491
584
|
const targetArgs = useNode ? [mcpServerPath] : ["mcp"];
|
|
@@ -516,6 +609,8 @@ function writeMcpConfig(mcpConfigPath, binaryPath, alwaysAllow, mcpServerPath, w
|
|
|
516
609
|
};
|
|
517
610
|
existing.mcpServers = servers;
|
|
518
611
|
fs.mkdirSync(path.dirname(mcpConfigPath), { recursive: true });
|
|
612
|
+
if (read.existed)
|
|
613
|
+
backupConfig(mcpConfigPath);
|
|
519
614
|
fs.writeFileSync(mcpConfigPath, JSON.stringify(existing, null, 2), "utf-8");
|
|
520
615
|
return true;
|
|
521
616
|
}
|
|
@@ -610,16 +705,13 @@ function configureCodexGlobal(binaryPath, mcpServerPath, workspaceRoot) {
|
|
|
610
705
|
* Write .vscode/mcp.json for GitHub Copilot.
|
|
611
706
|
* VS Code uses "servers" (not "mcpServers") + "type": "stdio".
|
|
612
707
|
*/
|
|
613
|
-
function writeVsCodeMcpConfig(p, binaryPath, mcpServerPath, workspaceRoot) {
|
|
614
|
-
|
|
615
|
-
if (
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
}
|
|
619
|
-
catch {
|
|
620
|
-
// Malformed JSON: overwrite
|
|
621
|
-
}
|
|
708
|
+
export function writeVsCodeMcpConfig(p, binaryPath, mcpServerPath, workspaceRoot) {
|
|
709
|
+
const read = readJsonConfigSafe(p);
|
|
710
|
+
if (!read.ok) {
|
|
711
|
+
warnUnparseable(p);
|
|
712
|
+
return false;
|
|
622
713
|
}
|
|
714
|
+
const existing = read.data;
|
|
623
715
|
const useNode = mcpServerPath && fs.existsSync(mcpServerPath);
|
|
624
716
|
const targetCmd = useNode ? "node" : binaryPath;
|
|
625
717
|
const targetArgs = useNode ? [mcpServerPath] : ["mcp"];
|
|
@@ -644,22 +736,21 @@ function writeVsCodeMcpConfig(p, binaryPath, mcpServerPath, workspaceRoot) {
|
|
|
644
736
|
};
|
|
645
737
|
existing.servers = servers;
|
|
646
738
|
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
739
|
+
if (read.existed)
|
|
740
|
+
backupConfig(p);
|
|
647
741
|
fs.writeFileSync(p, JSON.stringify(existing, null, 2), "utf-8");
|
|
648
742
|
return true;
|
|
649
743
|
}
|
|
650
744
|
/**
|
|
651
745
|
* Write context_servers in .zed/settings.json for Zed.
|
|
652
746
|
*/
|
|
653
|
-
function writeZedMcpConfig(p, binaryPath, mcpServerPath, workspaceRoot) {
|
|
654
|
-
|
|
655
|
-
if (
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
}
|
|
659
|
-
catch {
|
|
660
|
-
// Malformed JSON: overwrite
|
|
661
|
-
}
|
|
747
|
+
export function writeZedMcpConfig(p, binaryPath, mcpServerPath, workspaceRoot) {
|
|
748
|
+
const read = readJsonConfigSafe(p);
|
|
749
|
+
if (!read.ok) {
|
|
750
|
+
warnUnparseable(p);
|
|
751
|
+
return false;
|
|
662
752
|
}
|
|
753
|
+
const settings = read.data;
|
|
663
754
|
const useNode = mcpServerPath && fs.existsSync(mcpServerPath);
|
|
664
755
|
const targetCmd = useNode ? "node" : binaryPath;
|
|
665
756
|
const targetArgs = useNode ? [mcpServerPath] : ["mcp"];
|
|
@@ -693,6 +784,8 @@ function writeZedMcpConfig(p, binaryPath, mcpServerPath, workspaceRoot) {
|
|
|
693
784
|
}
|
|
694
785
|
settings.agent = { ...agent, tool_permissions: { ...tp, tools } };
|
|
695
786
|
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
787
|
+
if (read.existed)
|
|
788
|
+
backupConfig(p);
|
|
696
789
|
fs.writeFileSync(p, JSON.stringify(settings, null, 2), "utf-8");
|
|
697
790
|
return true;
|
|
698
791
|
}
|
|
@@ -700,20 +793,17 @@ function writeZedMcpConfig(p, binaryPath, mcpServerPath, workspaceRoot) {
|
|
|
700
793
|
* Configure vexp MCP in ~/.claude.json (user-scope) using the Rust binary.
|
|
701
794
|
* Returns true if config was written/updated.
|
|
702
795
|
*/
|
|
703
|
-
function configureClaudeCodeGlobal(binaryPath, mcpServerPath, workspaceRoot) {
|
|
796
|
+
export function configureClaudeCodeGlobal(binaryPath, mcpServerPath, workspaceRoot) {
|
|
704
797
|
const home = os.homedir();
|
|
705
798
|
if (!home)
|
|
706
799
|
return false;
|
|
707
800
|
const configPath = path.join(home, ".claude.json");
|
|
708
|
-
|
|
709
|
-
if (
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
}
|
|
713
|
-
catch {
|
|
714
|
-
// Malformed JSON: overwrite
|
|
715
|
-
}
|
|
801
|
+
const read = readJsonConfigSafe(configPath);
|
|
802
|
+
if (!read.ok) {
|
|
803
|
+
warnUnparseable(configPath);
|
|
804
|
+
return false;
|
|
716
805
|
}
|
|
806
|
+
const config = read.data;
|
|
717
807
|
const beforeServers = config.mcpServers;
|
|
718
808
|
const existing = beforeServers?.["vexp"];
|
|
719
809
|
const useNode = mcpServerPath && fs.existsSync(mcpServerPath);
|
|
@@ -735,6 +825,8 @@ function configureClaudeCodeGlobal(binaryPath, mcpServerPath, workspaceRoot) {
|
|
|
735
825
|
...(desiredEnv ? { env: desiredEnv } : {}),
|
|
736
826
|
};
|
|
737
827
|
config.mcpServers = servers;
|
|
828
|
+
if (read.existed)
|
|
829
|
+
backupConfig(configPath);
|
|
738
830
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
739
831
|
return true;
|
|
740
832
|
}
|
|
@@ -746,7 +838,7 @@ function configureClaudeCodeGlobal(binaryPath, mcpServerPath, workspaceRoot) {
|
|
|
746
838
|
* Creates .claude/hooks/vexp-guard.sh and merges hook config into .claude/settings.json.
|
|
747
839
|
* Returns the action taken, or null if nothing was written.
|
|
748
840
|
*/
|
|
749
|
-
function installClaudeCodeHook(workspaceRoot) {
|
|
841
|
+
export function installClaudeCodeHook(workspaceRoot) {
|
|
750
842
|
const hookDir = path.join(workspaceRoot, ".claude", "hooks");
|
|
751
843
|
const hookPath = path.join(hookDir, "vexp-guard.sh");
|
|
752
844
|
const settingsPath = path.join(workspaceRoot, ".claude", "settings.json");
|
|
@@ -760,15 +852,12 @@ function installClaudeCodeHook(workspaceRoot) {
|
|
|
760
852
|
}
|
|
761
853
|
fs.writeFileSync(hookPath, VEXP_GUARD_HOOK, { mode: 0o755 });
|
|
762
854
|
// 2. Merge hook config into .claude/settings.json
|
|
763
|
-
|
|
764
|
-
if (
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
}
|
|
768
|
-
catch {
|
|
769
|
-
// Malformed JSON: overwrite
|
|
770
|
-
}
|
|
855
|
+
const read = readJsonConfigSafe(settingsPath);
|
|
856
|
+
if (!read.ok) {
|
|
857
|
+
warnUnparseable(settingsPath);
|
|
858
|
+
return existed ? "updated" : "created";
|
|
771
859
|
}
|
|
860
|
+
const settings = read.data;
|
|
772
861
|
const hooks = (settings.hooks ?? {});
|
|
773
862
|
const existingPreToolUse = Array.isArray(hooks.PreToolUse) ? hooks.PreToolUse : [];
|
|
774
863
|
// Aggressively remove ALL vexp-related hook entries (old format, stale matchers, malformed)
|
|
@@ -805,6 +894,8 @@ function installClaudeCodeHook(workspaceRoot) {
|
|
|
805
894
|
],
|
|
806
895
|
});
|
|
807
896
|
settings.hooks = { ...hooks, PreToolUse: filtered };
|
|
897
|
+
if (read.existed)
|
|
898
|
+
backupConfig(settingsPath);
|
|
808
899
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
809
900
|
return existed ? "updated" : "created";
|
|
810
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
|
}
|