vexp-cli 2.6.3 → 2.7.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 +90 -2
- package/dist/doctor.js +84 -30
- package/dist/hook-template.js +49 -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, 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 } from "./hook-template.js";
|
|
14
14
|
// ---------------------------------------------------------------------------
|
|
15
15
|
// Constants
|
|
16
16
|
// ---------------------------------------------------------------------------
|
|
@@ -403,6 +403,7 @@ export function configureAgents(workspaceRoot, binaryPath, version, agentFilter,
|
|
|
403
403
|
// Event-driven hint hook: default-on (non-blocking, fail-open — the
|
|
404
404
|
// 2.3 A2 opt-in applies to DENY hooks, this one cannot block).
|
|
405
405
|
const hintResult = installClaudeCodeHintHook(workspaceRoot, binaryPath);
|
|
406
|
+
installClaudeCodeSearchHook(workspaceRoot, binaryPath);
|
|
406
407
|
installClaudeCodeStopGate(workspaceRoot, binaryPath);
|
|
407
408
|
installClaudeCodeSessionContext(workspaceRoot, binaryPath);
|
|
408
409
|
if (hintResult) {
|
|
@@ -2026,6 +2027,69 @@ function isVexpHintHookEntry(h) {
|
|
|
2026
2027
|
* opt-in applies to DENY hooks, which add a failure mode; this one cannot
|
|
2027
2028
|
* block anything).
|
|
2028
2029
|
*/
|
|
2030
|
+
/** Does this PreToolUse entry belong to us? */
|
|
2031
|
+
function isVexpSearchHookEntry(h) {
|
|
2032
|
+
if (!h || typeof h !== "object")
|
|
2033
|
+
return false;
|
|
2034
|
+
const hks = h.hooks;
|
|
2035
|
+
return Array.isArray(hks) && hks.some((hook) => typeof hook?.command === "string" &&
|
|
2036
|
+
hook.command.includes("vexp-search"));
|
|
2037
|
+
}
|
|
2038
|
+
/**
|
|
2039
|
+
* v4 M1: install the search answer as a PreToolUse hook on Bash.
|
|
2040
|
+
*
|
|
2041
|
+
* Matcher is Bash and nothing else. The hook rewrites only a recursive grep
|
|
2042
|
+
* for names this index defines, keeps the original command after a `||`, and
|
|
2043
|
+
* prints nothing at all in every other case — so a session where vexp has no
|
|
2044
|
+
* opinion is byte-for-byte a session without vexp.
|
|
2045
|
+
*
|
|
2046
|
+
* Why a hook rather than a tool: 9 of 227 measured agent sessions called the
|
|
2047
|
+
* MCP tools. All 227 called Bash.
|
|
2048
|
+
*/
|
|
2049
|
+
export function installClaudeCodeSearchHook(workspaceRoot, binaryPath) {
|
|
2050
|
+
const hookDir = path.join(workspaceRoot, ".claude", "hooks");
|
|
2051
|
+
const hookPath = path.join(hookDir, "vexp-search.sh");
|
|
2052
|
+
const settingsPath = path.join(workspaceRoot, ".claude", "settings.json");
|
|
2053
|
+
const script = vexpSearchHookScript(binaryPath);
|
|
2054
|
+
fs.mkdirSync(hookDir, { recursive: true });
|
|
2055
|
+
const existed = fs.existsSync(hookPath);
|
|
2056
|
+
const scriptIdentical = existed && fs.readFileSync(hookPath, "utf-8") === script;
|
|
2057
|
+
if (!scriptIdentical)
|
|
2058
|
+
fs.writeFileSync(hookPath, script, { mode: 0o755 });
|
|
2059
|
+
const read = readJsonConfigSafe(settingsPath);
|
|
2060
|
+
if (!read.ok) {
|
|
2061
|
+
warnUnparseable(settingsPath);
|
|
2062
|
+
return scriptIdentical ? null : existed ? "updated" : "created";
|
|
2063
|
+
}
|
|
2064
|
+
const settings = read.data;
|
|
2065
|
+
const hooks = (settings.hooks ?? {});
|
|
2066
|
+
const existing = Array.isArray(hooks.PreToolUse) ? hooks.PreToolUse : [];
|
|
2067
|
+
const filtered = existing.filter((h) => !isVexpSearchHookEntry(h));
|
|
2068
|
+
// Same shell-form-with-quoted-path shape as every other hook we write: exec
|
|
2069
|
+
// form cannot run a .sh on Windows, and an unquoted path word-splits on
|
|
2070
|
+
// "C:\Program Files" (both learned the hard way, c4a0b9e).
|
|
2071
|
+
filtered.push({
|
|
2072
|
+
matcher: "Bash",
|
|
2073
|
+
hooks: [
|
|
2074
|
+
{
|
|
2075
|
+
type: "command",
|
|
2076
|
+
command: 'bash "$CLAUDE_PROJECT_DIR/.claude/hooks/vexp-search.sh"',
|
|
2077
|
+
timeout: 10,
|
|
2078
|
+
},
|
|
2079
|
+
],
|
|
2080
|
+
});
|
|
2081
|
+
const merged = { ...hooks, PreToolUse: filtered };
|
|
2082
|
+
const settingsIdentical = JSON.stringify(merged) === JSON.stringify(settings.hooks ?? {});
|
|
2083
|
+
if (scriptIdentical && settingsIdentical)
|
|
2084
|
+
return null;
|
|
2085
|
+
settings.hooks = merged;
|
|
2086
|
+
if (!settingsIdentical) {
|
|
2087
|
+
if (read.existed)
|
|
2088
|
+
backupConfig(settingsPath);
|
|
2089
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
2090
|
+
}
|
|
2091
|
+
return existed ? "updated" : "created";
|
|
2092
|
+
}
|
|
2029
2093
|
export function installClaudeCodeHintHook(workspaceRoot, binaryPath) {
|
|
2030
2094
|
const hookDir = path.join(workspaceRoot, ".claude", "hooks");
|
|
2031
2095
|
const hookPath = path.join(hookDir, "vexp-hint.sh");
|
|
@@ -2100,10 +2164,33 @@ export function installCodexHintHook(workspaceRoot, binaryPath) {
|
|
|
2100
2164
|
const script = vexpHintHookScript(binaryPath);
|
|
2101
2165
|
fs.mkdirSync(dir, { recursive: true });
|
|
2102
2166
|
const existed = fs.existsSync(hookPath);
|
|
2103
|
-
|
|
2167
|
+
let scriptIdentical = existed && fs.readFileSync(hookPath, "utf-8") === script;
|
|
2104
2168
|
if (!scriptIdentical) {
|
|
2105
2169
|
fs.writeFileSync(hookPath, script, { mode: 0o755 });
|
|
2106
2170
|
}
|
|
2171
|
+
// Windows gets a batch twin. Codex runs a hook command through
|
|
2172
|
+
// `cmd.exe /C` (COMSPEC, codex-rs/hooks command_runner), so `bash "..."`
|
|
2173
|
+
// reaches a shell that has never heard of bash: a user with neither Git
|
|
2174
|
+
// Bash nor WSL got "bash not found" on every prompt and orientation was
|
|
2175
|
+
// inert. Codex's own answer is `commandWindows`, an optional per-OS
|
|
2176
|
+
// override on the command handler (openai/codex#22159, ~0.131, months
|
|
2177
|
+
// before the version that requires the nested `hooks` shape we already
|
|
2178
|
+
// write). Older builds ignore the field rather than reject the file —
|
|
2179
|
+
// the handler is not deny_unknown_fields, unlike the top level.
|
|
2180
|
+
const cmdPath = path.join(dir, "vexp-hint.cmd");
|
|
2181
|
+
let cmdCommand;
|
|
2182
|
+
if (process.platform === "win32") {
|
|
2183
|
+
const cmdScript = vexpHintHookCmdScript(binaryPath);
|
|
2184
|
+
const cmdExisted = fs.existsSync(cmdPath);
|
|
2185
|
+
if (!cmdExisted || fs.readFileSync(cmdPath, "utf-8") !== cmdScript) {
|
|
2186
|
+
fs.writeFileSync(cmdPath, cmdScript);
|
|
2187
|
+
scriptIdentical = false;
|
|
2188
|
+
}
|
|
2189
|
+
// Quoted: Codex hands `cmd /C` the whole line wrapped in one more pair
|
|
2190
|
+
// of quotes, and cmd then strips the outermost pair — which leaves this
|
|
2191
|
+
// path quoted and therefore safe to contain spaces.
|
|
2192
|
+
cmdCommand = `"${cmdPath}"`;
|
|
2193
|
+
}
|
|
2107
2194
|
let root = {};
|
|
2108
2195
|
if (fs.existsSync(hooksJsonPath)) {
|
|
2109
2196
|
try {
|
|
@@ -2141,6 +2228,7 @@ export function installCodexHintHook(workspaceRoot, binaryPath) {
|
|
|
2141
2228
|
{
|
|
2142
2229
|
type: "command",
|
|
2143
2230
|
command: `bash "${hookPath.replace(/\\/g, "/")}"`,
|
|
2231
|
+
...(cmdCommand ? { commandWindows: cmdCommand } : {}),
|
|
2144
2232
|
timeout: 5,
|
|
2145
2233
|
},
|
|
2146
2234
|
],
|
package/dist/doctor.js
CHANGED
|
@@ -103,6 +103,47 @@ function isAlive(pid) {
|
|
|
103
103
|
return false;
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* The git-hooks verdict, as data.
|
|
108
|
+
*
|
|
109
|
+
* Extracted because it was wrong in a way no test could catch: an overriding
|
|
110
|
+
* `core.hooksPath` was reported as a FAILURE claiming "the index does not
|
|
111
|
+
* refresh on commit/merge/checkout", and the remedy offered was to edit a
|
|
112
|
+
* file that moon, husky and lefthook all regenerate. The claim is untrue —
|
|
113
|
+
* the daemon watches the tree live and reconciles it against disk every five
|
|
114
|
+
* minutes — and the remedy was unusable. A user spent a round trip on it.
|
|
115
|
+
*/
|
|
116
|
+
export function gitHooksVerdict(hooksPath, repoRoot, installedCount) {
|
|
117
|
+
const ourHooksDir = path.join(repoRoot, ".git", "hooks");
|
|
118
|
+
if (!hooksPath) {
|
|
119
|
+
if (installedCount === 3) {
|
|
120
|
+
return { level: OK, message: "vexp hooks present in .git/hooks and git will run them" };
|
|
121
|
+
}
|
|
122
|
+
if (installedCount === 0) {
|
|
123
|
+
return { level: OK, message: "no vexp git hooks (index refreshes on demand)" };
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
level: WARN,
|
|
127
|
+
message: `only ${installedCount}/3 vexp git hooks present — re-run 'vexp hooks install'`,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
const resolved = path.isAbsolute(hooksPath) ? hooksPath : path.join(repoRoot, hooksPath);
|
|
131
|
+
if (path.resolve(resolved) === path.resolve(ourHooksDir)) {
|
|
132
|
+
return {
|
|
133
|
+
level: OK,
|
|
134
|
+
message: `core.hooksPath points at .git/hooks (${installedCount}/3 vexp hooks present)`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
if (installedCount === 0) {
|
|
138
|
+
return { level: OK, message: `core.hooksPath = ${resolved} (no vexp git hooks installed here)` };
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
level: WARN,
|
|
142
|
+
message: `core.hooksPath = ${resolved} — git runs hooks ONLY from there, so the ${installedCount} vexp hook(s) in .git/hooks never run.\n` +
|
|
143
|
+
` the index still refreshes: the daemon watches the tree live and reconciles it against disk every 5 minutes (VEXP_RECONCILE_INTERVAL_SECS).\n` +
|
|
144
|
+
` the hooks only make that immediate — if you want them and a tool manages this directory (moon, husky, lefthook), add 'vexp index --finalize || true' through ITS config, not the generated file.`,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
106
147
|
export async function runDoctor() {
|
|
107
148
|
const home = vexpHome();
|
|
108
149
|
let warns = 0;
|
|
@@ -149,7 +190,26 @@ export async function runDoctor() {
|
|
|
149
190
|
: st.compressor ?? "unknown (older daemon)";
|
|
150
191
|
line(OK, `index: ${st.total_files ?? "?"} files · ${st.total_nodes ?? "?"} nodes · state ${st.status ?? "?"} · compressor ${comp}`);
|
|
151
192
|
if (st.llm_configured_but_inactive === true) {
|
|
152
|
-
|
|
193
|
+
// The daemon knows WHY, and doctor was throwing it away: it printed
|
|
194
|
+
// the same "restart to load the model" the sidebar prints, to a user
|
|
195
|
+
// whose model fails on every start. Restarting is advice only when
|
|
196
|
+
// the cause can change between starts.
|
|
197
|
+
const why = typeof st.llm_inactive_reason === "string" ? st.llm_inactive_reason : undefined;
|
|
198
|
+
const restartIsPointless = why !== undefined &&
|
|
199
|
+
/vanilla build|model load failed|incomplete|did not survive|worker did not start/i.test(why);
|
|
200
|
+
// A daemon still running its startup sync has not REACHED the model
|
|
201
|
+
// yet. Restarting there is worse than useless: it starts the sync
|
|
202
|
+
// over, and the user never arrives.
|
|
203
|
+
const stillStarting = why !== undefined && /still completing its startup/i.test(why);
|
|
204
|
+
line(WARN, `local LLM is installed and enabled in config but this daemon runs the RULE compressor — results are not LLM-compressed.` +
|
|
205
|
+
(why ? `\n reason: ${why}` : "") +
|
|
206
|
+
(stillStarting
|
|
207
|
+
? `\n wait for the sync to finish — do NOT restart, that begins it again` +
|
|
208
|
+
`\n the daemon logs 'LlmWorker active' when the model is up`
|
|
209
|
+
: restartIsPointless
|
|
210
|
+
? `\n restarting will NOT change this — fix the cause above`
|
|
211
|
+
: `\n run 'vexp daemon-cmd restart' to load the model`) +
|
|
212
|
+
`\n daemon: v${st.daemon_version ?? "?"} at ${st.workspace_root ?? "?"}`);
|
|
153
213
|
}
|
|
154
214
|
// 2.4.0 upgrade guard (CLI side): a daemon surviving an upgrade keeps
|
|
155
215
|
// serving the OLD feature set and its gaps read as product bugs
|
|
@@ -435,27 +495,8 @@ export async function runDoctor() {
|
|
|
435
495
|
return false;
|
|
436
496
|
}
|
|
437
497
|
});
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
line(OK, "vexp hooks present in .git/hooks and git will run them");
|
|
441
|
-
else if (installed.length === 0)
|
|
442
|
-
line(OK, "no vexp git hooks (index refreshes on demand)");
|
|
443
|
-
else
|
|
444
|
-
line(WARN, `only ${installed.length}/3 vexp git hooks present — re-run 'vexp hooks install'`);
|
|
445
|
-
}
|
|
446
|
-
else {
|
|
447
|
-
const resolved = path.isAbsolute(hooksPath) ? hooksPath : path.join(ws.root, hooksPath);
|
|
448
|
-
const sameDir = path.resolve(resolved) === path.resolve(ours);
|
|
449
|
-
if (sameDir) {
|
|
450
|
-
line(OK, `core.hooksPath points at .git/hooks (${installed.length}/3 vexp hooks present)`);
|
|
451
|
-
}
|
|
452
|
-
else if (installed.length > 0) {
|
|
453
|
-
line(BAD, `core.hooksPath = ${resolved} — git runs hooks ONLY from there, so the ${installed.length} vexp hook(s) in .git/hooks NEVER run and the index does not refresh on commit/merge/checkout. Add 'vexp index --finalize || true' to ${path.join(resolved, "pre-commit")}, or 'git config --unset core.hooksPath' for this repo.`);
|
|
454
|
-
}
|
|
455
|
-
else {
|
|
456
|
-
line(OK, `core.hooksPath = ${resolved} (no vexp git hooks installed here)`);
|
|
457
|
-
}
|
|
458
|
-
}
|
|
498
|
+
const verdict = gitHooksVerdict(hooksPath, ws.root, installed.length);
|
|
499
|
+
line(verdict.level, verdict.message);
|
|
459
500
|
}
|
|
460
501
|
console.log(chalk.bold("\nClaude Code orientation hooks (.claude/settings.json)"));
|
|
461
502
|
{
|
|
@@ -578,12 +619,23 @@ export async function runDoctor() {
|
|
|
578
619
|
else if (!fs.existsSync(scriptPath)) {
|
|
579
620
|
line(BAD, "hooks.json points at .codex/vexp-hint.sh but the script is missing — the hook fails on every prompt.");
|
|
580
621
|
}
|
|
622
|
+
else if (process.platform === "win32" && typeof hook.commandWindows !== "string") {
|
|
623
|
+
// On Windows Codex runs the command through cmd.exe, which has no
|
|
624
|
+
// bash: without the commandWindows override the entry is inert on
|
|
625
|
+
// every prompt unless Git Bash happens to be on PATH. Probing it
|
|
626
|
+
// through bash here would hide exactly that.
|
|
627
|
+
line(BAD, "the entry has no 'commandWindows' — Codex runs hooks through cmd.exe on Windows, which cannot run the bash script, so orientation never fires (unless Git Bash is on PATH). Re-run 'vexp setup' to write the batch twin.");
|
|
628
|
+
}
|
|
581
629
|
else {
|
|
582
|
-
// Run it
|
|
583
|
-
//
|
|
584
|
-
//
|
|
585
|
-
//
|
|
586
|
-
|
|
630
|
+
// Run it, through the same shell Codex would use: cmd.exe on
|
|
631
|
+
// Windows (COMSPEC), sh elsewhere. The script bakes an absolute path
|
|
632
|
+
// to the vexp binary and exits 0 when that path is not executable, so
|
|
633
|
+
// a stale or wrong-profile path leaves NO trace anywhere: no
|
|
634
|
+
// orientation, no error, forever. A Windows user found exactly that
|
|
635
|
+
// by reading the generated file.
|
|
636
|
+
const isWin = process.platform === "win32";
|
|
637
|
+
const cmdLine = (isWin ? hook.commandWindows : hook.command);
|
|
638
|
+
const r = spawnSync(isWin ? (process.env.COMSPEC || "cmd.exe") : "sh", [isWin ? "/c" : "-c", cmdLine], {
|
|
587
639
|
env: { ...process.env, CLAUDE_PROJECT_DIR: ws.root },
|
|
588
640
|
input: JSON.stringify({ session_id: "vexp-doctor", prompt: "vexp doctor probe", cwd: ws.root }),
|
|
589
641
|
timeout: 10000,
|
|
@@ -591,15 +643,17 @@ export async function runDoctor() {
|
|
|
591
643
|
});
|
|
592
644
|
const baked = (() => {
|
|
593
645
|
try {
|
|
594
|
-
|
|
595
|
-
|
|
646
|
+
// One pattern covers both twins: VEXP_BIN="path" (bash) and
|
|
647
|
+
// set "VEXP_BIN=path" (cmd).
|
|
648
|
+
const probed = isWin ? path.join(ws.root, ".codex", "vexp-hint.cmd") : scriptPath;
|
|
649
|
+
return fs.readFileSync(probed, "utf-8").match(/VEXP_BIN="?([^"\r\n]+)"?/)?.[1];
|
|
596
650
|
}
|
|
597
651
|
catch {
|
|
598
652
|
return undefined;
|
|
599
653
|
}
|
|
600
654
|
})();
|
|
601
655
|
if (r.error) {
|
|
602
|
-
line(BAD, `hook DID NOT RUN: ${r.error.code ?? r.error.message} — orientation is inert
|
|
656
|
+
line(BAD, `hook DID NOT RUN: ${r.error.code ?? r.error.message} — orientation is inert.`);
|
|
603
657
|
}
|
|
604
658
|
else if (r.status !== 0) {
|
|
605
659
|
line(BAD, `hook exited ${r.status}${r.stderr ? ` — ${String(r.stderr).trim().slice(0, 160)}` : ""} — Codex continues without vexp.`);
|
package/dist/hook-template.js
CHANGED
|
@@ -397,6 +397,55 @@ exit 0
|
|
|
397
397
|
export function vexpHintHookScript(binaryPath) {
|
|
398
398
|
return VEXP_HINT_HOOK.replace("__VEXP_BIN__", binaryPath.replace(/\\/g, "/"));
|
|
399
399
|
}
|
|
400
|
+
/**
|
|
401
|
+
* Windows twin of the hint hook. Codex runs a hook command through
|
|
402
|
+
* `cmd.exe /C` (COMSPEC) on Windows, and cmd has no `bash`: a Windows user
|
|
403
|
+
* with neither Git Bash nor WSL got "bash not found" on every prompt, so
|
|
404
|
+
* orientation was inert and nothing said so. The logic is four lines and
|
|
405
|
+
* none of it is shell-specific, so it translates exactly — and `exit /b 0`
|
|
406
|
+
* on the last line keeps the same fail-open contract as the bash twin.
|
|
407
|
+
*
|
|
408
|
+
* CRLF on purpose: a batch file with bare LF endings is parsed
|
|
409
|
+
* inconsistently by cmd, and this one has to run on the machines least
|
|
410
|
+
* likely to have a POSIX toolchain that would normalise it.
|
|
411
|
+
*/
|
|
412
|
+
export const VEXP_HINT_HOOK_CMD = [
|
|
413
|
+
"@echo off",
|
|
414
|
+
"REM vexp-hint: event-driven orientation hint (UserPromptSubmit). Fails open.",
|
|
415
|
+
'set "VEXP_BIN=__VEXP_BIN__"',
|
|
416
|
+
'if not exist "%VEXP_BIN%" exit /b 0',
|
|
417
|
+
'"%VEXP_BIN%" prompt-hint 2>nul',
|
|
418
|
+
"exit /b 0",
|
|
419
|
+
"",
|
|
420
|
+
].join("\r\n");
|
|
421
|
+
/** Bake the binary path into the Windows hint hook. Backslashes stay. */
|
|
422
|
+
export function vexpHintHookCmdScript(binaryPath) {
|
|
423
|
+
return VEXP_HINT_HOOK_CMD.replace("__VEXP_BIN__", binaryPath.replace(/\//g, "\\"));
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* v4 M1: the search answer (PreToolUse on Bash).
|
|
427
|
+
*
|
|
428
|
+
* Of 227 measured agent sessions with vexp installed, 9 called it — 4%. Bash
|
|
429
|
+
* is called in all of them. So this stops asking the model to adopt anything:
|
|
430
|
+
* when it is about to grep the repository for a name the index knows, the
|
|
431
|
+
* command is rewritten to ask vexp first, with the original grep preserved
|
|
432
|
+
* after a `||` so the failure mode is exactly "vexp is not installed".
|
|
433
|
+
*
|
|
434
|
+
* All the logic is in the binary (`vexp-core search-hook`), like prompt-hint:
|
|
435
|
+
* the script is a cross-OS wrapper that exits silently when anything is
|
|
436
|
+
* missing.
|
|
437
|
+
*/
|
|
438
|
+
export const VEXP_SEARCH_HOOK = `#!/bin/bash
|
|
439
|
+
# vexp-search: answer a grep with structure instead of lines. Fails open.
|
|
440
|
+
VEXP_BIN="__VEXP_BIN__"
|
|
441
|
+
[ -x "$VEXP_BIN" ] || exit 0
|
|
442
|
+
"$VEXP_BIN" search-hook 2>/dev/null
|
|
443
|
+
exit 0
|
|
444
|
+
`;
|
|
445
|
+
/** Bake the binary path into the search hook script. */
|
|
446
|
+
export function vexpSearchHookScript(binaryPath) {
|
|
447
|
+
return VEXP_SEARCH_HOOK.replace("__VEXP_BIN__", binaryPath.replace(/\\/g, "/"));
|
|
448
|
+
}
|
|
400
449
|
/**
|
|
401
450
|
* Horizon F2a: Stop-hook verification gate (Claude Code). All logic lives
|
|
402
451
|
* in the Rust binary (stop-gate): mechanical completion check via daemon,
|