vexp-cli 3.1.2 → 3.2.0
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/README.md +1 -1
- package/dist/agent-config.js +213 -1
- package/dist/cli.js +11 -2
- package/dist/doctor.js +307 -12
- package/dist/mcp-supervisor.js +36 -1
- package/dist/serve.js +8 -0
- package/dist/slow-mount.js +81 -0
- package/dist/socket-path.js +24 -0
- package/mcp/mcp-server.cjs +6 -6
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -62,7 +62,7 @@ vexp doctor
|
|
|
62
62
|
|
|
63
63
|
## Supported agents
|
|
64
64
|
|
|
65
|
-
Claude Code · Cursor · Windsurf · GitHub Copilot · OpenAI Codex · Zed · Continue.dev · Cline · Aider · Kiro · Kilo Code · opencode · Augment · Antigravity
|
|
65
|
+
Claude Code · Cursor · Windsurf · GitHub Copilot · OpenAI Codex · Zed · Continue.dev · Cline · Aider · Kiro · Kilo Code · opencode · Augment · Antigravity · ZCode · Friday Code
|
|
66
66
|
|
|
67
67
|
## Supported languages
|
|
68
68
|
|
package/dist/agent-config.js
CHANGED
|
@@ -246,6 +246,29 @@ const AGENT_DETECTORS = [
|
|
|
246
246
|
// MCP is machine-global (~/.gemini/antigravity/mcp_config.json) — handled
|
|
247
247
|
// by configureAntigravityGlobal(), not via mcpConfigFile.
|
|
248
248
|
},
|
|
249
|
+
{
|
|
250
|
+
// ZCode (zcode.z.ai), Z.ai's desktop agentic IDE. It reads the
|
|
251
|
+
// cross-tool AGENTS.md; CLAUDE.md is a one-time onboarding import it
|
|
252
|
+
// never reads at runtime. MCP is `mcp.servers` inside .zcode/config.json
|
|
253
|
+
// — written by configureZcodeMcp(), not via mcpConfigFile, because the
|
|
254
|
+
// shape is not `mcpServers`. Hooks are user-level only; see the ZCode
|
|
255
|
+
// section below.
|
|
256
|
+
agent: "ZCode",
|
|
257
|
+
detectPath: ".zcode",
|
|
258
|
+
configFile: "AGENTS.md",
|
|
259
|
+
templateName: "agents-md",
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
// Friday Code (tryfriday.ai): reads `friday.md` at the project root — the
|
|
263
|
+
// same mandate every AGENTS.md agent gets, under a name of its own. It
|
|
264
|
+
// connects to vexp on its side, so there is no MCP file to write here:
|
|
265
|
+
// the instructions are the whole integration. Detected by the file
|
|
266
|
+
// itself, as Codex is by AGENTS.md.
|
|
267
|
+
agent: "Friday Code",
|
|
268
|
+
detectPath: "friday.md",
|
|
269
|
+
configFile: "friday.md",
|
|
270
|
+
templateName: "agents-md",
|
|
271
|
+
},
|
|
249
272
|
];
|
|
250
273
|
// ---------------------------------------------------------------------------
|
|
251
274
|
// Public API
|
|
@@ -320,6 +343,10 @@ export function plannedWrites(agent, guard = guardMode(), interventions = interv
|
|
|
320
343
|
case "GitHub Copilot":
|
|
321
344
|
out.push(".vscode/mcp.json (MCP server entry)");
|
|
322
345
|
break;
|
|
346
|
+
case "ZCode":
|
|
347
|
+
out.push(".zcode/config.json `mcp.servers` (MCP server entry; .agents/mcp.json instead when the project keeps its servers there)");
|
|
348
|
+
out.push("~/.zcode/cli/config.json hooks.events.UserPromptSubmit (orientation, user scope: ZCode runs no project hooks)");
|
|
349
|
+
break;
|
|
323
350
|
default:
|
|
324
351
|
break;
|
|
325
352
|
}
|
|
@@ -507,6 +534,24 @@ export function configureAgents(workspaceRoot, binaryPath, version, agentFilter,
|
|
|
507
534
|
if (wrote)
|
|
508
535
|
mcpConfigs.push("~/.gemini/antigravity/mcp_config.json");
|
|
509
536
|
}
|
|
537
|
+
// ZCode: `mcp.servers` in .zcode/config.json (or the .agents/mcp.json
|
|
538
|
+
// compatibility file when that is what the project uses) + the
|
|
539
|
+
// orientation hook in ZCode's USER config — see configureZcodeMcp.
|
|
540
|
+
if (detector.agent === "ZCode") {
|
|
541
|
+
const wrote = configureZcodeMcp(workspaceRoot, binaryPath);
|
|
542
|
+
if (wrote)
|
|
543
|
+
mcpConfigs.push(wrote);
|
|
544
|
+
const hint = installZcodeHintHook(binaryPath);
|
|
545
|
+
if (hint) {
|
|
546
|
+
results.push({
|
|
547
|
+
agent: "ZCode Hint",
|
|
548
|
+
configFile: "~/.zcode/cli/config.json",
|
|
549
|
+
content: "",
|
|
550
|
+
alreadyExists: hint === "updated",
|
|
551
|
+
action: hint,
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
}
|
|
510
555
|
// GitHub Copilot: VS Code format (.vscode/mcp.json with "servers" key)
|
|
511
556
|
if (detector.agent === "GitHub Copilot") {
|
|
512
557
|
const mcpPath = path.join(workspaceRoot, ".vscode/mcp.json");
|
|
@@ -831,6 +876,12 @@ export function configureSelectedAgents(workspaceRoot, binaryPath, version, sele
|
|
|
831
876
|
if (wrote)
|
|
832
877
|
mcpConfigs.push("~/.gemini/antigravity/mcp_config.json");
|
|
833
878
|
}
|
|
879
|
+
if (detector.agent === "ZCode") {
|
|
880
|
+
const wrote = configureZcodeMcp(workspaceRoot, binaryPath);
|
|
881
|
+
if (wrote)
|
|
882
|
+
mcpConfigs.push(wrote);
|
|
883
|
+
installZcodeHintHook(binaryPath);
|
|
884
|
+
}
|
|
834
885
|
if (detector.agent === "GitHub Copilot") {
|
|
835
886
|
const mcpPath = path.join(workspaceRoot, ".vscode/mcp.json");
|
|
836
887
|
if (writeVsCodeMcpConfig(mcpPath, binaryPath, mcpServerPath, workspaceRoot))
|
|
@@ -1150,7 +1201,7 @@ export function isEditorExecutable(entry) {
|
|
|
1150
1201
|
// recognised when read from WSL/CI too (path.basename splits only on the
|
|
1151
1202
|
// host's separator).
|
|
1152
1203
|
const base = (command.split(/[\\/]/).pop() ?? "").toLowerCase().replace(/\.exe$/, "");
|
|
1153
|
-
return ["code", "code-insiders", "code-oss", "cursor", "windsurf", "windsurf-next", "trae", "kiro", "zed", "antigravity", "electron", "devin"].includes(base);
|
|
1204
|
+
return ["code", "code-insiders", "code-oss", "cursor", "windsurf", "windsurf-next", "trae", "kiro", "zed", "antigravity", "electron", "devin", "zcode"].includes(base);
|
|
1154
1205
|
}
|
|
1155
1206
|
export function isBareInterpreter(entry) {
|
|
1156
1207
|
if (!entry || typeof entry !== "object")
|
|
@@ -1688,6 +1739,164 @@ approveKey = "alwaysAllow") {
|
|
|
1688
1739
|
fs.writeFileSync(mcpConfigPath, JSON.stringify(existing, null, 2), "utf-8");
|
|
1689
1740
|
return true;
|
|
1690
1741
|
}
|
|
1742
|
+
// ---------------------------------------------------------------------------
|
|
1743
|
+
// ZCode (zcode.z.ai) — Z.ai's desktop agentic IDE, GLM-powered.
|
|
1744
|
+
//
|
|
1745
|
+
// Three facts from its docs (v3.11) shape everything below:
|
|
1746
|
+
//
|
|
1747
|
+
// 1. Instructions: it reads the cross-tool AGENTS.md at the workspace root
|
|
1748
|
+
// (plus ~/.zcode/AGENTS.md). CLAUDE.md is a one-time onboarding import,
|
|
1749
|
+
// never read at runtime — hence the agents-md template, like Codex.
|
|
1750
|
+
// 2. MCP: `mcp.servers.<name>` inside `<root>/.zcode/config.json`, with
|
|
1751
|
+
// `<root>/.agents/mcp.json` (`mcpServers` shape) as a compatibility
|
|
1752
|
+
// fallback read ONLY while no .zcode config exists. Creating the native
|
|
1753
|
+
// file therefore silently unplugs every server a project keeps in the
|
|
1754
|
+
// compatibility one — see zcodeMcpTarget.
|
|
1755
|
+
// 3. Hooks: user-level only (~/.zcode/cli/config.json); a `hooks` block in
|
|
1756
|
+
// the workspace config is ignored by design. Payload and reply are
|
|
1757
|
+
// Claude Code's (stdin JSON with `prompt` and `cwd`, stdout
|
|
1758
|
+
// `hookSpecificOutput.additionalContext`), so `vexp-core prompt-hint`
|
|
1759
|
+
// runs unchanged — as a `process` hook, argv without a shell, which
|
|
1760
|
+
// sidesteps the bash-vs-cmd quoting every other hook here carries.
|
|
1761
|
+
//
|
|
1762
|
+
// It is a GUI app launched from a dock or Start menu, so the MCP command is
|
|
1763
|
+
// the vexp binary itself, never `node`: the Kiro lesson, where a
|
|
1764
|
+
// PATH-resolved `node` was not found and the server silently never started.
|
|
1765
|
+
// ---------------------------------------------------------------------------
|
|
1766
|
+
/**
|
|
1767
|
+
* Where ZCode's project MCP entry goes: the native file, unless the project
|
|
1768
|
+
* has none and keeps its servers in the `.agents/mcp.json` compatibility
|
|
1769
|
+
* file. ZCode stops reading that file the moment `.zcode/config.json`
|
|
1770
|
+
* exists, so writing the native one there would disconnect the user's other
|
|
1771
|
+
* servers in order to connect ours.
|
|
1772
|
+
*/
|
|
1773
|
+
export function zcodeMcpTarget(workspaceRoot) {
|
|
1774
|
+
const native = path.join(workspaceRoot, ".zcode", "config.json");
|
|
1775
|
+
if (fs.existsSync(native))
|
|
1776
|
+
return native;
|
|
1777
|
+
const compat = path.join(workspaceRoot, ".agents", "mcp.json");
|
|
1778
|
+
const read = readJsonConfigSafe(compat);
|
|
1779
|
+
if (read.existed && read.ok) {
|
|
1780
|
+
const servers = read.data.mcpServers;
|
|
1781
|
+
if (servers && typeof servers === "object" && Object.keys(servers).length > 0)
|
|
1782
|
+
return compat;
|
|
1783
|
+
}
|
|
1784
|
+
return native;
|
|
1785
|
+
}
|
|
1786
|
+
/**
|
|
1787
|
+
* Register vexp with ZCode. Returns the workspace-relative path written, or
|
|
1788
|
+
* null when nothing changed: already registered, a working entry adopted, or
|
|
1789
|
+
* a file that would not parse (reported through warnUnparseable, never
|
|
1790
|
+
* overwritten).
|
|
1791
|
+
*/
|
|
1792
|
+
export function configureZcodeMcp(workspaceRoot, binaryPath) {
|
|
1793
|
+
const target = zcodeMcpTarget(workspaceRoot);
|
|
1794
|
+
const rel = path.relative(workspaceRoot, target).replace(/\\/g, "/");
|
|
1795
|
+
if (rel === ".agents/mcp.json") {
|
|
1796
|
+
// The compatibility file has the mcpServers shape the shared writer
|
|
1797
|
+
// knows; withholding mcpServerPath selects the binary transport.
|
|
1798
|
+
return writeMcpConfig(target, binaryPath, undefined, undefined, workspaceRoot) ? rel : null;
|
|
1799
|
+
}
|
|
1800
|
+
const read = readJsonConfigSafe(target);
|
|
1801
|
+
if (!read.ok) {
|
|
1802
|
+
warnUnparseable(target);
|
|
1803
|
+
return null;
|
|
1804
|
+
}
|
|
1805
|
+
const cfg = read.data;
|
|
1806
|
+
const mcp = (cfg.mcp && typeof cfg.mcp === "object" ? cfg.mcp : {});
|
|
1807
|
+
const servers = (mcp.servers && typeof mcp.servers === "object" ? mcp.servers : {});
|
|
1808
|
+
const targetArgs = ["mcp"];
|
|
1809
|
+
const targetEnv = { VEXP_WORKSPACE: workspaceRoot };
|
|
1810
|
+
const prev = servers["vexp"];
|
|
1811
|
+
const envMatches = JSON.stringify(prev?.env) === JSON.stringify(targetEnv);
|
|
1812
|
+
const identical = prev?.command === binaryPath &&
|
|
1813
|
+
JSON.stringify(prev?.args) === JSON.stringify(targetArgs) &&
|
|
1814
|
+
envMatches;
|
|
1815
|
+
if (identical)
|
|
1816
|
+
return null;
|
|
1817
|
+
// Another vexp install already left a working entry here — don't fight it.
|
|
1818
|
+
// The VEXP_WORKSPACE pin still gates, so a moved project is repinned.
|
|
1819
|
+
if (envMatches && adoptableEntry(prev))
|
|
1820
|
+
return null;
|
|
1821
|
+
servers["vexp"] = { command: binaryPath, args: targetArgs, env: targetEnv };
|
|
1822
|
+
mcp.servers = servers;
|
|
1823
|
+
cfg.mcp = mcp;
|
|
1824
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
1825
|
+
if (read.existed)
|
|
1826
|
+
backupConfig(target);
|
|
1827
|
+
fs.writeFileSync(target, JSON.stringify(cfg, null, 2) + "\n", "utf-8");
|
|
1828
|
+
return rel;
|
|
1829
|
+
}
|
|
1830
|
+
/** ZCode's user-level config — the only place it runs hooks from. */
|
|
1831
|
+
export function zcodeUserConfigPath() {
|
|
1832
|
+
return path.join(os.homedir(), ".zcode", "cli", "config.json");
|
|
1833
|
+
}
|
|
1834
|
+
/** Does this UserPromptSubmit entry belong to us? Ours runs the vexp binary's `prompt-hint`. */
|
|
1835
|
+
function isVexpZcodeHookEntry(h) {
|
|
1836
|
+
if (!h || typeof h !== "object")
|
|
1837
|
+
return false;
|
|
1838
|
+
const hks = h.hooks;
|
|
1839
|
+
if (!Array.isArray(hks))
|
|
1840
|
+
return false;
|
|
1841
|
+
return hks.some((hook) => {
|
|
1842
|
+
if (!hook || typeof hook !== "object")
|
|
1843
|
+
return false;
|
|
1844
|
+
const e = hook;
|
|
1845
|
+
const cmd = typeof e.command === "string" ? e.command : "";
|
|
1846
|
+
const args = Array.isArray(e.args) ? e.args.join(" ") : "";
|
|
1847
|
+
return cmd.includes("vexp-hint") || (/vexp/i.test(cmd) && args.includes("prompt-hint"));
|
|
1848
|
+
});
|
|
1849
|
+
}
|
|
1850
|
+
function noteZcodeHooksDisabled(cfgPath) {
|
|
1851
|
+
const msg = `ZCode hooks are switched off in ${cfgPath} (hooks.enabled: false). ` +
|
|
1852
|
+
"The vexp orientation hook is registered there but stays inert until hooks are enabled in ZCode.";
|
|
1853
|
+
if (!unreachableTargets.includes(msg))
|
|
1854
|
+
unreachableTargets.push(msg);
|
|
1855
|
+
}
|
|
1856
|
+
/**
|
|
1857
|
+
* The orientation hook, user scope — ZCode runs no project-level hooks.
|
|
1858
|
+
*
|
|
1859
|
+
* One entry serves every workspace on the machine: `prompt-hint` takes the
|
|
1860
|
+
* workspace from the `cwd` the payload carries, and is fail-open by
|
|
1861
|
+
* construction — no daemon there, no index, no binary: nothing is injected
|
|
1862
|
+
* and the session is vanilla. `hooks.enabled` is switched on only when that
|
|
1863
|
+
* is not already a decision: a user who set it to false while keeping hooks
|
|
1864
|
+
* of their own has turned hooks off on purpose, and that is honoured (the
|
|
1865
|
+
* entry is still registered, so enabling hooks later activates it, and the
|
|
1866
|
+
* setup summary says so).
|
|
1867
|
+
*/
|
|
1868
|
+
export function installZcodeHintHook(binaryPath) {
|
|
1869
|
+
const cfgPath = zcodeUserConfigPath();
|
|
1870
|
+
const read = readJsonConfigSafe(cfgPath);
|
|
1871
|
+
if (!read.ok) {
|
|
1872
|
+
warnUnparseable(cfgPath);
|
|
1873
|
+
return null;
|
|
1874
|
+
}
|
|
1875
|
+
const cfg = read.data;
|
|
1876
|
+
const before = JSON.stringify(cfg);
|
|
1877
|
+
const hooks = (cfg.hooks && typeof cfg.hooks === "object" ? cfg.hooks : {});
|
|
1878
|
+
const events = (hooks.events && typeof hooks.events === "object" ? hooks.events : {});
|
|
1879
|
+
const existing = Array.isArray(events.UserPromptSubmit) ? events.UserPromptSubmit : [];
|
|
1880
|
+
const filtered = existing.filter((h) => !isVexpZcodeHookEntry(h));
|
|
1881
|
+
filtered.push({
|
|
1882
|
+
hooks: [{ type: "process", command: binaryPath, args: ["prompt-hint"], timeoutMs: 5000 }],
|
|
1883
|
+
});
|
|
1884
|
+
events.UserPromptSubmit = filtered;
|
|
1885
|
+
hooks.events = events;
|
|
1886
|
+
const theirs = Object.entries(events).some(([event, list]) => Array.isArray(list) && list.some((h) => event !== "UserPromptSubmit" || !isVexpZcodeHookEntry(h)));
|
|
1887
|
+
if (hooks.enabled === false && theirs)
|
|
1888
|
+
noteZcodeHooksDisabled(cfgPath);
|
|
1889
|
+
else
|
|
1890
|
+
hooks.enabled = true;
|
|
1891
|
+
cfg.hooks = hooks;
|
|
1892
|
+
if (JSON.stringify(cfg) === before)
|
|
1893
|
+
return null;
|
|
1894
|
+
fs.mkdirSync(path.dirname(cfgPath), { recursive: true });
|
|
1895
|
+
if (read.existed)
|
|
1896
|
+
backupConfig(cfgPath);
|
|
1897
|
+
fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + "\n", "utf-8");
|
|
1898
|
+
return read.existed ? "updated" : "created";
|
|
1899
|
+
}
|
|
1691
1900
|
/**
|
|
1692
1901
|
* Read or generate a bearer token for MCP HTTP auth.
|
|
1693
1902
|
* Persists to ~/.vexp/mcp.token so the MCP server and Codex config share it.
|
|
@@ -2800,6 +3009,9 @@ export function installClaudeCodeHintHook(workspaceRoot, binaryPath) {
|
|
|
2800
3009
|
{
|
|
2801
3010
|
type: "command",
|
|
2802
3011
|
command: 'bash "$CLAUDE_PROJECT_DIR/.claude/hooks/vexp-hint.sh"',
|
|
3012
|
+
// The prompt-hint client's budget ceiling (HINT_BUDGET_CEILING_MS,
|
|
3013
|
+
// main.rs) stays under these 5 s: past them the agent kills the
|
|
3014
|
+
// hook while the daemon still believes the client is waiting.
|
|
2803
3015
|
timeout: 5,
|
|
2804
3016
|
},
|
|
2805
3017
|
],
|
package/dist/cli.js
CHANGED
|
@@ -74,6 +74,14 @@ program.hook("preAction", async (_thisCmd, actionCmd) => {
|
|
|
74
74
|
// 2026-09-05). Doing nothing quietly is the one outcome that cannot be
|
|
75
75
|
// told apart from success.
|
|
76
76
|
if (!eff) {
|
|
77
|
+
// `index` and `init` are the commands that CREATE a workspace, so telling
|
|
78
|
+
// them to go run `vexp setup` first is advice the command is in the middle
|
|
79
|
+
// of superseding — and it read as a contradiction to a user watching one
|
|
80
|
+
// invocation warn that no workspace exists and the next one start a daemon
|
|
81
|
+
// for it (field report, 2026-09-08). The bootstrap still does nothing here;
|
|
82
|
+
// it just does not misdirect.
|
|
83
|
+
if (actionCmd.name() === "index" || actionCmd.name() === "init")
|
|
84
|
+
return;
|
|
77
85
|
console.error(chalk.yellow(` ⚠ no vexp workspace found from ${process.cwd()} — the daemon and the MCP server were NOT started or updated.`));
|
|
78
86
|
console.error(chalk.dim(` vexp walks up for .vexp/manifest.json; run 'vexp setup' in the workspace root.`));
|
|
79
87
|
return;
|
|
@@ -645,8 +653,9 @@ program
|
|
|
645
653
|
program
|
|
646
654
|
.command("doctor")
|
|
647
655
|
.description("Diagnose vexp MCP/daemon state — workspace drift, stale daemons, Codex transport, license (no daemon needed)")
|
|
648
|
-
.
|
|
649
|
-
|
|
656
|
+
.option("--report [file]", "Also write a shareable Markdown report: this diagnosis, the local tool-call ledger (counts and timings, never contents), daemon log warnings, config — secrets masked (default: .vexp/vexp-report.md)")
|
|
657
|
+
.action(async (opts) => {
|
|
658
|
+
await runDoctor({ report: opts.report });
|
|
650
659
|
});
|
|
651
660
|
program
|
|
652
661
|
.command("setup [dir]")
|