vexp-cli 3.1.3 → 3.2.1
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/doctor.js +28 -3
- package/dist/serve.js +8 -0
- package/mcp/mcp-server.cjs +3 -3
- 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/doctor.js
CHANGED
|
@@ -373,6 +373,32 @@ export function workspaceCoverageFindings(root) {
|
|
|
373
373
|
}
|
|
374
374
|
return out;
|
|
375
375
|
}
|
|
376
|
+
/**
|
|
377
|
+
* The 7-day activity ledger as doctor lines. Zero calls is not "not
|
|
378
|
+
* working": silence on oriented prompts is vexp doing its job, so the OK
|
|
379
|
+
* line exists whenever prompts were analyzed. The WARN is the other half of
|
|
380
|
+
* the same ledger: orientations the daemon finished after the hook client
|
|
381
|
+
* (3 s) had stopped waiting were computed, counted by 3.1.x as served, and
|
|
382
|
+
* never read by the agent (field ledger, 3.1.3: up to 8 of 33).
|
|
383
|
+
*/
|
|
384
|
+
export function ledgerFindings(ledger) {
|
|
385
|
+
const out = [];
|
|
386
|
+
const analyzed = Number(ledger.prompts_analyzed) || 0;
|
|
387
|
+
if (analyzed > 0) {
|
|
388
|
+
out.push({
|
|
389
|
+
level: OK,
|
|
390
|
+
message: `savings ledger (7d): ${analyzed} prompt(s) analyzed — ${Number(ledger.silences) || 0} silences (task already oriented), ${Number(ledger.hints_served) || 0} hints served. Details: vexp savings`,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
const late = Number(ledger.late) || 0;
|
|
394
|
+
if (late > 0) {
|
|
395
|
+
out.push({
|
|
396
|
+
level: WARN,
|
|
397
|
+
message: `${late} orientation(s) in the last 7 days finished after the hook client's budget (3 s by default) and never reached the agent - long prompts are slow on this index; vexp savings shows the count`,
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
return out;
|
|
401
|
+
}
|
|
376
402
|
export const SUPPORT_EMAIL = "staff@vexp.dev";
|
|
377
403
|
export const DEFAULT_REPORT_FILE = path.join(".vexp", "vexp-report.md");
|
|
378
404
|
// eslint-disable-next-line no-control-regex
|
|
@@ -703,9 +729,8 @@ async function doctorChecks(onWorkspace) {
|
|
|
703
729
|
// support tickets ("is it working? it never got called").
|
|
704
730
|
const ledger = (st.ledger ?? {});
|
|
705
731
|
const analyzed = Number(ledger.prompts_analyzed) || 0;
|
|
706
|
-
|
|
707
|
-
line(
|
|
708
|
-
}
|
|
732
|
+
for (const f of ledgerFindings(ledger))
|
|
733
|
+
line(f.level, f.message);
|
|
709
734
|
if (sessions.length > 0) {
|
|
710
735
|
const total = sessions.reduce((n, s) => n + (Number(s.pipeline_calls) || 0), 0);
|
|
711
736
|
line(OK, `sessions (4h): ${sessions.length} active, ${total} pipeline calls total`);
|
package/dist/serve.js
CHANGED
|
@@ -75,6 +75,14 @@ async function resurrectDaemon(workspaceRoot, socketPath) {
|
|
|
75
75
|
return false;
|
|
76
76
|
}
|
|
77
77
|
try {
|
|
78
|
+
// `detached` stays. On Windows it is DETACHED_PROCESS: the daemon gets no
|
|
79
|
+
// console, which is what lets it outlive this supervisor — a non-detached
|
|
80
|
+
// child sits in libuv's kill-on-close job object and dies with `vexp
|
|
81
|
+
// serve`. A console-less parent's git children used to open a Windows
|
|
82
|
+
// Terminal window each (field report, 2026-09: 98 a minute); the cure is
|
|
83
|
+
// in vexp-core, which spawns every child with CREATE_NO_WINDOW
|
|
84
|
+
// (packages/vexp-core/src/proc.rs). `windowsHide` would not help here:
|
|
85
|
+
// Windows ignores it next to DETACHED_PROCESS.
|
|
78
86
|
const child = spawn(binary, ["daemon", "--workspace", workspaceRoot, "--socket", socketPath], {
|
|
79
87
|
detached: true,
|
|
80
88
|
stdio: "ignore",
|
package/mcp/mcp-server.cjs
CHANGED
|
@@ -101,11 +101,11 @@ To lift the limit now, upgrade to Pro or Team: https://vexp.dev/#pricing`)}var H
|
|
|
101
101
|
`);if(i.length&&i.some(o=>o.trim()))return`
|
|
102
102
|
--- ${n} (last ${Math.min(e,i.length)} lines) ---
|
|
103
103
|
`+i.slice(-e).join(`
|
|
104
|
-
`)}catch{}return""}get socketPath(){return this.explicitSocketPath?this.explicitSocketPath:this.spawnWorkspaceRoot?GL(this.spawnWorkspaceRoot):D$()??HL()}static isRetryableError(e){let r=e.message;return t.RETRYABLE_CODES.some(n=>r.includes(n))}async call(e,r){let n;for(let a=0;a<=t.MAX_RETRIES;a++)try{return await this.callOnce(e,r)}catch(i){if(n=i instanceof Error?i:new Error(String(i)),t.isRetryableError(n)){let o=R$(Vo(this.spawnWorkspaceRoot));if(o)throw ML(o)}if(a<t.MAX_RETRIES&&t.isRetryableError(n)){a===0&&await this.maybeSpawnDaemon(),console.error(`[vexp-mcp] Daemon connection failed (${n.message}), retrying in ${t.RETRY_DELAY_MS}ms (attempt ${a+1}/${t.MAX_RETRIES})`),await new Promise(o=>setTimeout(o,t.RETRY_DELAY_MS));continue}throw this.augmentDaemonError(n)}throw this.augmentDaemonError(n)}augmentDaemonError(e){if(!t.isRetryableError(e))return e;let r=this.daemonLogTail();return r&&!e.message.includes("--- daemon.log")&&!e.message.includes("--- vexp.log")&&(e.message+=r),!this.explicitSocketPath&&!this.spawnWorkspaceRoot&&!e.message.includes("--- vexp targeting diagnostics ---")&&(e.message+=VL()),e}async callStreaming(e,r,n){return new Promise((a,i)=>{let s={id:++this.requestCounter,tool:e,params:r,session_id:this.sessionId,stream:!0},c=JSON.stringify(s)+`
|
|
104
|
+
`)}catch{}return""}get socketPath(){return this.explicitSocketPath?this.explicitSocketPath:this.spawnWorkspaceRoot?GL(this.spawnWorkspaceRoot):D$()??HL()}static isRetryableError(e){if(t.wasHandlingRequest(e))return!1;let r=e.message;return t.RETRYABLE_CODES.some(n=>r.includes(n))}static wasHandlingRequest(e){return e.vexpDaemonGone===!0}static daemonGoneError(e,r){let n=new Error(t.daemonGoneMessage(e,r));return n.vexpDaemonGone=!0,n}async call(e,r){let n;for(let a=0;a<=t.MAX_RETRIES;a++)try{return await this.callOnce(e,r)}catch(i){if(n=i instanceof Error?i:new Error(String(i)),t.isRetryableError(n)){let o=R$(Vo(this.spawnWorkspaceRoot));if(o)throw ML(o)}if(a<t.MAX_RETRIES&&t.isRetryableError(n)){a===0&&await this.maybeSpawnDaemon(),console.error(`[vexp-mcp] Daemon connection failed (${n.message}), retrying in ${t.RETRY_DELAY_MS}ms (attempt ${a+1}/${t.MAX_RETRIES})`),await new Promise(o=>setTimeout(o,t.RETRY_DELAY_MS));continue}throw this.augmentDaemonError(n)}throw this.augmentDaemonError(n)}static daemonGoneMessage(e,r){let n=r?` (${r})`:"";return`The vexp daemon closed the connection before answering '${e}': it stopped or was restarted while handling the request${n}. If it crashed, the reason is in the last lines of daemon.log (daemon.log.1 once a new daemon has started; vexp.log when VS Code runs it) in the .vexp folder of the workspace it serves. AGENT INSTRUCTION: retry this call once; if it fails the same way, continue with your own tools and tell the user.`}augmentDaemonError(e){if(!t.isRetryableError(e)&&!t.wasHandlingRequest(e))return e;let r=this.daemonLogTail();return r&&!e.message.includes("--- daemon.log")&&!e.message.includes("--- vexp.log")&&(e.message+=r),!t.wasHandlingRequest(e)&&!this.explicitSocketPath&&!this.spawnWorkspaceRoot&&!e.message.includes("--- vexp targeting diagnostics ---")&&(e.message+=VL()),e}async callStreaming(e,r,n){return new Promise((a,i)=>{let s={id:++this.requestCounter,tool:e,params:r,session_id:this.sessionId,stream:!0},c=JSON.stringify(s)+`
|
|
105
105
|
`,u,p="",l=!1,d=setTimeout(()=>{l||(l=!0,u?.destroy(),i(new Error(`Timeout: no streaming response from daemon for tool '${e}' after 60s`)))},6e4),f=m=>{l||(l=!0,clearTimeout(d),m())};process.platform==="win32"?u=Bo.createConnection(this.socketPath):u=Bo.createConnection({path:this.socketPath}),u.on("connect",()=>{u.write(c)}),u.on("data",m=>{p+=m.toString();let h;for(;(h=p.indexOf(`
|
|
106
106
|
`))!==-1;){let g=p.slice(0,h);if(p=p.slice(h+1),!!g.trim())try{let x=JSON.parse(g);if(x.type==="chunk")n?.(x);else if(x.type==="result"){u.destroy();let _=x.response;_?.type==="response"?f(()=>a(_.result)):_?.type==="error"?f(()=>i(new Error(_.message??"Daemon error"))):f(()=>a(_))}}catch{}}}),u.on("error",m=>{f(()=>i(new Error(`Daemon connection error: ${m.message}. ${j$()}`)))}),u.on("close",()=>{f(()=>i(new Error("Daemon connection closed unexpectedly")))})})}static callBudgetMs(e){switch(e){case"verify_done":return 12e4;case"run_pipeline":case"get_context_capsule":return 9e4;default:return 3e4}}static timeoutMessage(e,r){let n=`Timeout: no response from daemon for tool '${e}' after ${Math.round(r/1e3)}s. The daemon accepted the request and keeps processing it (a client that stops waiting does not cancel it)`;return e==="verify_done"?n+="; call verify_done once more in ~30s: a finished verdict on an unchanged tree is served from cache instantly. Phase timings for the slow call are in .vexp/daemon.log (grep verify_done).":n+=".",n}async callOnce(e,r){return new Promise((n,a)=>{let o={id:++this.requestCounter,tool:e,params:r,session_id:this.sessionId},s=JSON.stringify(o)+`
|
|
107
|
-
`,c,u="",p=!1,l=t.callBudgetMs(e),
|
|
108
|
-
`);if(
|
|
107
|
+
`,c,u="",p=!1,l=!1,d=t.callBudgetMs(e),f=setTimeout(()=>{p||(p=!0,c?.destroy(),a(new Error(t.timeoutMessage(e,d))))},d),m=h=>{p||(p=!0,clearTimeout(f),h())};process.platform==="win32"?c=Bo.createConnection(this.socketPath):c=Bo.createConnection({path:this.socketPath}),c.on("connect",()=>{c.write(s,h=>{h||(l=!0)})}),c.on("data",h=>{u+=h.toString();let g=u.indexOf(`
|
|
108
|
+
`);if(g!==-1){let x=u.slice(0,g);c.destroy();try{let _=JSON.parse(x);_.type==="response"?m(()=>n(_.result)):m(()=>a(new Error(_.message??"Daemon error")))}catch{m(()=>a(new Error(`Invalid response JSON: ${x}`)))}}}),c.on("error",h=>{m(()=>a(l?t.daemonGoneError(e,h.message):new Error(`Daemon connection error: ${h.message}. ${j$()}`)))}),c.on("close",()=>{m(()=>a(t.daemonGoneError(e)))})})}async health(){try{return await this.call("index_status",{}),!0}catch{return!1}}};function LL(){if(process.env.VEXP_CORE_PATH&&oe.existsSync(process.env.VEXP_CORE_PATH))return process.env.VEXP_CORE_PATH;let t;try{t=typeof __dirname<"u"?__dirname:L.dirname((0,A$.fileURLToPath)(WL.url))}catch{t=process.cwd()}let e=`${process.platform}-${process.arch==="arm64"?"arm64":"x64"}`,r=process.platform==="win32"?".exe":"",n=[L.resolve(t,"..","..","@vexp",`core-${e}`,"bin",`vexp-core${r}`),L.resolve(t,"..","node_modules","@vexp",`core-${e}`,"bin",`vexp-core${r}`),L.resolve(t,"..","binaries",`vexp-core-${e}`,`vexp-core${r}`),L.resolve(t,"..","..","..","target","release",`vexp-core${r}`)];for(let a of n)if(oe.existsSync(a))return a;return null}function FL(t){let e=L.dirname(t),r={...process.env};return process.platform==="linux"?r.LD_LIBRARY_PATH=r.LD_LIBRARY_PATH?`${e}:${r.LD_LIBRARY_PATH}`:e:process.platform==="darwin"&&(r.DYLD_LIBRARY_PATH=r.DYLD_LIBRARY_PATH?`${e}:${r.DYLD_LIBRARY_PATH}`:e,r.DYLD_FALLBACK_LIBRARY_PATH=r.DYLD_FALLBACK_LIBRARY_PATH?`${e}:${r.DYLD_FALLBACK_LIBRARY_PATH}`:e),r}function Iu(t){let e=BigInt("0xcbf29ce484222325"),r=BigInt("0x100000001b3"),n=BigInt("0xffffffffffffffff"),a=Buffer.from(t,"utf-8");for(let i of a)e^=BigInt(i),e=e*r&n;return e.toString(16)}function ZL(){let t=process.cwd(),e=t;for(;;){let n=L.join(e,".vexp");if(oe.existsSync(L.join(n,"manifest.json"))||oe.existsSync(L.join(n,"index.db")))return e;let a=L.dirname(e);if(a===e)break;e=a}let r=L.resolve(process.env.VEXP_HOME&&L.isAbsolute(process.env.VEXP_HOME)?process.env.VEXP_HOME:Gm.homedir());for(e=t;;){if(oe.existsSync(L.join(e,".vexp"))&&L.resolve(e)!==r)return e;let n=L.dirname(e);if(n===e)break;e=n}for(e=t;;){let n=L.join(e,".git");if(oe.existsSync(n))return e;let a=L.dirname(e);if(a===e)break;e=a}return t}var O$=new Set;function UL(t){O$.has(t)||(O$.add(t),console.error(t))}function N$(t){if(t)return{root:t,source:"VEXP_WORKSPACE"};let e=process.env.VEXP_WORKSPACE,r=process.env.CLAUDE_PROJECT_DIR;return e&&r&&L.resolve(e).toLowerCase()!==L.resolve(r).toLowerCase()?(UL(`[vexp-mcp] VEXP_WORKSPACE=${e} disagrees with this session's CLAUDE_PROJECT_DIR=${r}; using CLAUDE_PROJECT_DIR for per-session targeting. Unset the global VEXP_WORKSPACE env var to silence this.`),{root:r,source:"CLAUDE_PROJECT_DIR"}):e?{root:e,source:"VEXP_WORKSPACE"}:r?{root:r,source:"CLAUDE_PROJECT_DIR"}:{root:ZL(),source:"cwd-discovery"}}function Vo(t){return N$(t).root}function I$(t){let e=process.env.HOME??process.env.USERPROFILE??"";if(!e)return null;let r=L.join(e,".vexp","daemons.json"),n;try{n=JSON.parse(oe.readFileSync(r,"utf-8"))}catch{return null}let a=Object.entries(n),i=process.platform==="win32"?a:a.filter(([,u])=>{try{return oe.statSync(u).isSocket()}catch{return!1}});if(i.length===0)return null;let o=t.toLowerCase();for(let[u,p]of i)if(u.toLowerCase()===o)return p;let s="",c=null;for(let[u,p]of i){let l=u.toLowerCase();(o.startsWith(l+L.sep)||o===l)&&l.length>s.length&&(s=l,c=p)}return c||null}function BL(){let t=process.env.HOME??process.env.USERPROFILE??"";if(!t)return[];let e;try{e=JSON.parse(oe.readFileSync(L.join(t,".vexp","daemons.json"),"utf-8"))}catch{return[]}let r=Object.entries(e);return process.platform==="win32"?r:r.filter(([,n])=>{try{return oe.statSync(n).isSocket()}catch{return!1}})}function Qa(){let{root:t,source:e}=N$(),r=process.env.VEXP_SOCKET;if(r)return{socket:r,source:e,socketOrigin:"explicit",workspaceRoot:t};if(process.platform==="win32"){let i=L.join(t,".vexp","daemon.pipe");try{let c=oe.readFileSync(i,"utf-8").trim();if(c)return{socket:c,source:e,socketOrigin:"workspace-local",workspaceRoot:t}}catch{}let o=I$(t);return o?{socket:o,source:e,socketOrigin:"registry",workspaceRoot:t}:{socket:`\\\\.\\pipe\\vexp-${Iu(t.toLowerCase()).slice(0,8)}`,source:e,socketOrigin:"hash-fallback",workspaceRoot:t}}let n=L.join(t,".vexp","daemon.sock");if(oe.existsSync(n))return{socket:n,source:e,socketOrigin:"workspace-local",workspaceRoot:t};let a=I$(t);return a?{socket:a,source:e,socketOrigin:"registry",workspaceRoot:t}:{socket:null,source:e,socketOrigin:"none",workspaceRoot:t}}function D$(){return Qa().socket}function VL(){let t;try{t=Qa()}catch{return""}let e=process.env.VEXP_WORKSPACE,r=process.env.CLAUDE_PROJECT_DIR,n=t.socket??L.join(t.workspaceRoot,".vexp","daemon.sock"),a=(()=>{try{return oe.existsSync(n)}catch{return!1}})(),i=oe.existsSync(L.join(t.workspaceRoot,".vexp","manifest.json"))||oe.existsSync(L.join(t.workspaceRoot,".vexp","index.db")),o=t.source==="cwd-discovery"&&!i,s=BL(),c=["","--- vexp targeting diagnostics ---","The MCP server could not reach a vexp daemon for this session.","","Workspace resolution:",` VEXP_WORKSPACE ${e||"(not set)"}`,` CLAUDE_PROJECT_DIR ${r||"(not set)"}`,` resolved workspace ${t.workspaceRoot} (via ${t.source}${o?" \u2014 no indexed .vexp/ found walking up from cwd":""})`,` socket tried ${n}${a?"":" (missing)"}`,""];if(s.length){c.push(`Live daemons in ~/.vexp/daemons.json (${s.length}):`);for(let[u,p]of s.slice(0,8))c.push(` ${u} -> ${p}`);s.length>8&&c.push(` ... and ${s.length-8} more`)}else c.push("No live daemons are registered in ~/.vexp/daemons.json.");return c.push(""),o?c.push("This host launched the MCP server outside your project and passed no","VEXP_WORKSPACE, so vexp cannot tell which project you mean. Set VEXP_WORKSPACE","to your project root in the MCP server's env"+(s.length===1?`, e.g. VEXP_WORKSPACE=${s[0][0]}`:"")+"."):s.some(([u])=>u.toLowerCase()===t.workspaceRoot.toLowerCase())||c.push(`No live daemon is registered for ${t.workspaceRoot}.`,"Start it with `vexp daemon-cmd start` in that directory, or `vexp index` to (re)index."),c.join(`
|
|
109
109
|
`)}function HL(){let t=D$();if(t)return t;let e=Vo();return L.join(e,".vexp","daemon.sock")}function M$(t){let e=process.env.HOME??process.env.USERPROFILE??"";if(!e)return null;let r=L.join(e,".vexp","daemons.json"),n;try{n=JSON.parse(oe.readFileSync(r,"utf-8"))}catch{return null}let a=t.toLowerCase();for(let[i,o]of Object.entries(n))if(Iu(i.toLowerCase()).slice(0,8)===a){if(process.platform==="win32")return o;try{if(oe.statSync(o).isSocket())return o}catch{}}return null}function L$(t){let e=process.env.HOME??process.env.USERPROFILE??"";if(!e)return null;let r;try{r=JSON.parse(oe.readFileSync(L.join(e,".vexp","daemons.json"),"utf-8"))}catch{return null}let n=t.toLowerCase();for(let a of Object.keys(r))if(Iu(a.toLowerCase()).slice(0,8)===n)return a;return null}function GL(t){return process.platform==="win32"?`\\\\.\\pipe\\vexp-${Iu(t.toLowerCase()).slice(0,8)}`:L.join(t,".vexp","daemon.sock")}function j$(){let t=Vo(),e=process.env.HOME??process.env.USERPROFILE??"",r=[];if(process.env.VEXP_SOCKET&&r.push(`VEXP_SOCKET=${process.env.VEXP_SOCKET}`),r.push(`${t}/.vexp/daemon.sock`),e)try{let n=JSON.parse(oe.readFileSync(L.join(e,".vexp","daemons.json"),"utf-8"));for(let[a,i]of Object.entries(n))r.push(`registry: ${a} \u2192 ${i}`)}catch{}return`Tried: [${r.join(", ")}]. Run \`vexp setup\` in your project to start the daemon, or set VEXP_WORKSPACE / VEXP_SOCKET.`}var or=t=>typeof t=="string"?[t]:t,ft=t=>t==="true"?!0:t==="false"?!1:t,pt=t=>typeof t=="string"&&t.trim()!==""&&!isNaN(Number(t))?Number(t):t;var KL=S.object({query:S.string().describe("Description of the task or what you need context for"),repos:S.preprocess(or,S.array(S.string()).optional()).describe("Repo aliases to query (default: all)"),max_tokens:S.preprocess(pt,S.number().optional().default(8e3)).describe("Max tokens for the capsule"),pivot_depth:S.preprocess(pt,S.number().optional().default(2)).describe("BFS depth from pivot nodes"),include_tests:S.preprocess(ft,S.boolean().optional().default(!1)).describe("Include test files"),skeleton_detail:S.enum(["minimal","standard","detailed"]).optional().default("standard")}),F$={name:"get_context_capsule",description:"Lightweight context search \u2014 finds relevant code via semantic + graph search. NOTE: For most tasks, prefer run_pipeline instead \u2014 it includes this capsule plus impact analysis and memory in a single call with fewer tokens. Use get_context_capsule only for quick, simple lookups where you don't need impact analysis or memory recall, or when you want a lighter, faster response.",inputSchema:{type:"object",properties:{query:{type:"string",description:"Description of the task"},repos:{type:"array",items:{type:"string"},description:"Repo aliases to query (from index_status). Default: all indexed repos"},max_tokens:{type:"number",description:"Max tokens (default: 8000)"},pivot_depth:{type:"number",description:"Graph traversal depth (default: 2)"},include_tests:{type:"boolean",description:"Include test files"},skeleton_detail:{type:"string",enum:["minimal","standard","detailed"],description:"Skeleton detail level (default: standard)"}},required:["query"]}};async function Z$(t,e){let r=KL.parse(t),n=await e.call("get_context_capsule",r);return JL(n)}function JL(t){let e=[];e.push("# vexp Context Capsule");let r=t.token_budget.saving_pct>1?`${t.token_budget.saving_pct.toFixed(0)}% token saving vs full content`:`${t.graph_stats.nodes_analyzed} nodes \xB7 ${t.graph_stats.edges_traversed} edges traversed`,n=t.memories?.length??0;if(e.push(`> Token budget: ${t.token_budget.used}/${t.token_budget.total} used | ${r} | ${t.graph_stats.query_time_ms}ms | memories: ${n}`),e.push(""),t.pivots.length===0)return e.push("*No relevant code found for this query. Try a different description.*"),e.join(`
|
|
110
110
|
`);let a=t.graph_stats.repos_queried.length>1;e.push("## Pivot Files (Full Content)"),e.push("");for(let i of t.pivots){let o=a?`\`${i.repo}\` / `:"";e.push(`### ${o}\`${i.file_path}\` (relevance: ${i.relevance_score.toFixed(2)})`),e.push(`*${i.why}*`),e.push(""),e.push("```"),e.push(i.content),e.push("```"),e.push("")}if(t.supporting.length>0){e.push("## Supporting Context (Skeletons)"),e.push("");for(let i of t.supporting){let o=i.cross_repo?" *(cross-repo)*":"";e.push(`### \`${i.file_path}\`${o}`),e.push(`*${i.relationship}*`),e.push(""),e.push("```"),e.push(i.skeleton),e.push("```"),e.push("")}}if(e.push("## Session Memory"),t.memories&&t.memories.length>0){e.push("*Relevant observations from this and previous sessions:*"),e.push("");for(let i of t.memories){let o=new Date(i.created_at*1e3),s=`${String(o.getMonth()+1).padStart(2,"0")}-${String(o.getDate()).padStart(2,"0")} ${String(o.getHours()).padStart(2,"0")}:${String(o.getMinutes()).padStart(2,"0")}`,c=i.stale?" ~stale~":"";e.push(`- [${s}${c}] ${i.content}`)}}else e.push("*No relevant memories found. Use `save_observation` to persist important decisions.*");return e.push(""),e.join(`
|
|
111
111
|
`)}var XL=S.object({symbol_fqn:S.string().describe("Fully qualified name of the symbol (e.g. 'src/auth/auth.ts::validateToken')"),depth:S.preprocess(pt,S.number().optional().default(5)).describe("BFS depth for impact traversal"),cross_repo:S.preprocess(ft,S.boolean().optional().default(!1)).describe("Include cross-repo dependencies"),format:S.enum(["list","tree","mermaid"]).optional().default("tree")}),U$={name:"get_impact_graph",description:"Get the impact graph for a symbol \u2014 shows all code that would break or be affected if this symbol changes. Returns callers, importers, and transitive dependents up to the specified depth. WHEN TO USE: (1) Before refactoring a function/class \u2014 to understand the blast radius. (2) Before changing a public API \u2014 to find all call sites. (3) When asked 'what uses X?' or 'what depends on X?'. Requires the exact FQN from a previous get_context_capsule result (e.g. 'src/auth/auth.ts::validateToken').",inputSchema:{type:"object",properties:{symbol_fqn:{type:"string",description:"Fully qualified name (e.g. 'src/auth/auth.ts::validateToken')"},depth:{type:"number",description:"Max traversal depth (default: 5)"},cross_repo:{type:"boolean",description:"Include cross-repo symbol matches and synthetic edges (API contracts, shared types). Use in multi-repo workspaces"},format:{type:"string",enum:["list","tree","mermaid"],description:"Output format (default: tree)"}},required:["symbol_fqn"]}};async function B$(t,e){let r=XL.parse(t),n=await e.call("get_impact_graph",r);return typeof n=="string"?n:YL(n,r.format??"tree")}function YL(t,e){let r=[];return r.push(`# Impact Graph: \`${t.root_fqn}\``),r.push(`> ${t.total_impacted} nodes impacted | depth ${t.max_depth_reached} | ${t.query_time_ms}ms`),r.push(""),t.total_impacted===0?(r.push("*No dependents found. This symbol is not referenced by other code.*"),r.join(`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vexp-cli",
|
|
3
|
-
"version": "3.1
|
|
3
|
+
"version": "3.2.1",
|
|
4
4
|
"description": "Local-first context engine for AI coding agents. Pre-indexes your codebase into a dependency graph and feeds any MCP agent only the code that matters — 87% fewer tokens per call. New in 2.5: mechanical work verification and a PII/secret scanner. Works with Claude Code, Cursor, Codex, Copilot, Windsurf, Cline, Aider and 14 agents. Your code never leaves your machine.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -105,10 +105,10 @@
|
|
|
105
105
|
},
|
|
106
106
|
"homepage": "https://vexp.dev",
|
|
107
107
|
"optionalDependencies": {
|
|
108
|
-
"@vexp/core-linux-x64": "3.1
|
|
109
|
-
"@vexp/core-linux-arm64": "3.1
|
|
110
|
-
"@vexp/core-darwin-x64": "3.1
|
|
111
|
-
"@vexp/core-darwin-arm64": "3.1
|
|
112
|
-
"@vexp/core-win32-x64": "3.1
|
|
108
|
+
"@vexp/core-linux-x64": "3.2.1",
|
|
109
|
+
"@vexp/core-linux-arm64": "3.2.1",
|
|
110
|
+
"@vexp/core-darwin-x64": "3.2.1",
|
|
111
|
+
"@vexp/core-darwin-arm64": "3.2.1",
|
|
112
|
+
"@vexp/core-win32-x64": "3.2.1"
|
|
113
113
|
}
|
|
114
114
|
}
|