vexp-cli 2.3.1 → 2.4.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/dist/agent-config.js +235 -85
- package/dist/doctor.js +60 -8
- package/dist/hook-template.js +63 -0
- package/dist/serve.js +10 -0
- package/mcp/mcp-server.cjs +43 -43
- package/package.json +6 -6
package/dist/agent-config.js
CHANGED
|
@@ -10,7 +10,7 @@ import * as fs from "fs";
|
|
|
10
10
|
import * as path from "path";
|
|
11
11
|
import * as os from "os";
|
|
12
12
|
import * as crypto from "crypto";
|
|
13
|
-
import { VEXP_GUARD_HOOK, VEXP_OPENCODE_GUARD, VEXP_CURSOR_GUARD } from "./hook-template.js";
|
|
13
|
+
import { VEXP_GUARD_HOOK, VEXP_OPENCODE_GUARD, VEXP_CURSOR_GUARD, vexpHintHookScript, vexpOpencodeHintPlugin } from "./hook-template.js";
|
|
14
14
|
// ---------------------------------------------------------------------------
|
|
15
15
|
// Constants
|
|
16
16
|
// ---------------------------------------------------------------------------
|
|
@@ -320,6 +320,18 @@ export function configureAgents(workspaceRoot, binaryPath, version, agentFilter,
|
|
|
320
320
|
const wrote = configureClaudeCodeGlobal(binaryPath, mcpServerPath, workspaceRoot);
|
|
321
321
|
if (wrote)
|
|
322
322
|
mcpConfigs.push("~/.claude.json");
|
|
323
|
+
// Event-driven hint hook: default-on (non-blocking, fail-open — the
|
|
324
|
+
// 2.3 A2 opt-in applies to DENY hooks, this one cannot block).
|
|
325
|
+
const hintResult = installClaudeCodeHintHook(workspaceRoot, binaryPath);
|
|
326
|
+
if (hintResult) {
|
|
327
|
+
results.push({
|
|
328
|
+
agent: "Claude Code Hint",
|
|
329
|
+
configFile: ".claude/hooks/vexp-hint.sh",
|
|
330
|
+
content: vexpHintHookScript(binaryPath),
|
|
331
|
+
alreadyExists: false,
|
|
332
|
+
action: hintResult,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
323
335
|
// Guard is OPT-IN from 2.3 (see GuardMode): strict installs the
|
|
324
336
|
// PreToolUse deny hook; default removes any previously installed one.
|
|
325
337
|
if (guardMode() === "strict") {
|
|
@@ -349,6 +361,7 @@ export function configureAgents(workspaceRoot, binaryPath, version, agentFilter,
|
|
|
349
361
|
const wrote = configureCodexGlobal(binaryPath, mcpServerPath, workspaceRoot);
|
|
350
362
|
if (wrote)
|
|
351
363
|
mcpConfigs.push("~/.codex/config.toml");
|
|
364
|
+
installCodexHintHook(workspaceRoot, binaryPath);
|
|
352
365
|
}
|
|
353
366
|
// Antigravity: machine-global MCP config (~/.gemini/antigravity/mcp_config.json)
|
|
354
367
|
if (detector.agent === "Antigravity") {
|
|
@@ -376,6 +389,7 @@ export function configureAgents(workspaceRoot, binaryPath, version, agentFilter,
|
|
|
376
389
|
const wrote = configureKiloMcp(workspaceRoot, binaryPath, mcpServerPath);
|
|
377
390
|
if (wrote)
|
|
378
391
|
mcpConfigs.push(wrote);
|
|
392
|
+
installOpencodeHintPlugin(workspaceRoot, binaryPath, ".kilo/plugin");
|
|
379
393
|
// Kilo v7 vendors opencode, so it takes the same guard plugin — the rules
|
|
380
394
|
// markdown alone was demonstrably not enough (a reported session loaded
|
|
381
395
|
// vexp.md, quoted it back, and still read five files by hand).
|
|
@@ -437,6 +451,7 @@ export function configureAgents(workspaceRoot, binaryPath, version, agentFilter,
|
|
|
437
451
|
// Claude Code PreToolUse hook) — which is only safe once MCP is registered.
|
|
438
452
|
if (detector.agent === "Opencode") {
|
|
439
453
|
const wroteMcp = configureOpencodeMcp(workspaceRoot, binaryPath, mcpServerPath);
|
|
454
|
+
installOpencodeHintPlugin(workspaceRoot, binaryPath, ".opencode/plugin");
|
|
440
455
|
if (wroteMcp)
|
|
441
456
|
mcpConfigs.push(wroteMcp);
|
|
442
457
|
if (guardMode() === "strict") {
|
|
@@ -607,6 +622,7 @@ export function configureSelectedAgents(workspaceRoot, binaryPath, version, sele
|
|
|
607
622
|
const wrote = configureClaudeCodeGlobal(binaryPath, mcpServerPath, workspaceRoot);
|
|
608
623
|
if (wrote)
|
|
609
624
|
mcpConfigs.push("~/.claude.json");
|
|
625
|
+
installClaudeCodeHintHook(workspaceRoot, binaryPath);
|
|
610
626
|
if (guardMode() === "strict")
|
|
611
627
|
installClaudeCodeHook(workspaceRoot);
|
|
612
628
|
else
|
|
@@ -616,6 +632,7 @@ export function configureSelectedAgents(workspaceRoot, binaryPath, version, sele
|
|
|
616
632
|
const wrote = configureCodexGlobal(binaryPath, mcpServerPath, workspaceRoot);
|
|
617
633
|
if (wrote)
|
|
618
634
|
mcpConfigs.push("~/.codex/config.toml");
|
|
635
|
+
installCodexHintHook(workspaceRoot, binaryPath);
|
|
619
636
|
}
|
|
620
637
|
if (detector.agent === "Antigravity") {
|
|
621
638
|
const wrote = configureAntigravityGlobal(binaryPath, mcpServerPath);
|
|
@@ -636,6 +653,7 @@ export function configureSelectedAgents(workspaceRoot, binaryPath, version, sele
|
|
|
636
653
|
const wrote = configureOpencodeMcp(workspaceRoot, binaryPath, mcpServerPath);
|
|
637
654
|
if (wrote)
|
|
638
655
|
mcpConfigs.push(wrote);
|
|
656
|
+
installOpencodeHintPlugin(workspaceRoot, binaryPath, ".opencode/plugin");
|
|
639
657
|
if (guardMode() === "strict")
|
|
640
658
|
installOpencodePlugin(workspaceRoot);
|
|
641
659
|
else
|
|
@@ -655,6 +673,7 @@ export function configureSelectedAgents(workspaceRoot, binaryPath, version, sele
|
|
|
655
673
|
const wrote = configureKiloMcp(workspaceRoot, binaryPath, mcpServerPath);
|
|
656
674
|
if (wrote)
|
|
657
675
|
mcpConfigs.push(wrote);
|
|
676
|
+
installOpencodeHintPlugin(workspaceRoot, binaryPath, ".kilo/plugin");
|
|
658
677
|
if (guardMode() === "strict")
|
|
659
678
|
installKiloPlugin(workspaceRoot);
|
|
660
679
|
else
|
|
@@ -1634,20 +1653,23 @@ export function installClaudeCodeHook(workspaceRoot) {
|
|
|
1634
1653
|
const existingPreToolUse = Array.isArray(hooks.PreToolUse) ? hooks.PreToolUse : [];
|
|
1635
1654
|
// Aggressively remove ALL vexp-related hook entries (old format, stale matchers, malformed)
|
|
1636
1655
|
const filtered = existingPreToolUse.filter((h) => !isVexpGuardHookEntry(h));
|
|
1637
|
-
//
|
|
1638
|
-
//
|
|
1639
|
-
//
|
|
1640
|
-
//
|
|
1641
|
-
//
|
|
1642
|
-
//
|
|
1643
|
-
//
|
|
1656
|
+
// Shell form with an explicit `bash` prefix and a QUOTED path — the one
|
|
1657
|
+
// shape that works on every OS Claude Code supports:
|
|
1658
|
+
// - the quotes fix the historical spaces bug (unquoted
|
|
1659
|
+
// `$CLAUDE_PROJECT_DIR/...` word-split and the guard never ran);
|
|
1660
|
+
// - exec form (`args` present → direct spawn) fixed spaces but broke
|
|
1661
|
+
// Windows entirely: the OS cannot spawn a `.sh` without a shell, the
|
|
1662
|
+
// hook errored, failed open, and enforced nothing (field report) —
|
|
1663
|
+
// Claude Code on Windows runs shell-form hooks through Git Bash, so
|
|
1664
|
+
// `bash "$VAR"` works there and on unix alike.
|
|
1665
|
+
// `timeout` is SECONDS (default 600), not ms: 3000 once configured a
|
|
1666
|
+
// 50-minute hook timeout.
|
|
1644
1667
|
filtered.push({
|
|
1645
1668
|
matcher: "Grep|Glob|Regex",
|
|
1646
1669
|
hooks: [
|
|
1647
1670
|
{
|
|
1648
1671
|
type: "command",
|
|
1649
|
-
command: "$
|
|
1650
|
-
args: [],
|
|
1672
|
+
command: 'bash "$CLAUDE_PROJECT_DIR/.claude/hooks/vexp-guard.sh"',
|
|
1651
1673
|
timeout: 5,
|
|
1652
1674
|
},
|
|
1653
1675
|
],
|
|
@@ -1667,6 +1689,144 @@ export function installClaudeCodeHook(workspaceRoot) {
|
|
|
1667
1689
|
}
|
|
1668
1690
|
return existed ? "updated" : "created";
|
|
1669
1691
|
}
|
|
1692
|
+
function isVexpHintHookEntry(h) {
|
|
1693
|
+
if (!h || typeof h !== "object")
|
|
1694
|
+
return false;
|
|
1695
|
+
const entry = h;
|
|
1696
|
+
const own = entry.command?.includes("vexp-hint");
|
|
1697
|
+
if (own)
|
|
1698
|
+
return true;
|
|
1699
|
+
const hks = entry.hooks;
|
|
1700
|
+
return Array.isArray(hks) && hks.some((hook) => {
|
|
1701
|
+
if (!hook || typeof hook !== "object")
|
|
1702
|
+
return false;
|
|
1703
|
+
return hook.command?.includes("vexp-hint") ?? false;
|
|
1704
|
+
});
|
|
1705
|
+
}
|
|
1706
|
+
/**
|
|
1707
|
+
* Install the event-driven hint hook for Claude Code (2.3.3 default).
|
|
1708
|
+
* Replaces the resident CLAUDE.md strategy text: a UserPromptSubmit hook
|
|
1709
|
+
* asks the daemon to classify each prompt against the index and injects a
|
|
1710
|
+
* one-line run_pipeline suggestion ONLY for unfamiliar-area prompts. On
|
|
1711
|
+
* oriented prompts the injection is empty — zero context cost. Fail-open by
|
|
1712
|
+
* construction (see VEXP_HINT_HOOK): any failure = vanilla behavior.
|
|
1713
|
+
* Non-blocking and additive, so it is NOT gated by guardMode (the 2.3 A2
|
|
1714
|
+
* opt-in applies to DENY hooks, which add a failure mode; this one cannot
|
|
1715
|
+
* block anything).
|
|
1716
|
+
*/
|
|
1717
|
+
export function installClaudeCodeHintHook(workspaceRoot, binaryPath) {
|
|
1718
|
+
const hookDir = path.join(workspaceRoot, ".claude", "hooks");
|
|
1719
|
+
const hookPath = path.join(hookDir, "vexp-hint.sh");
|
|
1720
|
+
const settingsPath = path.join(workspaceRoot, ".claude", "settings.json");
|
|
1721
|
+
const script = vexpHintHookScript(binaryPath);
|
|
1722
|
+
fs.mkdirSync(hookDir, { recursive: true });
|
|
1723
|
+
const existed = fs.existsSync(hookPath);
|
|
1724
|
+
const scriptIdentical = existed && fs.readFileSync(hookPath, "utf-8") === script;
|
|
1725
|
+
if (!scriptIdentical) {
|
|
1726
|
+
fs.writeFileSync(hookPath, script, { mode: 0o755 });
|
|
1727
|
+
}
|
|
1728
|
+
const read = readJsonConfigSafe(settingsPath);
|
|
1729
|
+
if (!read.ok) {
|
|
1730
|
+
warnUnparseable(settingsPath);
|
|
1731
|
+
return scriptIdentical ? null : existed ? "updated" : "created";
|
|
1732
|
+
}
|
|
1733
|
+
const settings = read.data;
|
|
1734
|
+
const hooks = (settings.hooks ?? {});
|
|
1735
|
+
const existing = Array.isArray(hooks.UserPromptSubmit)
|
|
1736
|
+
? hooks.UserPromptSubmit
|
|
1737
|
+
: [];
|
|
1738
|
+
const filtered = existing.filter((h) => !isVexpHintHookEntry(h));
|
|
1739
|
+
// Same cross-OS shape as the guard entry: bash prefix + QUOTED path
|
|
1740
|
+
// (spaces-safe on unix, Git Bash on Windows). timeout is SECONDS.
|
|
1741
|
+
filtered.push({
|
|
1742
|
+
hooks: [
|
|
1743
|
+
{
|
|
1744
|
+
type: "command",
|
|
1745
|
+
command: 'bash "$CLAUDE_PROJECT_DIR/.claude/hooks/vexp-hint.sh"',
|
|
1746
|
+
timeout: 5,
|
|
1747
|
+
},
|
|
1748
|
+
],
|
|
1749
|
+
});
|
|
1750
|
+
const merged = { ...hooks, UserPromptSubmit: filtered };
|
|
1751
|
+
const settingsIdentical = JSON.stringify(merged) === JSON.stringify(settings.hooks ?? {});
|
|
1752
|
+
if (scriptIdentical && settingsIdentical)
|
|
1753
|
+
return null;
|
|
1754
|
+
settings.hooks = merged;
|
|
1755
|
+
if (!settingsIdentical) {
|
|
1756
|
+
if (read.existed)
|
|
1757
|
+
backupConfig(settingsPath);
|
|
1758
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
1759
|
+
}
|
|
1760
|
+
return existed ? "updated" : "created";
|
|
1761
|
+
}
|
|
1762
|
+
/**
|
|
1763
|
+
* opencode/Kilo hint plugin (2.4.0): default-ON, non-blocking, fail-open —
|
|
1764
|
+
* unlike the deny guard (strict-only), this cannot break a session; it only
|
|
1765
|
+
* ever appends an orientation line the daemon judged useful.
|
|
1766
|
+
*/
|
|
1767
|
+
export function installOpencodeHintPlugin(workspaceRoot, binaryPath, pluginDir) {
|
|
1768
|
+
const pluginPath = path.join(workspaceRoot, pluginDir, "vexp-hint.js");
|
|
1769
|
+
const content = vexpOpencodeHintPlugin(binaryPath);
|
|
1770
|
+
fs.mkdirSync(path.dirname(pluginPath), { recursive: true });
|
|
1771
|
+
const existed = fs.existsSync(pluginPath);
|
|
1772
|
+
if (existed && fs.readFileSync(pluginPath, "utf-8") === content)
|
|
1773
|
+
return null;
|
|
1774
|
+
fs.writeFileSync(pluginPath, content);
|
|
1775
|
+
return existed ? "updated" : "created";
|
|
1776
|
+
}
|
|
1777
|
+
/**
|
|
1778
|
+
* Codex hint hook (2.4.0). Codex >=0.129 discovers `<repo>/.codex/hooks.json`
|
|
1779
|
+
* and its UserPromptSubmit stdout envelope is byte-identical to Claude
|
|
1780
|
+
* Code's (hookSpecificOutput.additionalContext), so the same prompt-hint
|
|
1781
|
+
* backend serves both: PROJECT-scope file, no global config touched.
|
|
1782
|
+
* Fail-open: missing bash/binary/daemon => no output => vanilla.
|
|
1783
|
+
*/
|
|
1784
|
+
export function installCodexHintHook(workspaceRoot, binaryPath) {
|
|
1785
|
+
const dir = path.join(workspaceRoot, ".codex");
|
|
1786
|
+
const hookPath = path.join(dir, "vexp-hint.sh");
|
|
1787
|
+
const hooksJsonPath = path.join(dir, "hooks.json");
|
|
1788
|
+
const script = vexpHintHookScript(binaryPath);
|
|
1789
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1790
|
+
const existed = fs.existsSync(hookPath);
|
|
1791
|
+
const scriptIdentical = existed && fs.readFileSync(hookPath, "utf-8") === script;
|
|
1792
|
+
if (!scriptIdentical) {
|
|
1793
|
+
fs.writeFileSync(hookPath, script, { mode: 0o755 });
|
|
1794
|
+
}
|
|
1795
|
+
let root = {};
|
|
1796
|
+
if (fs.existsSync(hooksJsonPath)) {
|
|
1797
|
+
try {
|
|
1798
|
+
root = JSON.parse(fs.readFileSync(hooksJsonPath, "utf-8"));
|
|
1799
|
+
}
|
|
1800
|
+
catch {
|
|
1801
|
+
warnUnparseable(hooksJsonPath);
|
|
1802
|
+
return scriptIdentical ? null : existed ? "updated" : "created";
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
// Codex hooks.json shape: EVENT NAME at top level (no "hooks" wrapper).
|
|
1806
|
+
const existing = Array.isArray(root.UserPromptSubmit)
|
|
1807
|
+
? root.UserPromptSubmit
|
|
1808
|
+
: [];
|
|
1809
|
+
const filtered = existing.filter((h) => !isVexpHintHookEntry(h));
|
|
1810
|
+
// Absolute path: Codex has no $CLAUDE_PROJECT_DIR substitution.
|
|
1811
|
+
filtered.push({
|
|
1812
|
+
hooks: [
|
|
1813
|
+
{
|
|
1814
|
+
type: "command",
|
|
1815
|
+
command: `bash "${hookPath.replace(/\\/g, "/")}"`,
|
|
1816
|
+
timeout: 5,
|
|
1817
|
+
},
|
|
1818
|
+
],
|
|
1819
|
+
});
|
|
1820
|
+
const before = JSON.stringify(root.UserPromptSubmit ?? null);
|
|
1821
|
+
root.UserPromptSubmit = filtered;
|
|
1822
|
+
const identical = before === JSON.stringify(filtered);
|
|
1823
|
+
if (scriptIdentical && identical)
|
|
1824
|
+
return null;
|
|
1825
|
+
if (!identical) {
|
|
1826
|
+
fs.writeFileSync(hooksJsonPath, JSON.stringify(root, null, 2) + "\n");
|
|
1827
|
+
}
|
|
1828
|
+
return existed ? "updated" : "created";
|
|
1829
|
+
}
|
|
1670
1830
|
// ---------------------------------------------------------------------------
|
|
1671
1831
|
// opencode plugin - blocks grep/glob (and shelled-out search) when the daemon
|
|
1672
1832
|
// is healthy. opencode has no PreToolUse hook, but auto-loads plugins from
|
|
@@ -1929,90 +2089,30 @@ function generateAgentConfig(template, vars) {
|
|
|
1929
2089
|
// ---------------------------------------------------------------------------
|
|
1930
2090
|
function claudeCodeTemplate(vars) {
|
|
1931
2091
|
return `## vexp - Context-Aware AI Coding <!-- vexp v${vars.version} -->
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
Division of labor (each tool where it wins):
|
|
1940
|
-
- **Architecture, impact, "where is X wired"**: \`run_pipeline\` - the graph
|
|
1941
|
-
answers these better and cheaper than exploratory reading.
|
|
1942
|
-
- **Literal text sweeps** (string constants, log messages, config keys, e.g.
|
|
1943
|
-
\`localStorage\`, API paths): Grep/Glob directly - text search is what they
|
|
1944
|
-
are best at, do NOT route text sweeps through vexp.
|
|
1945
|
-
- **Editing**: Read the files you are about to modify; use \`get_skeleton\`
|
|
1946
|
-
(detail: minimal/standard/detailed, 70-90% token savings) for files you only
|
|
2092
|
+
vexp runs entirely on this machine: local daemon, index in \`.vexp/\`.
|
|
2093
|
+
\`run_pipeline\` transmits nothing to any external service.
|
|
2094
|
+
- \`run_pipeline({ "task": "..." })\` - orientation in one call (ranked pivot
|
|
2095
|
+
files with line ranges + blast radius + session notes) when a task does NOT
|
|
2096
|
+
name the files/symbols to touch. If it does, SKIP vexp - use your normal tools.
|
|
2097
|
+
- \`get_skeleton\` - file structure at 70-90% token savings for files you only
|
|
1947
2098
|
need to understand, not edit.
|
|
2099
|
+
- vexp may append a one-line hint to a prompt when orientation would help;
|
|
2100
|
+
otherwise it stays silent.
|
|
1948
2101
|
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
vexp only covers indexed source inside the workspace: for runtime logs, build
|
|
1953
|
-
output (dist/, .vite/, node_modules/) or files outside the repo use Bash/Read.
|
|
1954
|
-
vexp runs entirely on this machine: local daemon, local index stored inside the
|
|
1955
|
-
workspace (\`.vexp/\`). \`run_pipeline\` transmits nothing to any external service -
|
|
1956
|
-
treat it like a local build tool; no data-sharing consent is needed to call it.
|
|
1957
|
-
|
|
1958
|
-
### Primary Tool
|
|
1959
|
-
- \`run_pipeline\` - **USE THIS FOR EVERYTHING**. Single call that runs
|
|
1960
|
-
capsule + impact + memory server-side. Returns compressed results.
|
|
1961
|
-
Auto-detects intent (debug/modify/refactor/explore) from your task.
|
|
1962
|
-
Includes full file content for pivots.
|
|
1963
|
-
Examples:
|
|
1964
|
-
- \`run_pipeline({ "task": "fix JWT validation bug" })\` - auto-detect
|
|
1965
|
-
- \`run_pipeline({ "task": "refactor db layer", "preset": "refactor" })\` - explicit
|
|
1966
|
-
- \`run_pipeline({ "task": "add auth", "observation": "using JWT" })\` - save insight in same call
|
|
1967
|
-
|
|
1968
|
-
### Other MCP tools (use only when run_pipeline is insufficient)
|
|
1969
|
-
- \`get_skeleton\` - **preferred over Read** for inspecting files (minimal/standard/detailed detail levels, 70-90% token savings)
|
|
1970
|
-
- \`index_status\` - indexing status and health check
|
|
1971
|
-
- \`expand_vexp_ref\` - expand V-REF hash placeholders in v2 compact output
|
|
1972
|
-
|
|
1973
|
-
${QUERY_SHAPE}
|
|
1974
|
-
|
|
1975
|
-
### Workflow
|
|
1976
|
-
1. \`run_pipeline("your task")\` - ONCE at task start. Returns pivots + impact + memories in 1 call
|
|
1977
|
-
2. Literal string sweeps? Grep/Glob directly. Editing a file? Read it directly.
|
|
1978
|
-
3. Structural overview of a non-edit file? \`get_skeleton({ files: [...], detail: "detailed" })\`
|
|
1979
|
-
4. Make targeted changes based on the context returned
|
|
1980
|
-
5. \`run_pipeline\` again ONLY when the task moves to a new area - do NOT chain vexp calls per turn
|
|
1981
|
-
|
|
1982
|
-
### Subagent / Explore / Plan mode
|
|
1983
|
-
- Subagents CAN call \`run_pipeline\` - always include the task description
|
|
1984
|
-
- Before spawning Agent(Explore) for architecture questions, call \`run_pipeline\`
|
|
1985
|
-
and pass the returned context into the agent prompt - it usually replaces the
|
|
1986
|
-
exploration entirely
|
|
1987
|
-
|
|
1988
|
-
### Smart Features (automatic - no action needed)
|
|
1989
|
-
- **Intent Detection**: auto-detects from your task keywords. "fix bug" -> Debug, "refactor" -> blast-radius, "add" -> Modify
|
|
1990
|
-
- **Hybrid Search**: keyword + semantic + graph centrality ranking
|
|
1991
|
-
- **Session Memory**: auto-captures observations; memories auto-surfaced in results
|
|
1992
|
-
- **LSP Bridge**: VS Code captures type-resolved call edges
|
|
1993
|
-
- **Change Coupling**: co-changed files included as related context
|
|
1994
|
-
|
|
1995
|
-
### Advanced Parameters
|
|
1996
|
-
- \`preset: "debug"\` - forces debug mode (capsule+tests+impact+memory)
|
|
1997
|
-
- \`preset: "refactor"\` - deep impact analysis (depth 5)
|
|
1998
|
-
- \`max_tokens: 12000\` - increase total budget for complex tasks
|
|
1999
|
-
- \`include_tests: true\` - include test files in results
|
|
2000
|
-
- \`include_file_content: false\` - omit full file content (lighter response)
|
|
2001
|
-
|
|
2002
|
-
### Fallback
|
|
2003
|
-
If \`run_pipeline\` returns \`status: "degraded"\` or 0 pivots with an INDEX EMPTY warning,
|
|
2004
|
-
the index is empty or rebuilding. Use Grep, Glob, and Read directly until the index is ready.
|
|
2005
|
-
|
|
2006
|
-
### Multi-Repo Workspaces
|
|
2007
|
-
\`run_pipeline\` auto-queries all indexed repos. Use \`repos: ["alias"]\` to scope.
|
|
2008
|
-
Use \`index_status\` to discover available repo aliases.
|
|
2102
|
+
### Query shape (do this)
|
|
2103
|
+
Anchor the task on real identifiers (ClassName, functionName) or file paths:
|
|
2104
|
+
\`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
|
|
2009
2105
|
<!-- /vexp -->`;
|
|
2010
2106
|
}
|
|
2011
2107
|
function cursorTemplate(vars) {
|
|
2012
2108
|
return `## vexp rules for Cursor <!-- vexp v${vars.version} -->
|
|
2013
2109
|
|
|
2014
2110
|
**Call \`run_pipeline\` ONCE at task start for orientation - then use your normal tools.**
|
|
2111
|
+
If the task already names the files/symbols to touch, skip vexp and work
|
|
2112
|
+
with your normal tools - a skipped call costs nothing.
|
|
2015
2113
|
vexp returns graph-ranked context (relevant files, blast radius, memories) in one call.
|
|
2114
|
+
On non-trivial tasks pass \`eager: true\`: the single response then carries full file
|
|
2115
|
+
bodies, covering tests and impact - work directly from it instead of re-reading sources.
|
|
2016
2116
|
For literal string sweeps (constants, log messages, config keys) use native search
|
|
2017
2117
|
directly - do NOT route text sweeps through vexp.
|
|
2018
2118
|
vexp runs entirely on this machine: local daemon, local index stored inside the
|
|
@@ -2032,6 +2132,10 @@ treat it like a local build tool; no data-sharing consent is needed to call it.
|
|
|
2032
2132
|
- \`index_status\` - indexing status
|
|
2033
2133
|
- \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
|
|
2034
2134
|
|
|
2135
|
+
(Recommended core set, not the full schema: paid plans advertise 14 MCP tools -
|
|
2136
|
+
capsule, impact graph, logic flow, memory and more. \`run_pipeline\` already runs
|
|
2137
|
+
those server-side, so the four above cover the normal workflow.)
|
|
2138
|
+
|
|
2035
2139
|
${QUERY_SHAPE}
|
|
2036
2140
|
|
|
2037
2141
|
### Agentic search
|
|
@@ -2059,7 +2163,11 @@ function windsurfTemplate(vars) {
|
|
|
2059
2163
|
return `## vexp for Windsurf <!-- vexp v${vars.version} -->
|
|
2060
2164
|
|
|
2061
2165
|
**Call \`run_pipeline\` ONCE at task start for orientation - then use your normal tools.**
|
|
2166
|
+
If the task already names the files/symbols to touch, skip vexp and work
|
|
2167
|
+
with your normal tools - a skipped call costs nothing.
|
|
2062
2168
|
vexp returns graph-ranked context (relevant files, blast radius, memories) in one call.
|
|
2169
|
+
On non-trivial tasks pass \`eager: true\`: the single response then carries full file
|
|
2170
|
+
bodies, covering tests and impact - work directly from it instead of re-reading sources.
|
|
2063
2171
|
For literal string sweeps (constants, log messages, config keys) use native search
|
|
2064
2172
|
directly - do NOT route text sweeps through vexp.
|
|
2065
2173
|
vexp runs entirely on this machine: local daemon, local index stored inside the
|
|
@@ -2079,6 +2187,10 @@ treat it like a local build tool; no data-sharing consent is needed to call it.
|
|
|
2079
2187
|
- \`index_status\` - indexing status
|
|
2080
2188
|
- \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
|
|
2081
2189
|
|
|
2190
|
+
(Recommended core set, not the full schema: paid plans advertise 14 MCP tools -
|
|
2191
|
+
capsule, impact graph, logic flow, memory and more. \`run_pipeline\` already runs
|
|
2192
|
+
those server-side, so the four above cover the normal workflow.)
|
|
2193
|
+
|
|
2082
2194
|
${QUERY_SHAPE}
|
|
2083
2195
|
|
|
2084
2196
|
### Agentic search
|
|
@@ -2117,7 +2229,11 @@ function augmentTemplate(vars) {
|
|
|
2117
2229
|
return `## vexp for Augment <!-- vexp v${vars.version} -->
|
|
2118
2230
|
|
|
2119
2231
|
**Call \`run_pipeline\` ONCE at task start for orientation - then use your normal tools.**
|
|
2232
|
+
If the task already names the files/symbols to touch, skip vexp and work
|
|
2233
|
+
with your normal tools - a skipped call costs nothing.
|
|
2120
2234
|
vexp returns graph-ranked context (relevant files, blast radius, memories) in one call.
|
|
2235
|
+
On non-trivial tasks pass \`eager: true\`: the single response then carries full file
|
|
2236
|
+
bodies, covering tests and impact - work directly from it instead of re-reading sources.
|
|
2121
2237
|
For literal string sweeps (constants, log messages, config keys) use native search
|
|
2122
2238
|
directly - do NOT route text sweeps through vexp.
|
|
2123
2239
|
vexp runs entirely on this machine: local daemon, local index stored inside the
|
|
@@ -2137,6 +2253,10 @@ When working on this codebase:
|
|
|
2137
2253
|
- \`index_status\` - indexing status
|
|
2138
2254
|
- \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
|
|
2139
2255
|
|
|
2256
|
+
(Recommended core set, not the full schema: paid plans advertise 14 MCP tools -
|
|
2257
|
+
capsule, impact graph, logic flow, memory and more. \`run_pipeline\` already runs
|
|
2258
|
+
those server-side, so the four above cover the normal workflow.)
|
|
2259
|
+
|
|
2140
2260
|
${QUERY_SHAPE}
|
|
2141
2261
|
|
|
2142
2262
|
### Agentic search
|
|
@@ -2156,7 +2276,11 @@ function copilotTemplate(vars) {
|
|
|
2156
2276
|
return `## vexp context tools <!-- vexp v${vars.version} -->
|
|
2157
2277
|
|
|
2158
2278
|
**Call \`run_pipeline\` ONCE at task start for orientation - then use your normal tools.**
|
|
2279
|
+
If the task already names the files/symbols to touch, skip vexp and work
|
|
2280
|
+
with your normal tools - a skipped call costs nothing.
|
|
2159
2281
|
vexp returns graph-ranked context (relevant files, blast radius, memories) in one call.
|
|
2282
|
+
On non-trivial tasks pass \`eager: true\`: the single response then carries full file
|
|
2283
|
+
bodies, covering tests and impact - work directly from it instead of re-reading sources.
|
|
2160
2284
|
For literal string sweeps (constants, log messages, config keys) use native search
|
|
2161
2285
|
directly - do NOT route text sweeps through vexp.
|
|
2162
2286
|
vexp runs entirely on this machine: local daemon, local index stored inside the
|
|
@@ -2176,6 +2300,10 @@ treat it like a local build tool; no data-sharing consent is needed to call it.
|
|
|
2176
2300
|
- \`index_status\` - indexing status
|
|
2177
2301
|
- \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
|
|
2178
2302
|
|
|
2303
|
+
(Recommended core set, not the full schema: paid plans advertise 14 MCP tools -
|
|
2304
|
+
capsule, impact graph, logic flow, memory and more. \`run_pipeline\` already runs
|
|
2305
|
+
those server-side, so the four above cover the normal workflow.)
|
|
2306
|
+
|
|
2179
2307
|
${QUERY_SHAPE}
|
|
2180
2308
|
|
|
2181
2309
|
### Agentic search
|
|
@@ -2195,7 +2323,11 @@ function zedTemplate(vars) {
|
|
|
2195
2323
|
return `## vexp for Zed <!-- vexp v${vars.version} -->
|
|
2196
2324
|
|
|
2197
2325
|
**Call \`run_pipeline\` ONCE at task start for orientation - then use your normal tools.**
|
|
2326
|
+
If the task already names the files/symbols to touch, skip vexp and work
|
|
2327
|
+
with your normal tools - a skipped call costs nothing.
|
|
2198
2328
|
vexp returns graph-ranked context (relevant files, blast radius, memories) in one call.
|
|
2329
|
+
On non-trivial tasks pass \`eager: true\`: the single response then carries full file
|
|
2330
|
+
bodies, covering tests and impact - work directly from it instead of re-reading sources.
|
|
2199
2331
|
For literal string sweeps (constants, log messages, config keys) use native search
|
|
2200
2332
|
directly - do NOT route text sweeps through vexp.
|
|
2201
2333
|
vexp runs entirely on this machine: local daemon, local index stored inside the
|
|
@@ -2215,6 +2347,10 @@ treat it like a local build tool; no data-sharing consent is needed to call it.
|
|
|
2215
2347
|
- \`index_status\` - indexing status
|
|
2216
2348
|
- \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
|
|
2217
2349
|
|
|
2350
|
+
(Recommended core set, not the full schema: paid plans advertise 14 MCP tools -
|
|
2351
|
+
capsule, impact graph, logic flow, memory and more. \`run_pipeline\` already runs
|
|
2352
|
+
those server-side, so the four above cover the normal workflow.)
|
|
2353
|
+
|
|
2218
2354
|
${QUERY_SHAPE}
|
|
2219
2355
|
|
|
2220
2356
|
### Agentic search
|
|
@@ -2234,7 +2370,11 @@ function agentsMdTemplate(vars) {
|
|
|
2234
2370
|
return `## vexp <!-- vexp v${vars.version} -->
|
|
2235
2371
|
|
|
2236
2372
|
**Call \`run_pipeline\` ONCE at task start for orientation - then use your normal tools.**
|
|
2373
|
+
If the task already names the files/symbols to touch, skip vexp and work
|
|
2374
|
+
with your normal tools - a skipped call costs nothing.
|
|
2237
2375
|
vexp returns graph-ranked context (relevant files, blast radius, memories) in one call.
|
|
2376
|
+
On non-trivial tasks pass \`eager: true\`: the single response then carries full file
|
|
2377
|
+
bodies, covering tests and impact - work directly from it instead of re-reading sources.
|
|
2238
2378
|
For literal string sweeps (constants, log messages, config keys) use native search
|
|
2239
2379
|
directly - do NOT route text sweeps through vexp.
|
|
2240
2380
|
vexp runs entirely on this machine: local daemon, local index stored inside the
|
|
@@ -2276,7 +2416,11 @@ function kiroTemplate(vars) {
|
|
|
2276
2416
|
return `# vexp steering <!-- vexp v${vars.version} -->
|
|
2277
2417
|
|
|
2278
2418
|
**Call \`run_pipeline\` ONCE at task start for orientation - then use your normal tools.**
|
|
2419
|
+
If the task already names the files/symbols to touch, skip vexp and work
|
|
2420
|
+
with your normal tools - a skipped call costs nothing.
|
|
2279
2421
|
vexp returns graph-ranked context (relevant files, blast radius, memories) in one call.
|
|
2422
|
+
On non-trivial tasks pass \`eager: true\`: the single response then carries full file
|
|
2423
|
+
bodies, covering tests and impact - work directly from it instead of re-reading sources.
|
|
2280
2424
|
For literal string sweeps (constants, log messages, config keys) use native search
|
|
2281
2425
|
directly - do NOT route text sweeps through vexp.
|
|
2282
2426
|
vexp runs entirely on this machine: local daemon, local index stored inside the
|
|
@@ -2296,6 +2440,10 @@ treat it like a local build tool; no data-sharing consent is needed to call it.
|
|
|
2296
2440
|
- \`index_status\` - indexing status
|
|
2297
2441
|
- \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
|
|
2298
2442
|
|
|
2443
|
+
(Recommended core set, not the full schema: paid plans advertise 14 MCP tools -
|
|
2444
|
+
capsule, impact graph, logic flow, memory and more. \`run_pipeline\` already runs
|
|
2445
|
+
those server-side, so the four above cover the normal workflow.)
|
|
2446
|
+
|
|
2299
2447
|
${QUERY_SHAPE}
|
|
2300
2448
|
|
|
2301
2449
|
## Agentic search
|
|
@@ -2315,6 +2463,8 @@ function genericTemplate(vars) {
|
|
|
2315
2463
|
return `## vexp - Context-Aware AI Coding <!-- vexp v${vars.version} -->
|
|
2316
2464
|
|
|
2317
2465
|
### Context strategy: call run_pipeline ONCE at task start
|
|
2466
|
+
If the task already names the files/symbols to touch, skip vexp and work
|
|
2467
|
+
with your normal tools - a skipped call costs nothing.
|
|
2318
2468
|
For every multi-file task - bug fixes, features, refactors, questions about the code:
|
|
2319
2469
|
**start with one \`run_pipeline\` call**. It runs context search + impact analysis +
|
|
2320
2470
|
memory recall in a single call and returns compressed, graph-ranked results: the
|
package/dist/doctor.js
CHANGED
|
@@ -151,13 +151,43 @@ export async function runDoctor() {
|
|
|
151
151
|
if (st.llm_configured_but_inactive === true) {
|
|
152
152
|
line(WARN, `local LLM is installed and enabled in config but this daemon runs the RULE compressor — results are not LLM-compressed. Run 'vexp daemon-cmd restart' to load the model.`);
|
|
153
153
|
}
|
|
154
|
+
// 2.4.0 upgrade guard (CLI side): a daemon surviving an upgrade keeps
|
|
155
|
+
// serving the OLD feature set and its gaps read as product bugs
|
|
156
|
+
// (field case: pre-ledger daemon after the 2.4.0 install). The VS Code
|
|
157
|
+
// extension restarts automatically; CLI users get told explicitly.
|
|
158
|
+
try {
|
|
159
|
+
const { getBinaryPath } = await import("./binary.js");
|
|
160
|
+
const { execFileSync } = await import("node:child_process");
|
|
161
|
+
const out = execFileSync(getBinaryPath(), ["--version"], { timeout: 5000, encoding: "utf8" });
|
|
162
|
+
const bundled = out.trim().split(/\s+/).pop();
|
|
163
|
+
const running = st.daemon_version;
|
|
164
|
+
if (bundled && running && bundled !== running) {
|
|
165
|
+
line(WARN, `daemon is v${running} but the installed binary is v${bundled} — this workspace is still served by the OLD version. Run 'vexp daemon-cmd restart' to upgrade it now.`);
|
|
166
|
+
}
|
|
167
|
+
else if (st.binary_stale === true) {
|
|
168
|
+
line(WARN, `daemon is running a deleted executable (upgraded on disk) — run 'vexp daemon-cmd restart' to load the new build.`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
catch { /* best-effort */ }
|
|
154
172
|
// 2.3 C1 — which agent sessions actually used vexp (a session with zero
|
|
155
173
|
// calls never shows up here; that absence is the diagnostic).
|
|
156
174
|
const sessions = Array.isArray(st.sessions) ? st.sessions : [];
|
|
175
|
+
// 2.3.3 Savings Ledger — activity counts even when no tool is ever
|
|
176
|
+
// called: silence on oriented prompts is vexp WORKING, and the old
|
|
177
|
+
// "never used" warning on quiet-but-active daemons generated real
|
|
178
|
+
// support tickets ("is it working? it never got called").
|
|
179
|
+
const ledger = (st.ledger ?? {});
|
|
180
|
+
const analyzed = Number(ledger.prompts_analyzed) || 0;
|
|
181
|
+
if (analyzed > 0) {
|
|
182
|
+
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`);
|
|
183
|
+
}
|
|
157
184
|
if (sessions.length > 0) {
|
|
158
185
|
const total = sessions.reduce((n, s) => n + (Number(s.pipeline_calls) || 0), 0);
|
|
159
186
|
line(OK, `sessions (4h): ${sessions.length} active, ${total} pipeline calls total`);
|
|
160
187
|
}
|
|
188
|
+
else if (analyzed > 0) {
|
|
189
|
+
line(OK, `no tool calls in the last 4h — but the ledger above shows vexp is analyzing prompts (zero calls is normal on oriented tasks)`);
|
|
190
|
+
}
|
|
161
191
|
else if (Number(st.daemon_uptime_s) > 600) {
|
|
162
192
|
// Embedded caveat: a stdio MCP server (`vexp-core mcp`) spawned while
|
|
163
193
|
// the daemon was unreachable serves from its own in-process index and
|
|
@@ -207,6 +237,12 @@ export async function runDoctor() {
|
|
|
207
237
|
// 3) License (fresh.jwt rolling token vs license.jwt).
|
|
208
238
|
console.log(chalk.bold("\nLicense tokens (~/.vexp)"));
|
|
209
239
|
const now = Math.floor(Date.now() / 1000);
|
|
240
|
+
// Either valid token keeps the plan ACTIVE. An expired license.jwt next to
|
|
241
|
+
// a valid fresh.jwt is normal steady-state (the long token re-rolls on the
|
|
242
|
+
// next online validation) — labeling it a bare WARN read as "my license
|
|
243
|
+
// expired" and generated tickets from perfectly licensed users.
|
|
244
|
+
const freshExp = jwtExp(path.join(home, ".vexp", "fresh.jwt"));
|
|
245
|
+
const freshValid = freshExp != null && freshExp > now;
|
|
210
246
|
for (const name of ["fresh.jwt", "license.jwt"]) {
|
|
211
247
|
const p = path.join(home, ".vexp", name);
|
|
212
248
|
if (!fs.existsSync(p)) {
|
|
@@ -220,7 +256,15 @@ export async function runDoctor() {
|
|
|
220
256
|
}
|
|
221
257
|
const days = Math.round((exp - now) / 86400);
|
|
222
258
|
if (exp < now) {
|
|
223
|
-
|
|
259
|
+
if (name === "fresh.jwt") {
|
|
260
|
+
line(OK, `${name}: expired ${-days}d ago (benign — rolling token, falls back to license.jwt)`);
|
|
261
|
+
}
|
|
262
|
+
else if (freshValid) {
|
|
263
|
+
line(OK, `${name}: long token expired ${-days}d ago — plan still ACTIVE via fresh.jwt; it renews automatically on the next online validation`);
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
line(WARN, `${name}: expired ${-days}d ago and no valid fresh token — plan features may be limited; go online or re-activate the license`);
|
|
267
|
+
}
|
|
224
268
|
}
|
|
225
269
|
else {
|
|
226
270
|
line(OK, `${name}: valid, ~${days}d remaining`);
|
|
@@ -310,31 +354,39 @@ export async function runDoctor() {
|
|
|
310
354
|
else if (guardHooks.length === 0) {
|
|
311
355
|
line(OK, "no vexp guard configured (2.3 default — enable with 'vexp setup --guard-strict')");
|
|
312
356
|
}
|
|
313
|
-
else if (process.platform === "win32") {
|
|
314
|
-
line(OK, `guard configured (${guardHooks.length} entry) — live execution check skipped on Windows`);
|
|
315
|
-
}
|
|
316
357
|
else {
|
|
317
358
|
for (const h of guardHooks) {
|
|
318
359
|
const cmd = h.command;
|
|
319
360
|
const execForm = Array.isArray(h.args);
|
|
320
361
|
const timeoutS = typeof h.timeout === "number" ? h.timeout : 600;
|
|
321
362
|
if (!execForm && /\$\{?CLAUDE_PROJECT_DIR\}?\//.test(cmd) && !cmd.includes('"')) {
|
|
322
|
-
line(ws.root.includes(" ") ? BAD : WARN, `shell-form hook command
|
|
363
|
+
line(ws.root.includes(" ") ? BAD : WARN, `shell-form hook command with unquoted $CLAUDE_PROJECT_DIR — word-splits on paths with spaces${ws.root.includes(" ") ? ` and THIS project path has one: the guard never runs` : ""}. Re-run 'vexp setup --guard-strict' to rewrite with a quoted path.`);
|
|
364
|
+
}
|
|
365
|
+
if (execForm && process.platform === "win32") {
|
|
366
|
+
// Exec form spawns the .sh directly, which Windows cannot do at
|
|
367
|
+
// all — the entry LOOKS installed and enforces nothing. This was
|
|
368
|
+
// invisible for days because doctor used to skip the live check
|
|
369
|
+
// on Windows entirely.
|
|
370
|
+
line(BAD, `exec-form hook entry ('args' present) cannot run a .sh on Windows — the guard fails open. Re-run 'vexp setup --guard-strict' to rewrite it (bash-prefixed shell form).`);
|
|
323
371
|
}
|
|
324
372
|
if (timeoutS > 600) {
|
|
325
373
|
line(WARN, `hook timeout ${timeoutS} is in SECONDS (${Math.round(timeoutS / 60)} minutes) — likely meant milliseconds. Re-run 'vexp setup --guard-strict' to fix.`);
|
|
326
374
|
}
|
|
327
375
|
// Run it exactly as Claude Code would: exec form = direct spawn with
|
|
328
|
-
// the placeholder substituted by the host; shell form =
|
|
329
|
-
// CLAUDE_PROJECT_DIR in the environment.
|
|
376
|
+
// the placeholder substituted by the host; shell form = a shell with
|
|
377
|
+
// CLAUDE_PROJECT_DIR in the environment. On Windows Claude Code runs
|
|
378
|
+
// shell-form hooks through Git Bash, so probe via bash there too —
|
|
379
|
+
// if bash is missing, that IS the finding (Claude Code itself
|
|
380
|
+
// requires Git Bash on Windows).
|
|
330
381
|
const substituted = cmd.replace(/\$\{CLAUDE_PROJECT_DIR\}/g, ws.root);
|
|
382
|
+
const shell = process.platform === "win32" ? "bash" : "sh";
|
|
331
383
|
const r = execForm
|
|
332
384
|
? spawnSync(substituted, h.args.map((a) => String(a).replace(/\$\{CLAUDE_PROJECT_DIR\}/g, ws.root)), {
|
|
333
385
|
env: { ...process.env, CLAUDE_PROJECT_DIR: ws.root },
|
|
334
386
|
timeout: 5000,
|
|
335
387
|
encoding: "utf-8",
|
|
336
388
|
})
|
|
337
|
-
: spawnSync(
|
|
389
|
+
: spawnSync(shell, ["-c", cmd], {
|
|
338
390
|
env: { ...process.env, CLAUDE_PROJECT_DIR: ws.root },
|
|
339
391
|
timeout: 5000,
|
|
340
392
|
encoding: "utf-8",
|