vexp-cli 2.0.11 → 2.0.12
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 +154 -75
- package/dist/autostart.js +375 -0
- package/dist/binary.js +63 -0
- package/dist/cli.js +258 -105
- package/dist/mcp-supervisor.js +235 -0
- package/dist/serve.js +160 -0
- package/dist/trace.js +80 -0
- package/mcp/mcp-server.cjs +38 -38
- package/package.json +6 -6
package/dist/agent-config.js
CHANGED
|
@@ -16,6 +16,66 @@ import { VEXP_GUARD_HOOK } from "./hook-template.js";
|
|
|
16
16
|
// ---------------------------------------------------------------------------
|
|
17
17
|
const VEXP_MARKER_RE = /<!-- vexp(?:\s+v[\d.]+)? -->/;
|
|
18
18
|
const VEXP_MARKER_END = "<!-- /vexp -->";
|
|
19
|
+
// Matches LEGACY vexp entry keys ONLY — "vexp-mcp", "vexp-core", "vexp-stdio",
|
|
20
|
+
// "vexp_old", etc. Explicitly excludes the canonical "vexp" key so that idempotency
|
|
21
|
+
// checks (command+args+env identical?) work: we only remove siblings, not the entry
|
|
22
|
+
// we're about to upsert.
|
|
23
|
+
const LEGACY_VEXP_KEY_RE = /^vexp[_-][a-z0-9_-]+$/i;
|
|
24
|
+
// Matches TOML sections for LEGACY vexp keys only: [mcp_servers.vexp-mcp],
|
|
25
|
+
// [mcp_servers.vexp_old], etc. Explicitly excludes the canonical
|
|
26
|
+
// [mcp_servers.vexp] to preserve idempotency.
|
|
27
|
+
// Stops at the next top-level header (`\n[` followed by a letter/underscore)
|
|
28
|
+
// so that array-valued TOML keys like `args = ["--legacy"]` do not prematurely
|
|
29
|
+
// terminate the section.
|
|
30
|
+
const LEGACY_VEXP_TOML_SECTION_RE = /\n?\[mcp_servers\.vexp[_-][a-z0-9_-]+(?:\.[a-z_]+)?\][\s\S]*?(?=\n\[[A-Za-z_]|$)/gi;
|
|
31
|
+
// Matches the canonical [mcp_servers.vexp] section (and its .http_headers
|
|
32
|
+
// sub-section), used when we need to overwrite it with new URL/token.
|
|
33
|
+
const CANONICAL_VEXP_TOML_SECTION_RE = /\n?\[mcp_servers\.vexp(?:\.[a-z_]+)?\][\s\S]*?(?=\n\[[A-Za-z_]|$)/gi;
|
|
34
|
+
/**
|
|
35
|
+
* FNV-1a 64-bit hash (first 8 hex chars) of the workspace root.
|
|
36
|
+
* Duplicates the helper in packages/vexp-mcp/src/daemon-client.ts:fnvHash
|
|
37
|
+
* to avoid a runtime dependency from the CLI on the MCP package.
|
|
38
|
+
* MUST stay in lockstep with that function.
|
|
39
|
+
*/
|
|
40
|
+
function workspaceHash(workspaceRoot) {
|
|
41
|
+
const input = workspaceRoot.toLowerCase();
|
|
42
|
+
let hash = BigInt("0xcbf29ce484222325");
|
|
43
|
+
const prime = BigInt("0x100000001b3");
|
|
44
|
+
const mask = BigInt("0xffffffffffffffff");
|
|
45
|
+
const bytes = Buffer.from(input, "utf-8");
|
|
46
|
+
for (const byte of bytes) {
|
|
47
|
+
hash ^= BigInt(byte);
|
|
48
|
+
hash = (hash * prime) & mask;
|
|
49
|
+
}
|
|
50
|
+
return hash.toString(16).padStart(16, "0").slice(0, 8);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Remove every legacy vexp entry from a JSON-shaped MCP config object.
|
|
54
|
+
* `key` selects which container to scrub: "mcpServers" (Claude Code,
|
|
55
|
+
* Cursor, Windsurf), "servers" (.vscode/mcp.json), "context_servers" (Zed).
|
|
56
|
+
* Returns the list of removed keys for logging.
|
|
57
|
+
*/
|
|
58
|
+
function stripLegacyVexpEntries(config, key) {
|
|
59
|
+
const container = config[key];
|
|
60
|
+
if (!container || typeof container !== "object")
|
|
61
|
+
return [];
|
|
62
|
+
const removed = [];
|
|
63
|
+
for (const k of Object.keys(container)) {
|
|
64
|
+
if (LEGACY_VEXP_KEY_RE.test(k)) {
|
|
65
|
+
delete container[k];
|
|
66
|
+
removed.push(`${key}.${k}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return removed;
|
|
70
|
+
}
|
|
71
|
+
/** Emit a one-line removal notice to stdout (chalk-less to avoid churn). */
|
|
72
|
+
function logRemoval(removed) {
|
|
73
|
+
if (removed.length === 0)
|
|
74
|
+
return;
|
|
75
|
+
for (const entry of removed) {
|
|
76
|
+
process.stdout.write(` · Removed legacy entry: ${entry}\n`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
19
79
|
// All vexp MCP tools (all read-only — safe to pre-approve).
|
|
20
80
|
const VEXP_TOOLS = [
|
|
21
81
|
"run_pipeline",
|
|
@@ -426,29 +486,35 @@ function writeMcpConfig(mcpConfigPath, binaryPath, alwaysAllow, mcpServerPath, w
|
|
|
426
486
|
// Malformed JSON: overwrite
|
|
427
487
|
}
|
|
428
488
|
}
|
|
429
|
-
const servers = existing.mcpServers;
|
|
430
489
|
const useNode = mcpServerPath && fs.existsSync(mcpServerPath);
|
|
431
490
|
const targetCmd = useNode ? "node" : binaryPath;
|
|
432
491
|
const targetArgs = useNode ? [mcpServerPath] : ["mcp"];
|
|
433
492
|
const targetEnv = workspaceRoot ? { VEXP_WORKSPACE: workspaceRoot } : undefined;
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
493
|
+
const beforeServers = existing.mcpServers;
|
|
494
|
+
const previousVexp = beforeServers?.["vexp"];
|
|
495
|
+
const currentCmd = previousVexp?.["command"];
|
|
496
|
+
const prevArgs = Array.isArray(previousVexp?.["args"]) ? previousVexp?.["args"] : undefined;
|
|
497
|
+
const prevEnv = previousVexp?.env;
|
|
498
|
+
const identical = currentCmd === targetCmd &&
|
|
499
|
+
prevArgs !== undefined &&
|
|
500
|
+
JSON.stringify(prevArgs) === JSON.stringify(targetArgs) &&
|
|
501
|
+
JSON.stringify(prevEnv) === JSON.stringify(targetEnv);
|
|
502
|
+
// Legacy cleanup: remove any vexp/vexp-* keys; if the canonical 'vexp'
|
|
503
|
+
// entry points at a command path that no longer exists on disk, drop it
|
|
504
|
+
// too so we don't keep a stale pointer.
|
|
505
|
+
const removed = stripLegacyVexpEntries(existing, "mcpServers");
|
|
506
|
+
logRemoval(removed);
|
|
507
|
+
if (identical && removed.length === 0) {
|
|
508
|
+
return false; // Already up to date and no legacy to clean
|
|
442
509
|
}
|
|
443
|
-
existing.mcpServers
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
...(alwaysAllow && alwaysAllow.length > 0 ? { alwaysAllow } : {}),
|
|
450
|
-
},
|
|
510
|
+
const servers = existing.mcpServers ?? {};
|
|
511
|
+
servers["vexp"] = {
|
|
512
|
+
command: targetCmd,
|
|
513
|
+
args: targetArgs,
|
|
514
|
+
...(targetEnv ? { env: targetEnv } : {}),
|
|
515
|
+
...(alwaysAllow && alwaysAllow.length > 0 ? { alwaysAllow } : {}),
|
|
451
516
|
};
|
|
517
|
+
existing.mcpServers = servers;
|
|
452
518
|
fs.mkdirSync(path.dirname(mcpConfigPath), { recursive: true });
|
|
453
519
|
fs.writeFileSync(mcpConfigPath, JSON.stringify(existing, null, 2), "utf-8");
|
|
454
520
|
return true;
|
|
@@ -508,20 +574,34 @@ function configureCodexGlobal(binaryPath, mcpServerPath, workspaceRoot) {
|
|
|
508
574
|
const configPath = path.join(home, ".codex", "config.toml");
|
|
509
575
|
const mcpPort = readMcpPort(workspaceRoot);
|
|
510
576
|
const token = resolveCodexToken();
|
|
511
|
-
|
|
577
|
+
// Per-workspace URL path — lets the single MCP server on :port route
|
|
578
|
+
// each codex request to the correct daemon in multi-workspace setups.
|
|
579
|
+
// Falls back to /mcp if no workspace is known (e.g. global bootstrap).
|
|
580
|
+
const wsHash = workspaceRoot ? workspaceHash(workspaceRoot) : "";
|
|
581
|
+
const urlPath = wsHash ? `/ws/${wsHash}/mcp` : "/mcp";
|
|
582
|
+
const desiredUrl = `http://127.0.0.1:${mcpPort}${urlPath}`;
|
|
583
|
+
const newSection = `\n[mcp_servers.vexp]\nurl = "${desiredUrl}"\ntool_timeout_sec = 120\n\n[mcp_servers.vexp.http_headers]\nAuthorization = "Bearer ${token}"\n`;
|
|
512
584
|
let content = "";
|
|
513
585
|
if (fs.existsSync(configPath)) {
|
|
514
586
|
content = fs.readFileSync(configPath, "utf-8");
|
|
515
587
|
}
|
|
516
588
|
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
content = content.replace(/\n?\[mcp_servers\.vexp(?:\.http_headers)?\][^\[]*/g, "");
|
|
589
|
+
// Step 1: strip LEGACY vexp-* sections (not the canonical one).
|
|
590
|
+
const beforeLegacy = content.length;
|
|
591
|
+
content = content.replace(LEGACY_VEXP_TOML_SECTION_RE, "");
|
|
592
|
+
const legacyStripped = content.length !== beforeLegacy;
|
|
593
|
+
if (legacyStripped) {
|
|
594
|
+
process.stdout.write(` · Removed legacy codex entries from ${configPath}\n`);
|
|
524
595
|
}
|
|
596
|
+
// Step 2: check if the canonical section already has the desired URL+token.
|
|
597
|
+
// If yes and no legacy was stripped, this is a true no-op.
|
|
598
|
+
const canonicalOk = content.includes(`url = "${desiredUrl}"`) &&
|
|
599
|
+
content.includes(`Authorization = "Bearer ${token}"`);
|
|
600
|
+
if (canonicalOk && !legacyStripped) {
|
|
601
|
+
return false;
|
|
602
|
+
}
|
|
603
|
+
// Step 3: rewrite canonical section (strip it if present, then append).
|
|
604
|
+
content = content.replace(CANONICAL_VEXP_TOML_SECTION_RE, "");
|
|
525
605
|
content += newSection;
|
|
526
606
|
fs.writeFileSync(configPath, content, "utf-8");
|
|
527
607
|
return true;
|
|
@@ -540,29 +620,29 @@ function writeVsCodeMcpConfig(p, binaryPath, mcpServerPath, workspaceRoot) {
|
|
|
540
620
|
// Malformed JSON: overwrite
|
|
541
621
|
}
|
|
542
622
|
}
|
|
543
|
-
const servers = existing.servers;
|
|
544
623
|
const useNode = mcpServerPath && fs.existsSync(mcpServerPath);
|
|
545
624
|
const targetCmd = useNode ? "node" : binaryPath;
|
|
546
625
|
const targetArgs = useNode ? [mcpServerPath] : ["mcp"];
|
|
547
626
|
const targetEnv = workspaceRoot ? { VEXP_WORKSPACE: workspaceRoot } : undefined;
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
},
|
|
627
|
+
const beforeServers = existing.servers;
|
|
628
|
+
const previousVexp = beforeServers?.["vexp"];
|
|
629
|
+
const prevArgs = Array.isArray(previousVexp?.["args"]) ? previousVexp?.["args"] : undefined;
|
|
630
|
+
const identical = previousVexp?.["command"] === targetCmd &&
|
|
631
|
+
prevArgs !== undefined &&
|
|
632
|
+
JSON.stringify(prevArgs) === JSON.stringify(targetArgs) &&
|
|
633
|
+
JSON.stringify(previousVexp?.env) === JSON.stringify(targetEnv);
|
|
634
|
+
const removed = stripLegacyVexpEntries(existing, "servers");
|
|
635
|
+
logRemoval(removed);
|
|
636
|
+
if (identical && removed.length === 0)
|
|
637
|
+
return false;
|
|
638
|
+
const servers = existing.servers ?? {};
|
|
639
|
+
servers["vexp"] = {
|
|
640
|
+
type: "stdio",
|
|
641
|
+
command: targetCmd,
|
|
642
|
+
args: targetArgs,
|
|
643
|
+
...(targetEnv ? { env: targetEnv } : {}),
|
|
565
644
|
};
|
|
645
|
+
existing.servers = servers;
|
|
566
646
|
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
567
647
|
fs.writeFileSync(p, JSON.stringify(existing, null, 2), "utf-8");
|
|
568
648
|
return true;
|
|
@@ -580,31 +660,30 @@ function writeZedMcpConfig(p, binaryPath, mcpServerPath, workspaceRoot) {
|
|
|
580
660
|
// Malformed JSON: overwrite
|
|
581
661
|
}
|
|
582
662
|
}
|
|
583
|
-
const cs = settings.context_servers;
|
|
584
663
|
const useNode = mcpServerPath && fs.existsSync(mcpServerPath);
|
|
585
664
|
const targetCmd = useNode ? "node" : binaryPath;
|
|
586
665
|
const targetArgs = useNode ? [mcpServerPath] : ["mcp"];
|
|
587
666
|
const targetEnv = workspaceRoot ? { VEXP_WORKSPACE: workspaceRoot } : undefined;
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
},
|
|
667
|
+
const beforeCs = settings.context_servers;
|
|
668
|
+
const previousVexp = beforeCs?.["vexp"];
|
|
669
|
+
const curCmd = previousVexp?.["command"];
|
|
670
|
+
const identical = curCmd?.["path"] === targetCmd &&
|
|
671
|
+
Array.isArray(curCmd?.["args"]) &&
|
|
672
|
+
JSON.stringify(curCmd["args"]) === JSON.stringify(targetArgs) &&
|
|
673
|
+
JSON.stringify(curCmd["env"]) === JSON.stringify(targetEnv);
|
|
674
|
+
const removed = stripLegacyVexpEntries(settings, "context_servers");
|
|
675
|
+
logRemoval(removed);
|
|
676
|
+
if (identical && removed.length === 0)
|
|
677
|
+
return false;
|
|
678
|
+
const cs = settings.context_servers ?? {};
|
|
679
|
+
cs["vexp"] = {
|
|
680
|
+
command: {
|
|
681
|
+
path: targetCmd,
|
|
682
|
+
args: targetArgs,
|
|
683
|
+
...(targetEnv ? { env: targetEnv } : {}),
|
|
606
684
|
},
|
|
607
685
|
};
|
|
686
|
+
settings.context_servers = cs;
|
|
608
687
|
// Pre-approve all vexp tools
|
|
609
688
|
const agent = (settings.agent ?? {});
|
|
610
689
|
const tp = (agent.tool_permissions ?? {});
|
|
@@ -635,27 +714,27 @@ function configureClaudeCodeGlobal(binaryPath, mcpServerPath, workspaceRoot) {
|
|
|
635
714
|
// Malformed JSON: overwrite
|
|
636
715
|
}
|
|
637
716
|
}
|
|
638
|
-
const
|
|
639
|
-
const existing =
|
|
717
|
+
const beforeServers = config.mcpServers;
|
|
718
|
+
const existing = beforeServers?.["vexp"];
|
|
640
719
|
const useNode = mcpServerPath && fs.existsSync(mcpServerPath);
|
|
641
720
|
const desiredCommand = useNode ? "node" : binaryPath;
|
|
642
721
|
const desiredArgs = useNode ? [mcpServerPath] : ["mcp"];
|
|
643
722
|
const desiredEnv = workspaceRoot ? { VEXP_WORKSPACE: workspaceRoot } : undefined;
|
|
644
|
-
|
|
645
|
-
if (existing?.["command"] === desiredCommand &&
|
|
723
|
+
const identical = existing?.["command"] === desiredCommand &&
|
|
646
724
|
Array.isArray(existing?.["args"]) &&
|
|
647
725
|
JSON.stringify(existing["args"]) === JSON.stringify(desiredArgs) &&
|
|
648
|
-
JSON.stringify(existing
|
|
726
|
+
JSON.stringify(existing?.env) === JSON.stringify(desiredEnv);
|
|
727
|
+
const removed = stripLegacyVexpEntries(config, "mcpServers");
|
|
728
|
+
logRemoval(removed);
|
|
729
|
+
if (identical && removed.length === 0)
|
|
649
730
|
return false;
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
args: desiredArgs,
|
|
656
|
-
...(desiredEnv ? { env: desiredEnv } : {}),
|
|
657
|
-
},
|
|
731
|
+
const servers = config.mcpServers ?? {};
|
|
732
|
+
servers["vexp"] = {
|
|
733
|
+
command: desiredCommand,
|
|
734
|
+
args: desiredArgs,
|
|
735
|
+
...(desiredEnv ? { env: desiredEnv } : {}),
|
|
658
736
|
};
|
|
737
|
+
config.mcpServers = servers;
|
|
659
738
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
660
739
|
return true;
|
|
661
740
|
}
|