vexp-cli 3.1.3 → 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 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
 
@@ -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
- if (analyzed > 0) {
707
- line(OK, `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`);
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vexp-cli",
3
- "version": "3.1.3",
3
+ "version": "3.2.0",
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.3",
109
- "@vexp/core-linux-arm64": "3.1.3",
110
- "@vexp/core-darwin-x64": "3.1.3",
111
- "@vexp/core-darwin-arm64": "3.1.3",
112
- "@vexp/core-win32-x64": "3.1.3"
108
+ "@vexp/core-linux-x64": "3.2.0",
109
+ "@vexp/core-linux-arm64": "3.2.0",
110
+ "@vexp/core-darwin-x64": "3.2.0",
111
+ "@vexp/core-darwin-arm64": "3.2.0",
112
+ "@vexp/core-win32-x64": "3.2.0"
113
113
  }
114
114
  }