vexp-cli 2.6.3 → 3.0.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 -0
- package/dist/agent-config.js +594 -454
- package/dist/cli.js +18 -11
- package/dist/doctor.js +147 -34
- package/dist/hook-template.js +226 -1
- package/dist/license.js +6 -2
- package/dist/secret-prompt.js +62 -0
- package/mcp/mcp-server.cjs +43 -41
- 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, vexpHintHookScript, vexpStopGateHookScript, vexpSessionContextHookScript, vexpOpencodeHintPlugin } from "./hook-template.js";
|
|
13
|
+
import { VEXP_GUARD_HOOK, VEXP_OPENCODE_GUARD, VEXP_CURSOR_GUARD, vexpHintHookScript, vexpSearchHookScript, vexpHintHookCmdScript, vexpStopGateHookScript, vexpSessionContextHookScript, vexpOpencodeHintPlugin, vexpOpencodeCompressPlugin, bakeEditHintHook, bakeReadHintHook, bakeBashCapHook } from "./hook-template.js";
|
|
14
14
|
// ---------------------------------------------------------------------------
|
|
15
15
|
// Constants
|
|
16
16
|
// ---------------------------------------------------------------------------
|
|
@@ -102,6 +102,13 @@ export function setGuardMode(mode) {
|
|
|
102
102
|
export function guardMode() {
|
|
103
103
|
return activeGuardMode;
|
|
104
104
|
}
|
|
105
|
+
let activeInterventionMode = "off";
|
|
106
|
+
export function setInterventionMode(mode) {
|
|
107
|
+
activeInterventionMode = mode;
|
|
108
|
+
}
|
|
109
|
+
export function interventionMode() {
|
|
110
|
+
return activeInterventionMode;
|
|
111
|
+
}
|
|
105
112
|
// ---------------------------------------------------------------------------
|
|
106
113
|
// Agent detectors - mirrors VS Code extension's AGENT_DETECTORS
|
|
107
114
|
// ---------------------------------------------------------------------------
|
|
@@ -123,7 +130,7 @@ const AGENT_DETECTORS = [
|
|
|
123
130
|
{
|
|
124
131
|
agent: "Windsurf",
|
|
125
132
|
detectPath: ".windsurf",
|
|
126
|
-
configFile: ".windsurf/rules.md",
|
|
133
|
+
configFile: ".windsurf/rules/vexp.md",
|
|
127
134
|
templateName: "windsurf",
|
|
128
135
|
mcpConfigFile: ".windsurf/mcp.json",
|
|
129
136
|
},
|
|
@@ -244,7 +251,7 @@ const AGENT_DETECTORS = [
|
|
|
244
251
|
* true but badly incomplete story. Kept next to the installers, with a
|
|
245
252
|
* lockstep test asserting every detector has an entry.
|
|
246
253
|
*/
|
|
247
|
-
export function plannedWrites(agent, guard = guardMode()) {
|
|
254
|
+
export function plannedWrites(agent, guard = guardMode(), interventions = interventionMode()) {
|
|
248
255
|
const det = AGENT_DETECTORS.find((d) => d.agent === agent);
|
|
249
256
|
if (!det)
|
|
250
257
|
return [];
|
|
@@ -255,8 +262,11 @@ export function plannedWrites(agent, guard = guardMode()) {
|
|
|
255
262
|
case "Claude Code":
|
|
256
263
|
out.push("~/.claude.json (MCP server entry, user scope)");
|
|
257
264
|
out.push(".claude/hooks/vexp-hint.sh + settings.json hooks.UserPromptSubmit (orientation)");
|
|
258
|
-
out.push(".claude/hooks/vexp-verify.sh + settings.json hooks.Stop (verification gate)");
|
|
259
265
|
out.push(".claude/hooks/vexp-restore.sh + settings.json hooks.SessionStart (context restore)");
|
|
266
|
+
if (interventions === "on") {
|
|
267
|
+
out.push(".claude/hooks/vexp-verify.sh + settings.json hooks.Stop (verification gate, opt-in)");
|
|
268
|
+
out.push(".claude/hooks/vexp-edit-hint.sh + settings.json hooks.PostToolUse (coupling, opt-in)");
|
|
269
|
+
}
|
|
260
270
|
if (guard === "strict") {
|
|
261
271
|
out.push(".claude/hooks/vexp-guard.sh + settings.json hooks.PreToolUse (guard, opt-in)");
|
|
262
272
|
}
|
|
@@ -334,6 +344,14 @@ export function configureAgents(workspaceRoot, binaryPath, version, agentFilter,
|
|
|
334
344
|
for (const detector of agents) {
|
|
335
345
|
const configFilePath = path.join(workspaceRoot, detector.configFile);
|
|
336
346
|
const alreadyExists = fs.existsSync(configFilePath);
|
|
347
|
+
// Sweep the location we used to write to. For several releases the
|
|
348
|
+
// Windsurf rules went to `.windsurf/rules.md`, which Cascade does not
|
|
349
|
+
// read: its rules are `.windsurf/rules/*.md`, `.devin/rules/*.md`, or the
|
|
350
|
+
// legacy root `.windsurfrules`. Upgrading users would otherwise keep a
|
|
351
|
+
// stale, unread copy of our instructions beside the live one.
|
|
352
|
+
if (detector.agent === "Windsurf") {
|
|
353
|
+
removeVexpSection(path.join(workspaceRoot, ".windsurf", "rules.md"));
|
|
354
|
+
}
|
|
337
355
|
const content = generateAgentConfig(detector.templateName, {
|
|
338
356
|
workspaceRoot,
|
|
339
357
|
binaryPath,
|
|
@@ -403,6 +421,16 @@ export function configureAgents(workspaceRoot, binaryPath, version, agentFilter,
|
|
|
403
421
|
// Event-driven hint hook: default-on (non-blocking, fail-open — the
|
|
404
422
|
// 2.3 A2 opt-in applies to DENY hooks, this one cannot block).
|
|
405
423
|
const hintResult = installClaudeCodeHintHook(workspaceRoot, binaryPath);
|
|
424
|
+
installClaudeCodeSearchHook(workspaceRoot, binaryPath);
|
|
425
|
+
// Both doors. Bounding either alone measured as no change at all: the
|
|
426
|
+
// work simply moves to the other one.
|
|
427
|
+
if (interventionMode() === "on") {
|
|
428
|
+
installClaudeCodeReadHint(workspaceRoot, binaryPath);
|
|
429
|
+
installClaudeCodeBashCap(workspaceRoot, binaryPath);
|
|
430
|
+
}
|
|
431
|
+
else {
|
|
432
|
+
removeClaudeCodePreToolHooks(workspaceRoot);
|
|
433
|
+
}
|
|
406
434
|
installClaudeCodeStopGate(workspaceRoot, binaryPath);
|
|
407
435
|
installClaudeCodeSessionContext(workspaceRoot, binaryPath);
|
|
408
436
|
if (hintResult) {
|
|
@@ -472,6 +500,7 @@ export function configureAgents(workspaceRoot, binaryPath, version, agentFilter,
|
|
|
472
500
|
if (wrote)
|
|
473
501
|
mcpConfigs.push(wrote);
|
|
474
502
|
installOpencodeHintPlugin(workspaceRoot, binaryPath, ".kilo/plugin");
|
|
503
|
+
installOpencodeCompressPlugin(workspaceRoot, binaryPath, ".kilo/plugin");
|
|
475
504
|
// Kilo v7 vendors opencode, so it takes the same guard plugin — the rules
|
|
476
505
|
// markdown alone was demonstrably not enough (a reported session loaded
|
|
477
506
|
// vexp.md, quoted it back, and still read five files by hand).
|
|
@@ -525,6 +554,18 @@ export function configureAgents(workspaceRoot, binaryPath, version, agentFilter,
|
|
|
525
554
|
action: "removed",
|
|
526
555
|
});
|
|
527
556
|
}
|
|
557
|
+
// v5: the coupling on edit. Default-ON and independent of the guard —
|
|
558
|
+
// it blocks nothing, so there is no reason to make it opt-in.
|
|
559
|
+
const edit = installCursorEditHint(workspaceRoot, binaryPath);
|
|
560
|
+
if (edit) {
|
|
561
|
+
results.push({
|
|
562
|
+
agent: "Cursor Edit Hint",
|
|
563
|
+
configFile: path.join(".cursor", "hooks", "vexp-edit-hint.sh"),
|
|
564
|
+
content: "",
|
|
565
|
+
alreadyExists: edit === "updated",
|
|
566
|
+
action: edit,
|
|
567
|
+
});
|
|
568
|
+
}
|
|
528
569
|
}
|
|
529
570
|
// Cline: rules in .clinerules (generic template, handled by the writer
|
|
530
571
|
// above); MCP registry is machine-global VS Code storage.
|
|
@@ -544,6 +585,7 @@ export function configureAgents(workspaceRoot, binaryPath, version, agentFilter,
|
|
|
544
585
|
if (detector.agent === "Opencode") {
|
|
545
586
|
const wroteMcp = configureOpencodeMcp(workspaceRoot, binaryPath, mcpServerPath);
|
|
546
587
|
installOpencodeHintPlugin(workspaceRoot, binaryPath, ".opencode/plugin");
|
|
588
|
+
installOpencodeCompressPlugin(workspaceRoot, binaryPath, ".opencode/plugin");
|
|
547
589
|
if (wroteMcp)
|
|
548
590
|
mcpConfigs.push(wroteMcp);
|
|
549
591
|
if (guardMode() === "strict") {
|
|
@@ -673,6 +715,11 @@ export function configureSelectedAgents(workspaceRoot, binaryPath, version, sele
|
|
|
673
715
|
fs.mkdirSync(mcpDir, { recursive: true });
|
|
674
716
|
}
|
|
675
717
|
}
|
|
718
|
+
// Same sweep as the other writer: the Windsurf rules moved out of
|
|
719
|
+
// `.windsurf/rules.md`, which Cascade never read.
|
|
720
|
+
if (detector.agent === "Windsurf") {
|
|
721
|
+
removeVexpSection(path.join(workspaceRoot, ".windsurf", "rules.md"));
|
|
722
|
+
}
|
|
676
723
|
const alreadyExists = fs.existsSync(configFilePath);
|
|
677
724
|
const content = generateAgentConfig(detector.templateName, {
|
|
678
725
|
workspaceRoot,
|
|
@@ -720,6 +767,13 @@ export function configureSelectedAgents(workspaceRoot, binaryPath, version, sele
|
|
|
720
767
|
if (wrote)
|
|
721
768
|
mcpConfigs.push("~/.claude.json");
|
|
722
769
|
installClaudeCodeHintHook(workspaceRoot, binaryPath);
|
|
770
|
+
if (interventionMode() === "on") {
|
|
771
|
+
installClaudeCodeReadHint(workspaceRoot, binaryPath);
|
|
772
|
+
installClaudeCodeBashCap(workspaceRoot, binaryPath);
|
|
773
|
+
}
|
|
774
|
+
else {
|
|
775
|
+
removeClaudeCodePreToolHooks(workspaceRoot);
|
|
776
|
+
}
|
|
723
777
|
installClaudeCodeStopGate(workspaceRoot, binaryPath);
|
|
724
778
|
installClaudeCodeSessionContext(workspaceRoot, binaryPath);
|
|
725
779
|
if (guardMode() === "strict")
|
|
@@ -760,12 +814,17 @@ export function configureSelectedAgents(workspaceRoot, binaryPath, version, sele
|
|
|
760
814
|
if (wrote)
|
|
761
815
|
mcpConfigs.push(wrote);
|
|
762
816
|
installOpencodeHintPlugin(workspaceRoot, binaryPath, ".opencode/plugin");
|
|
817
|
+
installOpencodeCompressPlugin(workspaceRoot, binaryPath, ".opencode/plugin");
|
|
763
818
|
if (guardMode() === "strict")
|
|
764
819
|
installOpencodePlugin(workspaceRoot);
|
|
765
820
|
else
|
|
766
821
|
uninstallOpencodePlugin(workspaceRoot);
|
|
767
822
|
}
|
|
768
823
|
if (detector.agent === "Cursor") {
|
|
824
|
+
if (interventionMode() === "on") {
|
|
825
|
+
installCursorReadHint(workspaceRoot, binaryPath);
|
|
826
|
+
installCursorBashCap(workspaceRoot, binaryPath);
|
|
827
|
+
}
|
|
769
828
|
if (guardMode() === "strict")
|
|
770
829
|
installCursorHook(workspaceRoot);
|
|
771
830
|
else
|
|
@@ -780,6 +839,7 @@ export function configureSelectedAgents(workspaceRoot, binaryPath, version, sele
|
|
|
780
839
|
if (wrote)
|
|
781
840
|
mcpConfigs.push(wrote);
|
|
782
841
|
installOpencodeHintPlugin(workspaceRoot, binaryPath, ".kilo/plugin");
|
|
842
|
+
installOpencodeCompressPlugin(workspaceRoot, binaryPath, ".kilo/plugin");
|
|
783
843
|
if (guardMode() === "strict")
|
|
784
844
|
installKiloPlugin(workspaceRoot);
|
|
785
845
|
else
|
|
@@ -1009,13 +1069,68 @@ function warnUnparseable(filePath) {
|
|
|
1009
1069
|
}
|
|
1010
1070
|
process.stderr.write(` [!] ${filePath} could not be parsed - leaving it untouched. Fix the file or add vexp manually, then re-run setup.\n`);
|
|
1011
1071
|
}
|
|
1072
|
+
/**
|
|
1073
|
+
* Remove a vexp section from a file we no longer write to, and delete the file
|
|
1074
|
+
* when nothing of the user's is left in it.
|
|
1075
|
+
*
|
|
1076
|
+
* Needed because vexp wrote its Windsurf rules to `.windsurf/rules.md` for
|
|
1077
|
+
* several releases. Cascade reads `.windsurf/rules/*.md` (a directory),
|
|
1078
|
+
* `.devin/rules/*.md`, or the legacy root `.windsurfrules` — never that path.
|
|
1079
|
+
* Leaving the old file behind would put a stale, unread copy of our
|
|
1080
|
+
* instructions next to the live one.
|
|
1081
|
+
*/
|
|
1082
|
+
export function removeVexpSection(filePath) {
|
|
1083
|
+
if (!fs.existsSync(filePath))
|
|
1084
|
+
return "absent";
|
|
1085
|
+
let existing;
|
|
1086
|
+
try {
|
|
1087
|
+
existing = fs.readFileSync(filePath, "utf-8");
|
|
1088
|
+
}
|
|
1089
|
+
catch {
|
|
1090
|
+
return "absent";
|
|
1091
|
+
}
|
|
1092
|
+
const marker = existing.match(VEXP_MARKER_RE);
|
|
1093
|
+
const endIdx = existing.indexOf(VEXP_MARKER_END);
|
|
1094
|
+
if (!marker || endIdx === -1)
|
|
1095
|
+
return "absent";
|
|
1096
|
+
const markerIdx = existing.indexOf(marker[0]);
|
|
1097
|
+
let startIdx = markerIdx;
|
|
1098
|
+
while (startIdx > 0 && existing[startIdx - 1] !== "\n")
|
|
1099
|
+
startIdx--;
|
|
1100
|
+
const rest = (existing.slice(0, startIdx) + existing.slice(endIdx + VEXP_MARKER_END.length)).trim();
|
|
1101
|
+
if (rest === "") {
|
|
1102
|
+
try {
|
|
1103
|
+
fs.unlinkSync(filePath);
|
|
1104
|
+
return "deleted";
|
|
1105
|
+
}
|
|
1106
|
+
catch {
|
|
1107
|
+
return "absent";
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
try {
|
|
1111
|
+
fs.writeFileSync(filePath, rest + "\n", "utf-8");
|
|
1112
|
+
return "stripped";
|
|
1113
|
+
}
|
|
1114
|
+
catch {
|
|
1115
|
+
return "absent";
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1012
1118
|
function appendOrCreate(filePath, content, version) {
|
|
1013
1119
|
if (!fs.existsSync(filePath)) {
|
|
1014
1120
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
1015
1121
|
fs.writeFileSync(filePath, content, "utf-8");
|
|
1016
1122
|
return "created";
|
|
1017
1123
|
}
|
|
1018
|
-
|
|
1124
|
+
let existing = fs.readFileSync(filePath, "utf-8");
|
|
1125
|
+
// Front matter has to stay the FIRST thing in the file, and the splice below
|
|
1126
|
+
// preserves whatever sits above our marker — so a rewrite would leave the
|
|
1127
|
+
// old block above and insert a second one. One rule file, one front matter.
|
|
1128
|
+
if (content.startsWith("---\n")) {
|
|
1129
|
+
const lead = existing.match(/^---\n[\s\S]*?\n---\n/);
|
|
1130
|
+
if (lead) {
|
|
1131
|
+
existing = existing.slice(lead[0].length).replace(/^\s*\n/, "");
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1019
1134
|
const markerMatch = existing.match(VEXP_MARKER_RE);
|
|
1020
1135
|
if (!markerMatch) {
|
|
1021
1136
|
fs.appendFileSync(filePath, "\n\n" + content);
|
|
@@ -1289,7 +1404,15 @@ function buildCodexSection(opts) {
|
|
|
1289
1404
|
const wsHash = opts.workspaceRoot ? workspaceHash(opts.workspaceRoot) : "";
|
|
1290
1405
|
const urlPath = wsHash ? `/ws/${wsHash}/mcp` : "/mcp";
|
|
1291
1406
|
const desiredUrl = `http://127.0.0.1:${opts.mcpPort}${urlPath}`;
|
|
1292
|
-
return
|
|
1407
|
+
return `
|
|
1408
|
+
[mcp_servers.vexp]
|
|
1409
|
+
${CODEX_MANAGED_MARKER}: http transport (set VEXP_CODEX_TRANSPORT=direct for stdio)
|
|
1410
|
+
url = "${desiredUrl}"
|
|
1411
|
+
tool_timeout_sec = 120
|
|
1412
|
+
|
|
1413
|
+
[mcp_servers.vexp.http_headers]
|
|
1414
|
+
Authorization = "Bearer ${opts.token}"
|
|
1415
|
+
`;
|
|
1293
1416
|
}
|
|
1294
1417
|
/**
|
|
1295
1418
|
* Configure MCP in ~/.codex/config.toml (global).
|
|
@@ -1964,11 +2087,27 @@ export function installClaudeCodeStopGate(workspaceRoot, binaryPath) {
|
|
|
1964
2087
|
const hookPath = path.join(hookDir, "vexp-verify.sh");
|
|
1965
2088
|
const settingsPath = path.join(workspaceRoot, ".claude", "settings.json");
|
|
1966
2089
|
fs.mkdirSync(hookDir, { recursive: true });
|
|
1967
|
-
|
|
2090
|
+
// Opt-in since the measurement: the gate bought 15 to 32 extra turns per 13
|
|
2091
|
+
// bench sessions, and they land at the END of a session where the
|
|
2092
|
+
// transcript is heaviest, so one of them costs more than a session's entire
|
|
2093
|
+
// tool output. See InterventionMode above. When off, a previously installed
|
|
2094
|
+
// gate is removed and de-registered so upgrades converge.
|
|
2095
|
+
const gateOn = interventionMode() === "on";
|
|
1968
2096
|
const existed = fs.existsSync(hookPath);
|
|
1969
|
-
const
|
|
1970
|
-
|
|
1971
|
-
|
|
2097
|
+
const script = vexpStopGateHookScript(binaryPath);
|
|
2098
|
+
const scriptIdentical = gateOn && existed && fs.readFileSync(hookPath, "utf8") === script;
|
|
2099
|
+
if (gateOn) {
|
|
2100
|
+
if (!scriptIdentical) {
|
|
2101
|
+
fs.writeFileSync(hookPath, script, { mode: 0o755 });
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
else if (existed) {
|
|
2105
|
+
try {
|
|
2106
|
+
fs.unlinkSync(hookPath);
|
|
2107
|
+
}
|
|
2108
|
+
catch {
|
|
2109
|
+
/* a hook we cannot remove is still de-registered below */
|
|
2110
|
+
}
|
|
1972
2111
|
}
|
|
1973
2112
|
const read = readJsonConfigSafe(settingsPath);
|
|
1974
2113
|
if (!read.ok) {
|
|
@@ -1980,16 +2119,20 @@ export function installClaudeCodeStopGate(workspaceRoot, binaryPath) {
|
|
|
1980
2119
|
const existing = Array.isArray(hooks.Stop) ? hooks.Stop : [];
|
|
1981
2120
|
const filtered = existing.filter((h) => !isVexpHookEntry(h, "vexp-verify"));
|
|
1982
2121
|
// Cross-OS shape as everywhere: bash + QUOTED path; timeout in SECONDS.
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
2122
|
+
if (gateOn) {
|
|
2123
|
+
filtered.push({
|
|
2124
|
+
hooks: [
|
|
2125
|
+
{
|
|
2126
|
+
type: "command",
|
|
2127
|
+
command: 'bash "$CLAUDE_PROJECT_DIR/.claude/hooks/vexp-verify.sh"',
|
|
2128
|
+
timeout: 15,
|
|
2129
|
+
},
|
|
2130
|
+
],
|
|
2131
|
+
});
|
|
2132
|
+
}
|
|
1992
2133
|
const merged = { ...hooks, Stop: filtered };
|
|
2134
|
+
if (filtered.length === 0)
|
|
2135
|
+
delete merged.Stop;
|
|
1993
2136
|
const settingsIdentical = JSON.stringify(merged) === JSON.stringify(settings.hooks ?? {});
|
|
1994
2137
|
if (scriptIdentical && settingsIdentical)
|
|
1995
2138
|
return null;
|
|
@@ -2001,6 +2144,10 @@ export function installClaudeCodeStopGate(workspaceRoot, binaryPath) {
|
|
|
2001
2144
|
}
|
|
2002
2145
|
return existed ? "updated" : "created";
|
|
2003
2146
|
}
|
|
2147
|
+
function isVexpEditHintHookEntry(entry) {
|
|
2148
|
+
const cmds = JSON.stringify(entry ?? "");
|
|
2149
|
+
return cmds.includes("vexp-edit-hint");
|
|
2150
|
+
}
|
|
2004
2151
|
function isVexpHintHookEntry(h) {
|
|
2005
2152
|
if (!h || typeof h !== "object")
|
|
2006
2153
|
return false;
|
|
@@ -2026,12 +2173,196 @@ function isVexpHintHookEntry(h) {
|
|
|
2026
2173
|
* opt-in applies to DENY hooks, which add a failure mode; this one cannot
|
|
2027
2174
|
* block anything).
|
|
2028
2175
|
*/
|
|
2176
|
+
/** Does this PreToolUse entry belong to us? */
|
|
2177
|
+
function isVexpSearchHookEntry(h) {
|
|
2178
|
+
if (!h || typeof h !== "object")
|
|
2179
|
+
return false;
|
|
2180
|
+
const hks = h.hooks;
|
|
2181
|
+
return Array.isArray(hks) && hks.some((hook) => typeof hook?.command === "string" &&
|
|
2182
|
+
hook.command.includes("vexp-search"));
|
|
2183
|
+
}
|
|
2184
|
+
/**
|
|
2185
|
+
* v4 M1: install the search answer as a PreToolUse hook on Bash.
|
|
2186
|
+
*
|
|
2187
|
+
* Matcher is Bash and nothing else. The hook rewrites only a recursive grep
|
|
2188
|
+
* for names this index defines, keeps the original command after a `||`, and
|
|
2189
|
+
* prints nothing at all in every other case — so a session where vexp has no
|
|
2190
|
+
* opinion is byte-for-byte a session without vexp.
|
|
2191
|
+
*
|
|
2192
|
+
* Why a hook rather than a tool: 9 of 227 measured agent sessions called the
|
|
2193
|
+
* MCP tools. All 227 called Bash.
|
|
2194
|
+
*/
|
|
2195
|
+
/**
|
|
2196
|
+
* Install one of the compression hooks into Claude Code's PreToolUse.
|
|
2197
|
+
*
|
|
2198
|
+
* Shared because the two are one mechanism seen from two sides. Bounding reads
|
|
2199
|
+
* alone did nothing measurable: Read-tool tokens fell 54% and shell output rose
|
|
2200
|
+
* 39%, and the total sat inside the band between two identical stock runs. An
|
|
2201
|
+
* agent takes what it needs through whichever door is open.
|
|
2202
|
+
*/
|
|
2203
|
+
/**
|
|
2204
|
+
* Undo the two compression hooks, so a workspace configured by an older
|
|
2205
|
+
* version converges on the next setup instead of quietly keeping a default
|
|
2206
|
+
* that was measured and withdrawn.
|
|
2207
|
+
*
|
|
2208
|
+
* Same contract as the guard's removal: take the files out AND de-register
|
|
2209
|
+
* them. A hook left in settings.json pointing at a script that no longer
|
|
2210
|
+
* exists is worse than either state on its own — Claude Code runs it, it
|
|
2211
|
+
* fails, and every Read or Bash call carries the failure.
|
|
2212
|
+
*/
|
|
2213
|
+
function removeClaudeCodePreToolHooks(workspaceRoot) {
|
|
2214
|
+
const hookDir = path.join(workspaceRoot, ".claude", "hooks");
|
|
2215
|
+
for (const name of ["read-hint", "bash-cap"]) {
|
|
2216
|
+
const p = path.join(hookDir, `vexp-${name}.sh`);
|
|
2217
|
+
if (fs.existsSync(p)) {
|
|
2218
|
+
try {
|
|
2219
|
+
fs.unlinkSync(p);
|
|
2220
|
+
}
|
|
2221
|
+
catch {
|
|
2222
|
+
/* still de-registered below */
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
const settingsPath = path.join(workspaceRoot, ".claude", "settings.json");
|
|
2227
|
+
const read = readJsonConfigSafe(settingsPath);
|
|
2228
|
+
if (!read.ok || !read.existed)
|
|
2229
|
+
return;
|
|
2230
|
+
const settings = read.data;
|
|
2231
|
+
const hooks = (settings.hooks ?? {});
|
|
2232
|
+
const existing = Array.isArray(hooks.PreToolUse) ? hooks.PreToolUse : [];
|
|
2233
|
+
const filtered = existing.filter((h) => !/vexp-(read-hint|bash-cap)/.test(JSON.stringify(h)));
|
|
2234
|
+
if (filtered.length === existing.length)
|
|
2235
|
+
return;
|
|
2236
|
+
const merged = { ...hooks, PreToolUse: filtered };
|
|
2237
|
+
if (filtered.length === 0)
|
|
2238
|
+
delete merged.PreToolUse;
|
|
2239
|
+
settings.hooks = merged;
|
|
2240
|
+
backupConfig(settingsPath);
|
|
2241
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
2242
|
+
}
|
|
2243
|
+
function installClaudeCodePreToolHook(workspaceRoot, binaryPath, name, matcher, script) {
|
|
2244
|
+
const hookDir = path.join(workspaceRoot, ".claude", "hooks");
|
|
2245
|
+
const hookPath = path.join(hookDir, `vexp-${name}.sh`);
|
|
2246
|
+
const settingsPath = path.join(workspaceRoot, ".claude", "settings.json");
|
|
2247
|
+
fs.mkdirSync(hookDir, { recursive: true });
|
|
2248
|
+
const existed = fs.existsSync(hookPath);
|
|
2249
|
+
const scriptIdentical = existed && fs.readFileSync(hookPath, "utf-8") === script;
|
|
2250
|
+
if (!scriptIdentical)
|
|
2251
|
+
fs.writeFileSync(hookPath, script, { mode: 0o755 });
|
|
2252
|
+
const read = readJsonConfigSafe(settingsPath);
|
|
2253
|
+
if (!read.ok) {
|
|
2254
|
+
warnUnparseable(settingsPath);
|
|
2255
|
+
return scriptIdentical ? null : existed ? "updated" : "created";
|
|
2256
|
+
}
|
|
2257
|
+
const settings = read.data;
|
|
2258
|
+
const hooks = (settings.hooks ?? {});
|
|
2259
|
+
const existing = Array.isArray(hooks.PreToolUse) ? hooks.PreToolUse : [];
|
|
2260
|
+
const filtered = existing.filter((h) => !JSON.stringify(h).includes(`vexp-${name}`));
|
|
2261
|
+
// Shell form with a quoted path: exec form cannot run a .sh on Windows, and
|
|
2262
|
+
// an unquoted path word-splits on "C:\\Program Files" (c4a0b9e).
|
|
2263
|
+
filtered.push({
|
|
2264
|
+
matcher,
|
|
2265
|
+
hooks: [
|
|
2266
|
+
{
|
|
2267
|
+
type: "command",
|
|
2268
|
+
command: `bash "$CLAUDE_PROJECT_DIR/.claude/hooks/vexp-${name}.sh"`,
|
|
2269
|
+
timeout: 10,
|
|
2270
|
+
},
|
|
2271
|
+
],
|
|
2272
|
+
});
|
|
2273
|
+
const merged = { ...hooks, PreToolUse: filtered };
|
|
2274
|
+
const settingsIdentical = JSON.stringify(merged) === JSON.stringify(settings.hooks ?? {});
|
|
2275
|
+
if (scriptIdentical && settingsIdentical)
|
|
2276
|
+
return null;
|
|
2277
|
+
settings.hooks = merged;
|
|
2278
|
+
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
|
2279
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
2280
|
+
return existed ? "updated" : "created";
|
|
2281
|
+
}
|
|
2282
|
+
export function installClaudeCodeReadHint(workspaceRoot, binaryPath) {
|
|
2283
|
+
return installClaudeCodePreToolHook(workspaceRoot, binaryPath, "read-hint", "Read", bakeReadHintHook(binaryPath));
|
|
2284
|
+
}
|
|
2285
|
+
export function installClaudeCodeBashCap(workspaceRoot, binaryPath) {
|
|
2286
|
+
return installClaudeCodePreToolHook(workspaceRoot, binaryPath, "bash-cap", "Bash", bakeBashCapHook(binaryPath));
|
|
2287
|
+
}
|
|
2288
|
+
export function installClaudeCodeSearchHook(workspaceRoot, binaryPath) {
|
|
2289
|
+
const hookDir = path.join(workspaceRoot, ".claude", "hooks");
|
|
2290
|
+
const hookPath = path.join(hookDir, "vexp-search.sh");
|
|
2291
|
+
const settingsPath = path.join(workspaceRoot, ".claude", "settings.json");
|
|
2292
|
+
const script = vexpSearchHookScript(binaryPath);
|
|
2293
|
+
fs.mkdirSync(hookDir, { recursive: true });
|
|
2294
|
+
const existed = fs.existsSync(hookPath);
|
|
2295
|
+
const scriptIdentical = existed && fs.readFileSync(hookPath, "utf-8") === script;
|
|
2296
|
+
if (!scriptIdentical)
|
|
2297
|
+
fs.writeFileSync(hookPath, script, { mode: 0o755 });
|
|
2298
|
+
const read = readJsonConfigSafe(settingsPath);
|
|
2299
|
+
if (!read.ok) {
|
|
2300
|
+
warnUnparseable(settingsPath);
|
|
2301
|
+
return scriptIdentical ? null : existed ? "updated" : "created";
|
|
2302
|
+
}
|
|
2303
|
+
const settings = read.data;
|
|
2304
|
+
const hooks = (settings.hooks ?? {});
|
|
2305
|
+
const existing = Array.isArray(hooks.PreToolUse) ? hooks.PreToolUse : [];
|
|
2306
|
+
const filtered = existing.filter((h) => !isVexpSearchHookEntry(h));
|
|
2307
|
+
// Same shell-form-with-quoted-path shape as every other hook we write: exec
|
|
2308
|
+
// form cannot run a .sh on Windows, and an unquoted path word-splits on
|
|
2309
|
+
// "C:\Program Files" (both learned the hard way, c4a0b9e).
|
|
2310
|
+
filtered.push({
|
|
2311
|
+
matcher: "Bash",
|
|
2312
|
+
hooks: [
|
|
2313
|
+
{
|
|
2314
|
+
type: "command",
|
|
2315
|
+
command: 'bash "$CLAUDE_PROJECT_DIR/.claude/hooks/vexp-search.sh"',
|
|
2316
|
+
timeout: 10,
|
|
2317
|
+
},
|
|
2318
|
+
],
|
|
2319
|
+
});
|
|
2320
|
+
const merged = { ...hooks, PreToolUse: filtered };
|
|
2321
|
+
const settingsIdentical = JSON.stringify(merged) === JSON.stringify(settings.hooks ?? {});
|
|
2322
|
+
if (scriptIdentical && settingsIdentical)
|
|
2323
|
+
return null;
|
|
2324
|
+
settings.hooks = merged;
|
|
2325
|
+
if (!settingsIdentical) {
|
|
2326
|
+
if (read.existed)
|
|
2327
|
+
backupConfig(settingsPath);
|
|
2328
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
2329
|
+
}
|
|
2330
|
+
return existed ? "updated" : "created";
|
|
2331
|
+
}
|
|
2029
2332
|
export function installClaudeCodeHintHook(workspaceRoot, binaryPath) {
|
|
2030
2333
|
const hookDir = path.join(workspaceRoot, ".claude", "hooks");
|
|
2031
2334
|
const hookPath = path.join(hookDir, "vexp-hint.sh");
|
|
2032
2335
|
const settingsPath = path.join(workspaceRoot, ".claude", "settings.json");
|
|
2033
2336
|
const script = vexpHintHookScript(binaryPath);
|
|
2034
2337
|
fs.mkdirSync(hookDir, { recursive: true });
|
|
2338
|
+
// v5: the edit-time coupling hook, written beside the prompt-time one -
|
|
2339
|
+
// but only when interventions are on. It is one of the two mechanisms that
|
|
2340
|
+
// ask the agent to go do something, and a turn costs 65,521 tokens. See
|
|
2341
|
+
// InterventionMode above for the measurement.
|
|
2342
|
+
const editHookPath = path.join(hookDir, "vexp-edit-hint.sh");
|
|
2343
|
+
const couplingOn = interventionMode() === "on";
|
|
2344
|
+
if (couplingOn) {
|
|
2345
|
+
const editScript = bakeEditHintHook(binaryPath);
|
|
2346
|
+
const editExisted = fs.existsSync(editHookPath);
|
|
2347
|
+
if (!editExisted || fs.readFileSync(editHookPath, "utf-8") !== editScript) {
|
|
2348
|
+
fs.writeFileSync(editHookPath, editScript, { mode: 0o755 });
|
|
2349
|
+
}
|
|
2350
|
+
try {
|
|
2351
|
+
fs.chmodSync(editHookPath, 0o755);
|
|
2352
|
+
}
|
|
2353
|
+
catch {
|
|
2354
|
+
/* Windows has no exec bit; Git Bash runs it anyway. */
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
else if (fs.existsSync(editHookPath)) {
|
|
2358
|
+
// Converge an upgraded workspace without asking anyone to clean up.
|
|
2359
|
+
try {
|
|
2360
|
+
fs.unlinkSync(editHookPath);
|
|
2361
|
+
}
|
|
2362
|
+
catch {
|
|
2363
|
+
/* a hook we cannot remove is still de-registered below */
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2035
2366
|
const existed = fs.existsSync(hookPath);
|
|
2036
2367
|
const scriptIdentical = existed && fs.readFileSync(hookPath, "utf-8") === script;
|
|
2037
2368
|
if (!scriptIdentical) {
|
|
@@ -2059,7 +2390,35 @@ export function installClaudeCodeHintHook(workspaceRoot, binaryPath) {
|
|
|
2059
2390
|
},
|
|
2060
2391
|
],
|
|
2061
2392
|
});
|
|
2062
|
-
|
|
2393
|
+
// v5: the coupling on the edit that needs it. PostToolUse rather than
|
|
2394
|
+
// PreToolUse because a deny on an edit stops real work, and this must never
|
|
2395
|
+
// block — it adds no turn and no permission decision, only a line the model
|
|
2396
|
+
// reads as a system-reminder.
|
|
2397
|
+
const existingPost = Array.isArray(hooks.PostToolUse)
|
|
2398
|
+
? hooks.PostToolUse
|
|
2399
|
+
: [];
|
|
2400
|
+
const filteredPost = existingPost.filter((h) => !isVexpEditHintHookEntry(h));
|
|
2401
|
+
if (couplingOn) {
|
|
2402
|
+
filteredPost.push({
|
|
2403
|
+
matcher: "Edit|Write|NotebookEdit",
|
|
2404
|
+
hooks: [
|
|
2405
|
+
{
|
|
2406
|
+
type: "command",
|
|
2407
|
+
command: 'bash "$CLAUDE_PROJECT_DIR/.claude/hooks/vexp-edit-hint.sh"',
|
|
2408
|
+
timeout: 5,
|
|
2409
|
+
},
|
|
2410
|
+
],
|
|
2411
|
+
});
|
|
2412
|
+
}
|
|
2413
|
+
const merged = {
|
|
2414
|
+
...hooks,
|
|
2415
|
+
UserPromptSubmit: filtered,
|
|
2416
|
+
PostToolUse: filteredPost,
|
|
2417
|
+
};
|
|
2418
|
+
// An empty array is not "no hook": it leaves a key behind that reads like a
|
|
2419
|
+
// configured-but-broken event. Drop it, as the extension copy does.
|
|
2420
|
+
if (filteredPost.length === 0)
|
|
2421
|
+
delete merged.PostToolUse;
|
|
2063
2422
|
const settingsIdentical = JSON.stringify(merged) === JSON.stringify(settings.hooks ?? {});
|
|
2064
2423
|
if (scriptIdentical && settingsIdentical)
|
|
2065
2424
|
return null;
|
|
@@ -2076,6 +2435,21 @@ export function installClaudeCodeHintHook(workspaceRoot, binaryPath) {
|
|
|
2076
2435
|
* unlike the deny guard (strict-only), this cannot break a session; it only
|
|
2077
2436
|
* ever appends an orientation line the daemon judged useful.
|
|
2078
2437
|
*/
|
|
2438
|
+
/**
|
|
2439
|
+
* opencode / Kilo compression plugin: both doors, mutating rather than
|
|
2440
|
+
* blocking. Their `tool.execute.before` can rewrite `output.args`, which is
|
|
2441
|
+
* the whole mechanism.
|
|
2442
|
+
*/
|
|
2443
|
+
export function installOpencodeCompressPlugin(workspaceRoot, binaryPath, pluginDir) {
|
|
2444
|
+
const pluginPath = path.join(workspaceRoot, pluginDir, "vexp-compress.js");
|
|
2445
|
+
const content = vexpOpencodeCompressPlugin(binaryPath);
|
|
2446
|
+
fs.mkdirSync(path.dirname(pluginPath), { recursive: true });
|
|
2447
|
+
const existed = fs.existsSync(pluginPath);
|
|
2448
|
+
if (existed && fs.readFileSync(pluginPath, "utf-8") === content)
|
|
2449
|
+
return null;
|
|
2450
|
+
fs.writeFileSync(pluginPath, content);
|
|
2451
|
+
return existed ? "updated" : "created";
|
|
2452
|
+
}
|
|
2079
2453
|
export function installOpencodeHintPlugin(workspaceRoot, binaryPath, pluginDir) {
|
|
2080
2454
|
const pluginPath = path.join(workspaceRoot, pluginDir, "vexp-hint.js");
|
|
2081
2455
|
const content = vexpOpencodeHintPlugin(binaryPath);
|
|
@@ -2100,10 +2474,33 @@ export function installCodexHintHook(workspaceRoot, binaryPath) {
|
|
|
2100
2474
|
const script = vexpHintHookScript(binaryPath);
|
|
2101
2475
|
fs.mkdirSync(dir, { recursive: true });
|
|
2102
2476
|
const existed = fs.existsSync(hookPath);
|
|
2103
|
-
|
|
2477
|
+
let scriptIdentical = existed && fs.readFileSync(hookPath, "utf-8") === script;
|
|
2104
2478
|
if (!scriptIdentical) {
|
|
2105
2479
|
fs.writeFileSync(hookPath, script, { mode: 0o755 });
|
|
2106
2480
|
}
|
|
2481
|
+
// Windows gets a batch twin. Codex runs a hook command through
|
|
2482
|
+
// `cmd.exe /C` (COMSPEC, codex-rs/hooks command_runner), so `bash "..."`
|
|
2483
|
+
// reaches a shell that has never heard of bash: a user with neither Git
|
|
2484
|
+
// Bash nor WSL got "bash not found" on every prompt and orientation was
|
|
2485
|
+
// inert. Codex's own answer is `commandWindows`, an optional per-OS
|
|
2486
|
+
// override on the command handler (openai/codex#22159, ~0.131, months
|
|
2487
|
+
// before the version that requires the nested `hooks` shape we already
|
|
2488
|
+
// write). Older builds ignore the field rather than reject the file —
|
|
2489
|
+
// the handler is not deny_unknown_fields, unlike the top level.
|
|
2490
|
+
const cmdPath = path.join(dir, "vexp-hint.cmd");
|
|
2491
|
+
let cmdCommand;
|
|
2492
|
+
if (process.platform === "win32") {
|
|
2493
|
+
const cmdScript = vexpHintHookCmdScript(binaryPath);
|
|
2494
|
+
const cmdExisted = fs.existsSync(cmdPath);
|
|
2495
|
+
if (!cmdExisted || fs.readFileSync(cmdPath, "utf-8") !== cmdScript) {
|
|
2496
|
+
fs.writeFileSync(cmdPath, cmdScript);
|
|
2497
|
+
scriptIdentical = false;
|
|
2498
|
+
}
|
|
2499
|
+
// Quoted: Codex hands `cmd /C` the whole line wrapped in one more pair
|
|
2500
|
+
// of quotes, and cmd then strips the outermost pair — which leaves this
|
|
2501
|
+
// path quoted and therefore safe to contain spaces.
|
|
2502
|
+
cmdCommand = `"${cmdPath}"`;
|
|
2503
|
+
}
|
|
2107
2504
|
let root = {};
|
|
2108
2505
|
if (fs.existsSync(hooksJsonPath)) {
|
|
2109
2506
|
try {
|
|
@@ -2141,6 +2538,7 @@ export function installCodexHintHook(workspaceRoot, binaryPath) {
|
|
|
2141
2538
|
{
|
|
2142
2539
|
type: "command",
|
|
2143
2540
|
command: `bash "${hookPath.replace(/\\/g, "/")}"`,
|
|
2541
|
+
...(cmdCommand ? { commandWindows: cmdCommand } : {}),
|
|
2144
2542
|
timeout: 5,
|
|
2145
2543
|
},
|
|
2146
2544
|
],
|
|
@@ -2229,6 +2627,58 @@ export function uninstallOpencodePlugin(workspaceRoot) {
|
|
|
2229
2627
|
* `failClosed` is left at its default (false): a crashing guard must let the
|
|
2230
2628
|
* agent work, not lock it out.
|
|
2231
2629
|
*/
|
|
2630
|
+
/**
|
|
2631
|
+
* Register a compression hook with Cursor.
|
|
2632
|
+
*
|
|
2633
|
+
* Cursor's `preToolUse` takes the SAME input as Claude Code's — `tool_name`,
|
|
2634
|
+
* `tool_input` — and answers in a different shape: `permission` plus a
|
|
2635
|
+
* snake_case `updated_input`. The binary emits either on request, so nothing
|
|
2636
|
+
* here has to know the difference beyond passing `--protocol cursor`.
|
|
2637
|
+
*
|
|
2638
|
+
* The binary is registered directly rather than through a shell script: there
|
|
2639
|
+
* is no `$CLAUDE_PROJECT_DIR` to expand here and no bash to assume on Windows.
|
|
2640
|
+
*/
|
|
2641
|
+
function installCursorCompressionHook(workspaceRoot, binaryPath, sub, matcher) {
|
|
2642
|
+
const cfgPath = path.join(workspaceRoot, ".cursor", "hooks.json");
|
|
2643
|
+
const command = `${binaryPath} ${sub} --protocol cursor`;
|
|
2644
|
+
const read = readJsonConfigSafe(cfgPath);
|
|
2645
|
+
if (!read.ok) {
|
|
2646
|
+
warnUnparseable(cfgPath);
|
|
2647
|
+
return false;
|
|
2648
|
+
}
|
|
2649
|
+
const cfg = read.data;
|
|
2650
|
+
const hooks = cfg.hooks ?? {};
|
|
2651
|
+
const preToolUse = Array.isArray(hooks.preToolUse) ? [...hooks.preToolUse] : [];
|
|
2652
|
+
// Idempotent by SUBCOMMAND, not by exact command: the binary path changes
|
|
2653
|
+
// between installs and must replace its own entry rather than add one.
|
|
2654
|
+
const mine = (e) => !!e &&
|
|
2655
|
+
typeof e === "object" &&
|
|
2656
|
+
typeof e.command === "string" &&
|
|
2657
|
+
(e.command).includes(` ${sub} --protocol cursor`);
|
|
2658
|
+
const entry = { command, matcher };
|
|
2659
|
+
const at = preToolUse.findIndex(mine);
|
|
2660
|
+
if (at >= 0 && JSON.stringify(preToolUse[at]) === JSON.stringify(entry))
|
|
2661
|
+
return false;
|
|
2662
|
+
if (at >= 0)
|
|
2663
|
+
preToolUse[at] = entry;
|
|
2664
|
+
else
|
|
2665
|
+
preToolUse.push(entry);
|
|
2666
|
+
hooks.preToolUse = preToolUse;
|
|
2667
|
+
cfg.hooks = hooks;
|
|
2668
|
+
if (cfg.version === undefined)
|
|
2669
|
+
cfg.version = 1;
|
|
2670
|
+
fs.mkdirSync(path.dirname(cfgPath), { recursive: true });
|
|
2671
|
+
if (read.existed)
|
|
2672
|
+
backupConfig(cfgPath);
|
|
2673
|
+
fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2), "utf-8");
|
|
2674
|
+
return true;
|
|
2675
|
+
}
|
|
2676
|
+
export function installCursorReadHint(workspaceRoot, binaryPath) {
|
|
2677
|
+
return installCursorCompressionHook(workspaceRoot, binaryPath, "read-hint", "Read");
|
|
2678
|
+
}
|
|
2679
|
+
export function installCursorBashCap(workspaceRoot, binaryPath) {
|
|
2680
|
+
return installCursorCompressionHook(workspaceRoot, binaryPath, "bash-cap", "Bash");
|
|
2681
|
+
}
|
|
2232
2682
|
export function installCursorHook(workspaceRoot) {
|
|
2233
2683
|
const rel = path.join(".cursor", "hooks", "vexp-guard.js");
|
|
2234
2684
|
const hookPath = path.join(workspaceRoot, rel);
|
|
@@ -2336,6 +2786,58 @@ export function uninstallKiloPlugin(workspaceRoot) {
|
|
|
2336
2786
|
}
|
|
2337
2787
|
return removed;
|
|
2338
2788
|
}
|
|
2789
|
+
/**
|
|
2790
|
+
* v5: the edit-time coupling for Cursor.
|
|
2791
|
+
*
|
|
2792
|
+
* Cursor sees a prompt but cannot inject at prompt time — `beforeSubmitPrompt`
|
|
2793
|
+
* is read-only. Its injectable channel is `postToolUse`, which is exactly
|
|
2794
|
+
* where this signal belongs anyway: the file has just been edited and the
|
|
2795
|
+
* coupling is what the plan is about to miss.
|
|
2796
|
+
*
|
|
2797
|
+
* Same binary, same daemon op, same fail-open contract as the Claude Code
|
|
2798
|
+
* hook. The point of doing both is that a signal only one agent receives is a
|
|
2799
|
+
* feature of that agent, not of the product.
|
|
2800
|
+
*/
|
|
2801
|
+
export function installCursorEditHint(workspaceRoot, binaryPath) {
|
|
2802
|
+
const dir = path.join(workspaceRoot, ".cursor", "hooks");
|
|
2803
|
+
const scriptPath = path.join(dir, "vexp-edit-hint.sh");
|
|
2804
|
+
const script = bakeEditHintHook(binaryPath);
|
|
2805
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
2806
|
+
const existed = fs.existsSync(scriptPath);
|
|
2807
|
+
if (!existed || fs.readFileSync(scriptPath, "utf-8") !== script) {
|
|
2808
|
+
fs.writeFileSync(scriptPath, script, { mode: 0o755 });
|
|
2809
|
+
}
|
|
2810
|
+
try {
|
|
2811
|
+
fs.chmodSync(scriptPath, 0o755);
|
|
2812
|
+
}
|
|
2813
|
+
catch {
|
|
2814
|
+
/* no exec bit on Windows */
|
|
2815
|
+
}
|
|
2816
|
+
const cfgPath = path.join(workspaceRoot, ".cursor", "hooks.json");
|
|
2817
|
+
const read = readJsonConfigSafe(cfgPath);
|
|
2818
|
+
if (!read.ok) {
|
|
2819
|
+
warnUnparseable(cfgPath);
|
|
2820
|
+
return null;
|
|
2821
|
+
}
|
|
2822
|
+
const cfg = read.data;
|
|
2823
|
+
const hooks = cfg.hooks ?? {};
|
|
2824
|
+
const post = Array.isArray(hooks.postToolUse) ? hooks.postToolUse : [];
|
|
2825
|
+
const filtered = post.filter((e) => {
|
|
2826
|
+
if (!e || typeof e !== "object")
|
|
2827
|
+
return true;
|
|
2828
|
+
const cmd = e.command;
|
|
2829
|
+
return typeof cmd !== "string" || !cmd.includes("vexp-edit-hint");
|
|
2830
|
+
});
|
|
2831
|
+
filtered.push({ command: `bash "${scriptPath}"` });
|
|
2832
|
+
hooks.postToolUse = filtered;
|
|
2833
|
+
const merged = { ...cfg, hooks };
|
|
2834
|
+
if (JSON.stringify(merged) === JSON.stringify(cfg) && existed)
|
|
2835
|
+
return null;
|
|
2836
|
+
if (read.existed)
|
|
2837
|
+
backupConfig(cfgPath);
|
|
2838
|
+
fs.writeFileSync(cfgPath, JSON.stringify(merged, null, 2), "utf-8");
|
|
2839
|
+
return read.existed ? "updated" : "created";
|
|
2840
|
+
}
|
|
2339
2841
|
/** Remove the Cursor guard (2.3 A2 default): delete the hook script and drop
|
|
2340
2842
|
* our preToolUse entry from .cursor/hooks.json. */
|
|
2341
2843
|
export function uninstallCursorHook(workspaceRoot) {
|
|
@@ -2384,12 +2886,47 @@ export function uninstallCursorHook(workspaceRoot) {
|
|
|
2384
2886
|
* Shared by every prose template so the wording cannot drift again. ASCII only
|
|
2385
2887
|
* (these land in files that get read on Windows).
|
|
2386
2888
|
*/
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2889
|
+
/**
|
|
2890
|
+
* The standing mandate, and it is deliberately short.
|
|
2891
|
+
*
|
|
2892
|
+
* Whatever this file says is resident in the agent's context on EVERY turn,
|
|
2893
|
+
* while a tool call happens at most once. Measured over 13 full bench
|
|
2894
|
+
* sessions: 37.1 API turns against a transcript averaging 65,521 tokens, so
|
|
2895
|
+
* a line here is billed 37 times and a call is billed once. The templates
|
|
2896
|
+
* this replaces ran 282 tokens (Claude Code) to 959 (generic) - up to 35,000
|
|
2897
|
+
* replayed tokens per session - and most of what they carried was a second
|
|
2898
|
+
* copy of the tool descriptions the MCP schemas already send.
|
|
2899
|
+
*
|
|
2900
|
+
* So this keeps only the three things a schema cannot carry:
|
|
2901
|
+
*
|
|
2902
|
+
* 1. WHEN to call, and when not to - the schema says what a tool returns,
|
|
2903
|
+
* not whether this task wants it.
|
|
2904
|
+
* 2. The two guardrails that stop wasted calls: text sweeps belong to
|
|
2905
|
+
* native search, and the index covers repo source only (logs, dist/,
|
|
2906
|
+
* node_modules are not in it - read those directly).
|
|
2907
|
+
* 3. The privacy line, because users ask and compliance reviews ask.
|
|
2908
|
+
*
|
|
2909
|
+
* Everything else was deleted, not shortened. See the catalog diet in
|
|
2910
|
+
* mcp_stdio.rs for the same argument applied one layer down.
|
|
2911
|
+
*/
|
|
2912
|
+
const MANDATE_CORE = `### Context strategy: call run_pipeline ONCE at task start
|
|
2913
|
+
If the task already names the files/symbols to touch, SKIP vexp. Otherwise one
|
|
2914
|
+
\`run_pipeline({ "task": "..." })\` returns ranked pivot files with line ranges and
|
|
2915
|
+
blast radius. Do NOT open files one by one to find your way around - every extra
|
|
2916
|
+
tool call costs a turn. Call it again ONLY when the task moves to a new area.
|
|
2917
|
+
\`get_skeleton\` for files to understand, not edit. \`verify_done\` before calling a
|
|
2918
|
+
multi-file task complete, then RUN the tests it names.
|
|
2919
|
+
|
|
2920
|
+
### Query shape (do this)
|
|
2921
|
+
Anchor the task on real identifiers (ClassName, functionName) or file paths:
|
|
2922
|
+
\`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
|
|
2923
|
+
|
|
2924
|
+
vexp runs entirely on this machine, index in \`.vexp/\`;
|
|
2925
|
+
\`run_pipeline\` transmits nothing to any external service.
|
|
2926
|
+
On \`status: "degraded"\` or 0 pivots the index is still building - use your own tools.
|
|
2927
|
+
For literal string sweeps use your native search - do NOT route text sweeps through vexp.
|
|
2928
|
+
Repo SOURCE only: logs, dist/, node_modules/ and files outside the repo are NOT indexed.`;
|
|
2929
|
+
export function generateAgentConfig(template, vars) {
|
|
2393
2930
|
switch (template) {
|
|
2394
2931
|
case "claude-code":
|
|
2395
2932
|
return claudeCodeTemplate(vars);
|
|
@@ -2418,469 +2955,72 @@ function generateAgentConfig(template, vars) {
|
|
|
2418
2955
|
// ---------------------------------------------------------------------------
|
|
2419
2956
|
function claudeCodeTemplate(vars) {
|
|
2420
2957
|
return `## vexp - Context-Aware AI Coding <!-- vexp v${vars.version} -->
|
|
2421
|
-
vexp runs entirely on this machine: local daemon, index in \`.vexp/\`.
|
|
2422
|
-
\`run_pipeline\` transmits nothing to any external service.
|
|
2423
|
-
- \`run_pipeline({ "task": "..." })\` - orientation in one call (ranked pivot
|
|
2424
|
-
files with line ranges + blast radius + session notes) when a task does NOT
|
|
2425
|
-
name the files/symbols to touch. If it does, SKIP vexp - use your normal tools.
|
|
2426
|
-
- \`get_skeleton\` - file structure at 70-90% token savings for files you only
|
|
2427
|
-
need to understand, not edit.
|
|
2428
|
-
- \`verify_done\` - call once BEFORE declaring a multi-file task complete:
|
|
2429
|
-
returns mechanically broken references (imports of removed names, parse
|
|
2430
|
-
errors), untouched dependents of the files you changed (file:line), and
|
|
2431
|
-
the impacted tests - RUN those tests before declaring done.
|
|
2432
|
-
- vexp may append a one-line hint to a prompt when orientation would help;
|
|
2433
|
-
otherwise it stays silent.
|
|
2434
2958
|
|
|
2435
|
-
|
|
2436
|
-
Anchor the task on real identifiers (ClassName, functionName) or file paths:
|
|
2437
|
-
\`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
|
|
2959
|
+
${MANDATE_CORE}
|
|
2438
2960
|
<!-- /vexp -->`;
|
|
2439
2961
|
}
|
|
2440
2962
|
function cursorTemplate(vars) {
|
|
2441
2963
|
return `## vexp rules for Cursor <!-- vexp v${vars.version} -->
|
|
2442
2964
|
|
|
2443
|
-
|
|
2444
|
-
If the task already names the files/symbols to touch, skip vexp and work
|
|
2445
|
-
with your normal tools - a skipped call costs nothing.
|
|
2446
|
-
vexp returns graph-ranked context (relevant files, blast radius, memories) in one call.
|
|
2447
|
-
On non-trivial tasks pass \`eager: true\`: the single response then carries full file
|
|
2448
|
-
bodies, covering tests and impact - work directly from it instead of re-reading sources.
|
|
2449
|
-
For literal string sweeps (constants, log messages, config keys) use native search
|
|
2450
|
-
directly - do NOT route text sweeps through vexp.
|
|
2451
|
-
vexp runs entirely on this machine: local daemon, local index stored inside the
|
|
2452
|
-
workspace (\`.vexp/\`). \`run_pipeline\` transmits nothing to any external service -
|
|
2453
|
-
treat it like a local build tool; no data-sharing consent is needed to call it.
|
|
2454
|
-
|
|
2455
|
-
### Workflow
|
|
2456
|
-
1. \`run_pipeline\` with your task description - ONCE at task start
|
|
2457
|
-
2. Literal text sweeps with native search; Read the files you will edit
|
|
2458
|
-
3. Make targeted changes based on the context returned
|
|
2459
|
-
4. \`run_pipeline\` again ONLY when the task moves to a new area - not per turn
|
|
2460
|
-
|
|
2461
|
-
### Available MCP tools
|
|
2462
|
-
- \`run_pipeline\` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
|
|
2463
|
-
Auto-detects intent. Includes file content. Example: \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
|
|
2464
|
-
- \`get_skeleton\` - compact file structure
|
|
2465
|
-
- \`verify_done\` - call once BEFORE declaring a multi-file task complete:
|
|
2466
|
-
mechanically broken references, untouched dependents, and impacted tests
|
|
2467
|
-
to RUN before declaring done, with file:line.
|
|
2468
|
-
- \`index_status\` - indexing status
|
|
2469
|
-
- \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
|
|
2470
|
-
|
|
2471
|
-
(Recommended core set, not the full schema: paid plans advertise 14 MCP tools -
|
|
2472
|
-
capsule, impact graph, logic flow, memory and more. \`run_pipeline\` already runs
|
|
2473
|
-
those server-side, so the four above cover the normal workflow.)
|
|
2474
|
-
|
|
2475
|
-
${QUERY_SHAPE}
|
|
2476
|
-
|
|
2477
|
-
### Agentic search
|
|
2478
|
-
- Ask vexp first for architecture/impact questions; native search remains the right
|
|
2479
|
-
tool for literal text sweeps
|
|
2480
|
-
- If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
|
|
2481
|
-
so they do not re-explore from scratch
|
|
2482
|
-
|
|
2483
|
-
### Tips
|
|
2484
|
-
- Add \`include_tests: true\` when debugging
|
|
2485
|
-
- Use \`preset: "refactor"\` for deep impact analysis
|
|
2486
|
-
|
|
2487
|
-
### Fallback
|
|
2488
|
-
If \`run_pipeline\` returns 0 pivots with an INDEX EMPTY warning, the index is rebuilding.
|
|
2489
|
-
Use file search and read tools directly until the index is ready.
|
|
2490
|
-
|
|
2491
|
-
### Multi-Repo
|
|
2492
|
-
\`run_pipeline\` auto-queries all indexed repos. Use \`repos: ["alias"]\` to scope. Run \`index_status\` to see aliases.
|
|
2493
|
-
|
|
2494
|
-
> **Tip:** To avoid confirmation prompts on every tool call, enable Yolo mode:
|
|
2495
|
-
> Cursor Settings -> Features -> Agent -> Enable Yolo Mode.
|
|
2965
|
+
${MANDATE_CORE}
|
|
2496
2966
|
<!-- /vexp -->`;
|
|
2497
2967
|
}
|
|
2498
2968
|
function windsurfTemplate(vars) {
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
### Workflow
|
|
2514
|
-
1. \`run_pipeline\` with your task description - ONCE at task start
|
|
2515
|
-
2. Literal text sweeps with native search; Read the files you will edit
|
|
2516
|
-
3. Make targeted changes based on the context returned
|
|
2517
|
-
4. \`run_pipeline\` again ONLY when the task moves to a new area - not per turn
|
|
2518
|
-
|
|
2519
|
-
### Available MCP tools
|
|
2520
|
-
- \`run_pipeline\` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
|
|
2521
|
-
Auto-detects intent. Includes file content. Example: \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
|
|
2522
|
-
- \`get_skeleton\` - compact file structure
|
|
2523
|
-
- \`verify_done\` - call once BEFORE declaring a multi-file task complete:
|
|
2524
|
-
mechanically broken references, untouched dependents, and impacted tests
|
|
2525
|
-
to RUN before declaring done, with file:line.
|
|
2526
|
-
- \`index_status\` - indexing status
|
|
2527
|
-
- \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
|
|
2528
|
-
|
|
2529
|
-
(Recommended core set, not the full schema: paid plans advertise 14 MCP tools -
|
|
2530
|
-
capsule, impact graph, logic flow, memory and more. \`run_pipeline\` already runs
|
|
2531
|
-
those server-side, so the four above cover the normal workflow.)
|
|
2532
|
-
|
|
2533
|
-
${QUERY_SHAPE}
|
|
2534
|
-
|
|
2535
|
-
### Agentic search
|
|
2536
|
-
- Ask vexp first for architecture/impact questions; native search remains the right
|
|
2537
|
-
tool for literal text sweeps
|
|
2538
|
-
- If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
|
|
2539
|
-
so they do not re-explore from scratch
|
|
2540
|
-
|
|
2541
|
-
### Smart Features
|
|
2542
|
-
Intent auto-detection, hybrid ranking, session memory, auto-expanding budget.
|
|
2543
|
-
|
|
2544
|
-
### Multi-Repo
|
|
2545
|
-
\`run_pipeline\` auto-queries all indexed repos. Use \`repos: ["alias"]\` to scope. Run \`index_status\` to see aliases.
|
|
2969
|
+
// Front matter, because a rule without one is not always on. Cascade puts
|
|
2970
|
+
// the FULL text of an \`always_on\` rule in the system prompt on every
|
|
2971
|
+
// message; the other modes make the agent choose to look, which is the
|
|
2972
|
+
// complaint every Windsurf user brings ("it ignores the index unless I
|
|
2973
|
+
// remind it"). This is the one lever Windsurf gives us and we were not
|
|
2974
|
+
// pulling it.
|
|
2975
|
+
return `---
|
|
2976
|
+
trigger: always_on
|
|
2977
|
+
description: vexp code-graph orientation for this workspace
|
|
2978
|
+
---
|
|
2979
|
+
|
|
2980
|
+
## vexp for Windsurf <!-- vexp v${vars.version} -->
|
|
2981
|
+
|
|
2982
|
+
${MANDATE_CORE}
|
|
2546
2983
|
<!-- /vexp -->`;
|
|
2547
2984
|
}
|
|
2548
2985
|
function continueTemplate(vars) {
|
|
2549
|
-
return
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
description: "Run vexp pipeline for current task",
|
|
2554
|
-
prompt: "{{{ input }}}\n\nFirst, call run_pipeline with the above task description.",
|
|
2555
|
-
},
|
|
2556
|
-
],
|
|
2557
|
-
mcpServers: [
|
|
2558
|
-
{
|
|
2559
|
-
name: "vexp",
|
|
2560
|
-
command: vars.binaryPath,
|
|
2561
|
-
args: ["mcp"],
|
|
2562
|
-
type: "stdio",
|
|
2563
|
-
},
|
|
2564
|
-
],
|
|
2565
|
-
}, null, 2);
|
|
2986
|
+
return `## vexp <!-- vexp v${vars.version} -->
|
|
2987
|
+
|
|
2988
|
+
${MANDATE_CORE}
|
|
2989
|
+
<!-- /vexp -->`;
|
|
2566
2990
|
}
|
|
2567
2991
|
function augmentTemplate(vars) {
|
|
2568
|
-
return `## vexp
|
|
2569
|
-
|
|
2570
|
-
**Call \`run_pipeline\` ONCE at task start for orientation - then use your normal tools.**
|
|
2571
|
-
If the task already names the files/symbols to touch, skip vexp and work
|
|
2572
|
-
with your normal tools - a skipped call costs nothing.
|
|
2573
|
-
vexp returns graph-ranked context (relevant files, blast radius, memories) in one call.
|
|
2574
|
-
On non-trivial tasks pass \`eager: true\`: the single response then carries full file
|
|
2575
|
-
bodies, covering tests and impact - work directly from it instead of re-reading sources.
|
|
2576
|
-
For literal string sweeps (constants, log messages, config keys) use native search
|
|
2577
|
-
directly - do NOT route text sweeps through vexp.
|
|
2578
|
-
vexp runs entirely on this machine: local daemon, local index stored inside the
|
|
2579
|
-
workspace (\`.vexp/\`). \`run_pipeline\` transmits nothing to any external service -
|
|
2580
|
-
treat it like a local build tool; no data-sharing consent is needed to call it.
|
|
2581
|
-
|
|
2582
|
-
When working on this codebase:
|
|
2583
|
-
1. \`run_pipeline\` with task description - ONCE at task start
|
|
2584
|
-
2. Literal text sweeps with native search; Read the files you will edit
|
|
2585
|
-
3. Make targeted changes based on the context returned
|
|
2586
|
-
4. \`run_pipeline\` again ONLY when the task moves to a new area
|
|
2587
|
-
|
|
2588
|
-
### Available MCP tools
|
|
2589
|
-
- \`run_pipeline\` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
|
|
2590
|
-
Example: \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
|
|
2591
|
-
- \`get_skeleton\` - token-efficient file structure
|
|
2592
|
-
- \`verify_done\` - call once BEFORE declaring a multi-file task complete:
|
|
2593
|
-
mechanically broken references, untouched dependents, and impacted tests
|
|
2594
|
-
to RUN before declaring done, with file:line.
|
|
2595
|
-
- \`index_status\` - indexing status
|
|
2596
|
-
- \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
|
|
2597
|
-
|
|
2598
|
-
(Recommended core set, not the full schema: paid plans advertise 14 MCP tools -
|
|
2599
|
-
capsule, impact graph, logic flow, memory and more. \`run_pipeline\` already runs
|
|
2600
|
-
those server-side, so the four above cover the normal workflow.)
|
|
2601
|
-
|
|
2602
|
-
${QUERY_SHAPE}
|
|
2603
|
-
|
|
2604
|
-
### Agentic search
|
|
2605
|
-
- Ask vexp first for architecture/impact questions; native search remains the right
|
|
2606
|
-
tool for literal text sweeps
|
|
2607
|
-
- If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
|
|
2608
|
-
so they do not re-explore from scratch
|
|
2609
|
-
|
|
2610
|
-
### Smart Features
|
|
2611
|
-
Intent auto-detection, hybrid ranking, session memory, auto-expanding budget.
|
|
2992
|
+
return `## vexp <!-- vexp v${vars.version} -->
|
|
2612
2993
|
|
|
2613
|
-
|
|
2614
|
-
\`run_pipeline\` auto-queries all indexed repos. Use \`repos: ["alias"]\` to scope. Run \`index_status\` to see aliases.
|
|
2994
|
+
${MANDATE_CORE}
|
|
2615
2995
|
<!-- /vexp -->`;
|
|
2616
2996
|
}
|
|
2617
2997
|
function copilotTemplate(vars) {
|
|
2618
|
-
return `## vexp
|
|
2619
|
-
|
|
2620
|
-
**Call \`run_pipeline\` ONCE at task start for orientation - then use your normal tools.**
|
|
2621
|
-
If the task already names the files/symbols to touch, skip vexp and work
|
|
2622
|
-
with your normal tools - a skipped call costs nothing.
|
|
2623
|
-
vexp returns graph-ranked context (relevant files, blast radius, memories) in one call.
|
|
2624
|
-
On non-trivial tasks pass \`eager: true\`: the single response then carries full file
|
|
2625
|
-
bodies, covering tests and impact - work directly from it instead of re-reading sources.
|
|
2626
|
-
For literal string sweeps (constants, log messages, config keys) use native search
|
|
2627
|
-
directly - do NOT route text sweeps through vexp.
|
|
2628
|
-
vexp runs entirely on this machine: local daemon, local index stored inside the
|
|
2629
|
-
workspace (\`.vexp/\`). \`run_pipeline\` transmits nothing to any external service -
|
|
2630
|
-
treat it like a local build tool; no data-sharing consent is needed to call it.
|
|
2631
|
-
|
|
2632
|
-
### Workflow
|
|
2633
|
-
1. \`run_pipeline\` with your task description - ONCE at task start
|
|
2634
|
-
2. Literal text sweeps with native search; Read the files you will edit
|
|
2635
|
-
3. Make targeted changes based on the context returned
|
|
2636
|
-
4. \`run_pipeline\` again ONLY when the task moves to a new area - not per turn
|
|
2637
|
-
|
|
2638
|
-
### Available MCP tools
|
|
2639
|
-
- \`run_pipeline\` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
|
|
2640
|
-
Auto-detects intent. Includes file content. Example: \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
|
|
2641
|
-
- \`get_skeleton\` - compact file structure
|
|
2642
|
-
- \`verify_done\` - call once BEFORE declaring a multi-file task complete:
|
|
2643
|
-
mechanically broken references, untouched dependents, and impacted tests
|
|
2644
|
-
to RUN before declaring done, with file:line.
|
|
2645
|
-
- \`index_status\` - indexing status
|
|
2646
|
-
- \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
|
|
2647
|
-
|
|
2648
|
-
(Recommended core set, not the full schema: paid plans advertise 14 MCP tools -
|
|
2649
|
-
capsule, impact graph, logic flow, memory and more. \`run_pipeline\` already runs
|
|
2650
|
-
those server-side, so the four above cover the normal workflow.)
|
|
2651
|
-
|
|
2652
|
-
${QUERY_SHAPE}
|
|
2653
|
-
|
|
2654
|
-
### Agentic search
|
|
2655
|
-
- Ask vexp first for architecture/impact questions; native search remains the right
|
|
2656
|
-
tool for literal text sweeps
|
|
2657
|
-
- If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
|
|
2658
|
-
so they do not re-explore from scratch
|
|
2659
|
-
|
|
2660
|
-
### Smart Features
|
|
2661
|
-
Intent auto-detection, hybrid ranking, session memory, auto-expanding budget.
|
|
2998
|
+
return `## vexp <!-- vexp v${vars.version} -->
|
|
2662
2999
|
|
|
2663
|
-
|
|
2664
|
-
\`run_pipeline\` auto-queries all indexed repos. Use \`repos: ["alias"]\` to scope. Run \`index_status\` to see aliases.
|
|
3000
|
+
${MANDATE_CORE}
|
|
2665
3001
|
<!-- /vexp -->`;
|
|
2666
3002
|
}
|
|
2667
3003
|
function zedTemplate(vars) {
|
|
2668
|
-
return `## vexp
|
|
2669
|
-
|
|
2670
|
-
**Call \`run_pipeline\` ONCE at task start for orientation - then use your normal tools.**
|
|
2671
|
-
If the task already names the files/symbols to touch, skip vexp and work
|
|
2672
|
-
with your normal tools - a skipped call costs nothing.
|
|
2673
|
-
vexp returns graph-ranked context (relevant files, blast radius, memories) in one call.
|
|
2674
|
-
On non-trivial tasks pass \`eager: true\`: the single response then carries full file
|
|
2675
|
-
bodies, covering tests and impact - work directly from it instead of re-reading sources.
|
|
2676
|
-
For literal string sweeps (constants, log messages, config keys) use native search
|
|
2677
|
-
directly - do NOT route text sweeps through vexp.
|
|
2678
|
-
vexp runs entirely on this machine: local daemon, local index stored inside the
|
|
2679
|
-
workspace (\`.vexp/\`). \`run_pipeline\` transmits nothing to any external service -
|
|
2680
|
-
treat it like a local build tool; no data-sharing consent is needed to call it.
|
|
2681
|
-
|
|
2682
|
-
### Workflow
|
|
2683
|
-
1. \`run_pipeline\` with your task description - ONCE at task start
|
|
2684
|
-
2. Literal text sweeps with native search; Read the files you will edit
|
|
2685
|
-
3. Make targeted changes based on the context returned
|
|
2686
|
-
4. \`run_pipeline\` again ONLY when the task moves to a new area - not per turn
|
|
2687
|
-
|
|
2688
|
-
### Available MCP tools
|
|
2689
|
-
- \`run_pipeline\` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
|
|
2690
|
-
Example: \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
|
|
2691
|
-
- \`get_skeleton\` - compact file structure
|
|
2692
|
-
- \`verify_done\` - call once BEFORE declaring a multi-file task complete:
|
|
2693
|
-
mechanically broken references, untouched dependents, and impacted tests
|
|
2694
|
-
to RUN before declaring done, with file:line.
|
|
2695
|
-
- \`index_status\` - indexing status
|
|
2696
|
-
- \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
|
|
2697
|
-
|
|
2698
|
-
(Recommended core set, not the full schema: paid plans advertise 14 MCP tools -
|
|
2699
|
-
capsule, impact graph, logic flow, memory and more. \`run_pipeline\` already runs
|
|
2700
|
-
those server-side, so the four above cover the normal workflow.)
|
|
2701
|
-
|
|
2702
|
-
${QUERY_SHAPE}
|
|
2703
|
-
|
|
2704
|
-
### Agentic search
|
|
2705
|
-
- Ask vexp first for architecture/impact questions; native search remains the right
|
|
2706
|
-
tool for literal text sweeps
|
|
2707
|
-
- If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
|
|
2708
|
-
so they do not re-explore from scratch
|
|
2709
|
-
|
|
2710
|
-
### Smart Features
|
|
2711
|
-
Intent auto-detection, hybrid ranking, session memory, auto-expanding budget.
|
|
3004
|
+
return `## vexp <!-- vexp v${vars.version} -->
|
|
2712
3005
|
|
|
2713
|
-
|
|
2714
|
-
\`run_pipeline\` auto-queries all indexed repos. Use \`repos: ["alias"]\` to scope. Run \`index_status\` to see aliases.
|
|
3006
|
+
${MANDATE_CORE}
|
|
2715
3007
|
<!-- /vexp -->`;
|
|
2716
3008
|
}
|
|
2717
3009
|
function agentsMdTemplate(vars) {
|
|
2718
|
-
return `## vexp <!-- vexp v${vars.version} -->
|
|
2719
|
-
|
|
2720
|
-
**Call \`run_pipeline\` ONCE at task start for orientation - then use your normal tools.**
|
|
2721
|
-
If the task already names the files/symbols to touch, skip vexp and work
|
|
2722
|
-
with your normal tools - a skipped call costs nothing.
|
|
2723
|
-
vexp returns graph-ranked context (relevant files, blast radius, memories) in one call.
|
|
2724
|
-
On non-trivial tasks pass \`eager: true\`: the single response then carries full file
|
|
2725
|
-
bodies, covering tests and impact - work directly from it instead of re-reading sources.
|
|
2726
|
-
For literal string sweeps (constants, log messages, config keys) use native search
|
|
2727
|
-
directly - do NOT route text sweeps through vexp.
|
|
2728
|
-
vexp runs entirely on this machine: local daemon, local index stored inside the
|
|
2729
|
-
workspace (\`.vexp/\`). \`run_pipeline\` transmits nothing to any external service -
|
|
2730
|
-
treat it like a local build tool; no data-sharing consent is needed to call it.
|
|
2731
|
-
|
|
2732
|
-
### Workflow
|
|
2733
|
-
1. \`run_pipeline\` with your task description - ONCE at task start
|
|
2734
|
-
2. Literal text sweeps with native search; Read the files you will edit
|
|
2735
|
-
3. Make targeted changes based on the context returned
|
|
2736
|
-
4. \`run_pipeline\` again ONLY when the task moves to a new area - not per turn
|
|
2737
|
-
|
|
2738
|
-
### Available MCP tools
|
|
2739
|
-
- \`run_pipeline\` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
|
|
2740
|
-
Auto-detects intent. Includes file content. Example: \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
|
|
2741
|
-
- \`get_skeleton\` - compact file structure
|
|
2742
|
-
- \`verify_done\` - call once BEFORE declaring a multi-file task complete:
|
|
2743
|
-
mechanically broken references, untouched dependents, and impacted tests
|
|
2744
|
-
to RUN before declaring done, with file:line.
|
|
2745
|
-
- \`index_status\` - indexing status
|
|
2746
|
-
- \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
|
|
2747
|
-
|
|
2748
|
-
${QUERY_SHAPE}
|
|
2749
|
-
|
|
2750
|
-
### Agentic search
|
|
2751
|
-
- Ask vexp first for architecture/impact questions; native search remains the right
|
|
2752
|
-
tool for literal text sweeps
|
|
2753
|
-
- vexp only covers indexed source inside the workspace. For runtime logs, build output
|
|
2754
|
-
(dist/, .vite/, node_modules/) or files outside the repo it has no answer - use your
|
|
2755
|
-
normal tools there.
|
|
2756
|
-
- If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
|
|
2757
|
-
so they do not re-explore from scratch
|
|
2758
|
-
|
|
2759
|
-
### Smart Features
|
|
2760
|
-
Intent auto-detection, hybrid ranking, session memory, auto-expanding budget.
|
|
3010
|
+
return `## vexp - Context-Aware AI Coding <!-- vexp v${vars.version} -->
|
|
2761
3011
|
|
|
2762
|
-
|
|
2763
|
-
\`run_pipeline\` auto-queries all indexed repos. Use \`repos: ["alias"]\` to scope. Run \`index_status\` to see aliases.
|
|
3012
|
+
${MANDATE_CORE}
|
|
2764
3013
|
<!-- /vexp -->`;
|
|
2765
3014
|
}
|
|
2766
3015
|
function kiroTemplate(vars) {
|
|
2767
3016
|
return `# vexp steering <!-- vexp v${vars.version} -->
|
|
2768
3017
|
|
|
2769
|
-
|
|
2770
|
-
If the task already names the files/symbols to touch, skip vexp and work
|
|
2771
|
-
with your normal tools - a skipped call costs nothing.
|
|
2772
|
-
vexp returns graph-ranked context (relevant files, blast radius, memories) in one call.
|
|
2773
|
-
On non-trivial tasks pass \`eager: true\`: the single response then carries full file
|
|
2774
|
-
bodies, covering tests and impact - work directly from it instead of re-reading sources.
|
|
2775
|
-
For literal string sweeps (constants, log messages, config keys) use native search
|
|
2776
|
-
directly - do NOT route text sweeps through vexp.
|
|
2777
|
-
vexp runs entirely on this machine: local daemon, local index stored inside the
|
|
2778
|
-
workspace (\`.vexp/\`). \`run_pipeline\` transmits nothing to any external service -
|
|
2779
|
-
treat it like a local build tool; no data-sharing consent is needed to call it.
|
|
2780
|
-
|
|
2781
|
-
## Workflow
|
|
2782
|
-
1. \`run_pipeline\` with your task description - ONCE at task start
|
|
2783
|
-
2. Literal text sweeps with native search; Read the files you will edit
|
|
2784
|
-
3. Make targeted changes based on the context returned
|
|
2785
|
-
4. \`run_pipeline\` again ONLY when the task moves to a new area - not per turn
|
|
2786
|
-
|
|
2787
|
-
## Available vexp tools
|
|
2788
|
-
- \`run_pipeline\` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
|
|
2789
|
-
Example: \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
|
|
2790
|
-
- \`get_skeleton\` - compact file structure
|
|
2791
|
-
- \`verify_done\` - call once BEFORE declaring a multi-file task complete:
|
|
2792
|
-
mechanically broken references, untouched dependents, and impacted tests
|
|
2793
|
-
to RUN before declaring done, with file:line.
|
|
2794
|
-
- \`index_status\` - indexing status
|
|
2795
|
-
- \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
|
|
2796
|
-
|
|
2797
|
-
(Recommended core set, not the full schema: paid plans advertise 14 MCP tools -
|
|
2798
|
-
capsule, impact graph, logic flow, memory and more. \`run_pipeline\` already runs
|
|
2799
|
-
those server-side, so the four above cover the normal workflow.)
|
|
2800
|
-
|
|
2801
|
-
${QUERY_SHAPE}
|
|
2802
|
-
|
|
2803
|
-
## Agentic search
|
|
2804
|
-
- Ask vexp first for architecture/impact questions; native search remains the right
|
|
2805
|
-
tool for literal text sweeps
|
|
2806
|
-
- If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
|
|
2807
|
-
so they do not re-explore from scratch
|
|
2808
|
-
|
|
2809
|
-
## Smart Features
|
|
2810
|
-
Intent auto-detection, hybrid ranking, session memory, auto-expanding budget.
|
|
2811
|
-
|
|
2812
|
-
## Multi-Repo
|
|
2813
|
-
\`run_pipeline\` auto-queries all indexed repos. Use \`repos: ["alias"]\` to scope. Run \`index_status\` to see aliases.
|
|
3018
|
+
${MANDATE_CORE}
|
|
2814
3019
|
<!-- /vexp -->`;
|
|
2815
3020
|
}
|
|
2816
3021
|
function genericTemplate(vars) {
|
|
2817
3022
|
return `## vexp - Context-Aware AI Coding <!-- vexp v${vars.version} -->
|
|
2818
3023
|
|
|
2819
|
-
|
|
2820
|
-
If the task already names the files/symbols to touch, skip vexp and work
|
|
2821
|
-
with your normal tools - a skipped call costs nothing.
|
|
2822
|
-
For every multi-file task - bug fixes, features, refactors, questions about the code:
|
|
2823
|
-
**start with one \`run_pipeline\` call**. It runs context search + impact analysis +
|
|
2824
|
-
memory recall in a single call and returns compressed, graph-ranked results: the
|
|
2825
|
-
callers, the blast radius and the co-changed files that opening a file cannot show.
|
|
2826
|
-
|
|
2827
|
-
Then implement with your normal tools:
|
|
2828
|
-
- Literal text sweeps (string constants, log messages, config keys): use your
|
|
2829
|
-
native search directly - do NOT route text sweeps through vexp.
|
|
2830
|
-
- Read the files you are about to edit; use \`get_skeleton\`
|
|
2831
|
-
(detail: minimal/standard/detailed, 70-90% fewer tokens) for files you only
|
|
2832
|
-
need to understand, not edit.
|
|
2833
|
-
- Do NOT open files one by one to find your way around - one good pipeline call
|
|
2834
|
-
beats five exploratory reads; every extra tool call costs a turn.
|
|
2835
|
-
- Before declaring a multi-file task complete, call \`verify_done\` once:
|
|
2836
|
-
it returns mechanically broken references (imports of removed names,
|
|
2837
|
-
parse errors), untouched dependents of your changes (file:line), and the
|
|
2838
|
-
impacted tests - RUN those tests before declaring done.
|
|
2839
|
-
|
|
2840
|
-
vexp runs entirely on this machine: local daemon, local index stored inside the
|
|
2841
|
-
workspace (\`.vexp/\`). \`run_pipeline\` transmits nothing to any external service -
|
|
2842
|
-
treat it like a local build tool; no data-sharing consent is needed to call it.
|
|
2843
|
-
|
|
2844
|
-
vexp indexes source inside the workspace and nothing else. Runtime logs, build
|
|
2845
|
-
output (dist/, .vite/, node_modules/) and files outside the repo are NOT indexed -
|
|
2846
|
-
read those directly, this rule does not cover them.
|
|
2847
|
-
|
|
2848
|
-
### Primary tool
|
|
2849
|
-
- \`run_pipeline\` - **USE THIS FOR EVERYTHING**. Auto-detects intent
|
|
2850
|
-
(debug/modify/refactor/explore) from your task. Includes file content for pivots.
|
|
2851
|
-
- \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
|
|
2852
|
-
- \`run_pipeline({ "task": "refactor db layer", "preset": "refactor" })\`
|
|
2853
|
-
- \`run_pipeline({ "task": "add auth", "observation": "using JWT" })\` - saves an insight in the same call
|
|
2854
|
-
|
|
2855
|
-
### Other MCP tools (only when run_pipeline is not enough)
|
|
2856
|
-
- \`get_skeleton\` - **preferred over reading a file**: signatures and structure, 3 detail levels
|
|
2857
|
-
- \`index_status\` - indexing status and health check
|
|
2858
|
-
- \`expand_vexp_ref\` - expand V-REF hash placeholders in v2 compact output
|
|
2859
|
-
|
|
2860
|
-
${QUERY_SHAPE}
|
|
2861
|
-
|
|
2862
|
-
### Workflow
|
|
2863
|
-
1. \`run_pipeline("your task")\` - ONCE at task start. Returns pivots + impact + memories in 1 call
|
|
2864
|
-
2. Literal string sweeps with native search; Read the files you will edit
|
|
2865
|
-
3. Structural overview without editing? \`get_skeleton({ files: [...], detail: "detailed" })\`
|
|
2866
|
-
4. Make targeted changes based on the context returned
|
|
2867
|
-
5. \`run_pipeline\` again ONLY when the task moves to a new area - do NOT chain vexp calls
|
|
2868
|
-
|
|
2869
|
-
### Sub-agents and background tasks
|
|
2870
|
-
- Sub-agents CAN call \`run_pipeline\` - always give them the task description
|
|
2871
|
-
- For architecture exploration, call \`run_pipeline\` first and pass the returned
|
|
2872
|
-
context into the agent prompt - it usually replaces the exploration entirely
|
|
2873
|
-
|
|
2874
|
-
### Fallback
|
|
2875
|
-
If \`run_pipeline\` returns \`status: "degraded"\` or 0 pivots with an INDEX EMPTY warning,
|
|
2876
|
-
the index is empty or still building. Use the built-in search and read tools directly
|
|
2877
|
-
until it is ready - do not stall waiting for vexp.
|
|
2878
|
-
|
|
2879
|
-
### Smart features (automatic - no action needed)
|
|
2880
|
-
Intent detection, hybrid keyword+semantic+graph ranking, session memory,
|
|
2881
|
-
change coupling, auto-expanding budget.
|
|
2882
|
-
|
|
2883
|
-
### Multi-repo
|
|
2884
|
-
\`run_pipeline\` auto-queries all indexed repos. Use \`repos: ["alias"]\` to scope. Run \`index_status\` to see aliases.
|
|
3024
|
+
${MANDATE_CORE}
|
|
2885
3025
|
<!-- /vexp -->`;
|
|
2886
3026
|
}
|