@guilz-dev/belay 0.9.2 → 0.9.4
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 +36 -2
- package/dist/adapters/codex/hooks.d.ts +1 -0
- package/dist/adapters/codex/hooks.js +3 -0
- package/dist/adapters/codex/runtime-entry.d.ts +3 -0
- package/dist/adapters/codex/runtime-entry.js +27 -4
- package/dist/adapters/cursor/cwd-resolution.d.ts +10 -0
- package/dist/adapters/cursor/cwd-resolution.js +58 -0
- package/dist/adapters/cursor/dispatcher-generation.d.ts +3 -0
- package/dist/adapters/cursor/dispatcher-generation.js +5 -0
- package/dist/adapters/cursor/hook-dispatch-entry.d.ts +7 -0
- package/dist/adapters/cursor/hook-dispatch-entry.js +126 -0
- package/dist/adapters/cursor/hook-router.d.ts +23 -0
- package/dist/adapters/cursor/hook-router.js +266 -0
- package/dist/adapters/cursor/hooks.d.ts +5 -0
- package/dist/adapters/cursor/hooks.js +115 -3
- package/dist/adapters/cursor/routing-layout.d.ts +5 -0
- package/dist/adapters/cursor/routing-layout.js +29 -0
- package/dist/adapters/cursor/runtime-entry.d.ts +18 -0
- package/dist/adapters/cursor/runtime-entry.js +135 -34
- package/dist/adapters/layouts/scope.d.ts +5 -0
- package/dist/adapters/layouts/scope.js +71 -0
- package/dist/adapters/shared/gate-runtime.js +27 -3
- package/dist/adapters/shared/repo-root.js +20 -1
- package/dist/bundle/claude-runtime.mjs +2660 -1951
- package/dist/bundle/codex-runtime.mjs +2680 -1953
- package/dist/bundle/cursor-dispatcher.mjs +443 -0
- package/dist/bundle/cursor-runtime.mjs +5638 -4697
- package/dist/cli.js +33 -3
- package/dist/commands/doctor.js +215 -16
- package/dist/commands/health-snapshot.d.ts +3 -0
- package/dist/commands/health-snapshot.js +61 -0
- package/dist/commands/recovery-checkpoints.d.ts +3 -3
- package/dist/commands/report.js +14 -0
- package/dist/commands/status.js +15 -0
- package/dist/commands/where.d.ts +4 -0
- package/dist/commands/where.js +52 -0
- package/dist/config-io.js +11 -2
- package/dist/core/approval-repo-lookup.d.ts +16 -0
- package/dist/core/approval-repo-lookup.js +48 -0
- package/dist/core/audit-io.d.ts +1 -1
- package/dist/core/audit-io.js +1 -1
- package/dist/core/audit-legacy-archive.js +43 -25
- package/dist/core/audit-query.d.ts +1 -0
- package/dist/core/audit-query.js +7 -0
- package/dist/core/audit-serialize.d.ts +3 -0
- package/dist/core/audit-serialize.js +39 -3
- package/dist/core/audit-summary.d.ts +9 -0
- package/dist/core/audit-summary.js +58 -1
- package/dist/core/audit-types.d.ts +4 -0
- package/dist/core/classify-tool.js +16 -8
- package/dist/core/effect-ir/argv-delegate.d.ts +9 -0
- package/dist/core/effect-ir/argv-delegate.js +42 -0
- package/dist/core/effect-ir/shell-lower/argv-delegate-gate.d.ts +3 -0
- package/dist/core/effect-ir/shell-lower/argv-delegate-gate.js +40 -0
- package/dist/core/effect-ir/shell-lower/augment.d.ts +4 -0
- package/dist/core/effect-ir/shell-lower/augment.js +97 -0
- package/dist/core/effect-ir/shell-lower/context.d.ts +10 -0
- package/dist/core/effect-ir/shell-lower/context.js +1 -0
- package/dist/core/effect-ir/shell-lower/decode-process.d.ts +9 -0
- package/dist/core/effect-ir/shell-lower/decode-process.js +201 -0
- package/dist/core/effect-ir/shell-lower/decoders/belay.d.ts +2 -0
- package/dist/core/effect-ir/shell-lower/decoders/belay.js +29 -0
- package/dist/core/effect-ir/shell-lower/decoders/builtins.d.ts +7 -0
- package/dist/core/effect-ir/shell-lower/decoders/builtins.js +194 -0
- package/dist/core/effect-ir/shell-lower/decoders/decode-package-exec.d.ts +4 -0
- package/dist/core/effect-ir/shell-lower/decoders/decode-package-exec.js +68 -0
- package/dist/core/effect-ir/shell-lower/decoders/docker.d.ts +3 -0
- package/dist/core/effect-ir/shell-lower/decoders/docker.js +43 -0
- package/dist/core/effect-ir/shell-lower/decoders/filesystem.d.ts +9 -0
- package/dist/core/effect-ir/shell-lower/decoders/filesystem.js +217 -0
- package/dist/core/effect-ir/shell-lower/decoders/prisma.d.ts +2 -0
- package/dist/core/effect-ir/shell-lower/decoders/prisma.js +54 -0
- package/dist/core/effect-ir/shell-lower/decoders/ruby.d.ts +11 -0
- package/dist/core/effect-ir/shell-lower/decoders/ruby.js +147 -0
- package/dist/core/effect-ir/shell-lower/decoders/toolchain.d.ts +7 -0
- package/dist/core/effect-ir/shell-lower/decoders/toolchain.js +155 -0
- package/dist/core/effect-ir/shell-lower/requirement.d.ts +12 -0
- package/dist/core/effect-ir/shell-lower/requirement.js +65 -0
- package/dist/core/effect-ir/shell-lower/segment.d.ts +11 -0
- package/dist/core/effect-ir/shell-lower/segment.js +82 -0
- package/dist/core/effect-ir/shell-lower/tokens.d.ts +26 -0
- package/dist/core/effect-ir/shell-lower/tokens.js +183 -0
- package/dist/core/effect-ir/shell-lower.d.ts +2 -7
- package/dist/core/effect-ir/shell-lower.js +122 -1511
- package/dist/core/glob.js +32 -16
- package/dist/core/integrity.d.ts +2 -2
- package/dist/core/integrity.js +52 -12
- package/dist/core/replay-scrub.d.ts +3 -0
- package/dist/core/replay-scrub.js +30 -3
- package/dist/core/shell-substitution.js +2 -2
- package/dist/core/shell-tokenizer.d.ts +28 -0
- package/dist/core/shell-tokenizer.js +111 -29
- package/dist/core/shell-unparseable.js +3 -1
- package/dist/core/verdict/docker-compose-run.d.ts +18 -0
- package/dist/core/verdict/docker-compose-run.js +136 -0
- package/dist/core/verdict/launcher-resolve.js +28 -28
- package/dist/core/verdict/parser.d.ts +3 -0
- package/dist/core/verdict/parser.js +23 -40
- package/dist/core/verdict/recursive-invocation.d.ts +20 -0
- package/dist/core/verdict/recursive-invocation.js +224 -0
- package/dist/defaults.d.ts +7 -3
- package/dist/defaults.js +35 -21
- package/dist/installer/bootstrap.d.ts +1 -0
- package/dist/installer/bootstrap.js +2 -2
- package/dist/installer/runtime-artifacts.js +43 -13
- package/dist/installer/scope-config.d.ts +2 -2
- package/dist/installer.d.ts +10 -1
- package/dist/installer.js +83 -7
- package/dist/node-resolution.d.ts +1 -0
- package/dist/node-resolution.js +61 -0
- package/dist/templates.d.ts +7 -4
- package/dist/templates.js +42 -4
- package/dist/types.d.ts +35 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -2
- package/skills/belay/SKILL.md +5 -0
- package/skills/belay/belay-report.md +4 -1
package/README.md
CHANGED
|
@@ -44,7 +44,7 @@ equivalent lifecycle points:
|
|
|
44
44
|
| Gate shell / tools / file mutations | `belay-tool-gate` | `beforeShellExecution`, `preToolUse` | `PreToolUse` | `PreToolUse` |
|
|
45
45
|
| Gate subagent launches | `belay-tool-gate` | `subagentStart` | (via `PreToolUse`) | `SubagentStart` |
|
|
46
46
|
| One-shot approvals | `belay-before-submit` | `beforeSubmitPrompt` | `UserPromptSubmit` | `UserPromptSubmit` |
|
|
47
|
-
| Audit log | `belay-audit` | `postToolUse`, `stop`, `sessionEnd` | `PostToolUse` | `PostToolUse` |
|
|
47
|
+
| Audit log | `belay-audit` | `postToolUse`, `postToolUseFailure`, `stop`, `sessionEnd` | `PostToolUse` | `PostToolUse` |
|
|
48
48
|
|
|
49
49
|
## Why
|
|
50
50
|
|
|
@@ -213,7 +213,41 @@ npx @guilz-dev/belay upgrade # refresh hooks/runtime, migrate con
|
|
|
213
213
|
**Install scope.** `--scope project` (default) writes artifacts under
|
|
214
214
|
`.cursor/` (or `.claude/`, `.codex/`). `--scope global` installs hooks, runtime,
|
|
215
215
|
and skill under `~/.cursor/`, so the gate is user-wide while `belay.config.json`,
|
|
216
|
-
approvals, and audit stay repo-local.
|
|
216
|
+
approvals, and audit stay repo-local. Cursor scope changes stage the complete target owner before
|
|
217
|
+
publishing `installScope`, then remove only exactly recognized artifacts from the previous owner.
|
|
218
|
+
The config file is replaced atomically so a concurrent hook does not observe a truncate-and-rewrite
|
|
219
|
+
window.
|
|
220
|
+
|
|
221
|
+
**Cursor source precedence.** Cursor may launch User/global hooks and hooks from multiple open
|
|
222
|
+
Projects for the same event. Belay chooses one effective source from the payload-derived action
|
|
223
|
+
repository: its matching Project install wins when `installScope` is `project`; User/global wins
|
|
224
|
+
when it is `global`. Other sources return a neutral Cursor response without loading the policy core
|
|
225
|
+
or writing audit/control-plane state. In a multi-root workspace, Shell
|
|
226
|
+
`tool_input.working_directory`, then `cwd`, then `workspace_roots` selects the action repository;
|
|
227
|
+
canonical paths prevent symlink aliases from becoming two owners; Project shims persist the
|
|
228
|
+
canonical repository identity at install time. An omitted `installScope` uses its documented
|
|
229
|
+
`project` default. A truly uninitialized repository is neutral to the global source, while a
|
|
230
|
+
present but malformed, unreadable, or invalid config remains selected by the matching Project
|
|
231
|
+
source and reaches Belay's fail-closed config path. A selected but incomplete Project owner fails
|
|
232
|
+
closed for gates and prompts (audit hooks remain safe and diagnostic).
|
|
233
|
+
|
|
234
|
+
Every managed Cursor hook entry is installed with `failClosed: true`. Cursor can therefore stop an
|
|
235
|
+
actionable prompt, shell, tool, or subagent operation when its runner, shim, or dispatcher cannot
|
|
236
|
+
start, crashes, times out, or returns invalid JSON. Post-action audit events cannot undo an action
|
|
237
|
+
that already completed, and Cursor documents `sessionEnd` as fire-and-forget with its response
|
|
238
|
+
unused; `failClosed` on those entries is defense-in-depth and diagnostics, not rollback. With
|
|
239
|
+
hash-pinned integrity enabled, both Project and global settings, runners, shims, core, and Cursor
|
|
240
|
+
dispatcher are pinned and checked by `belay doctor`. The dispatcher itself contains only payload
|
|
241
|
+
routing and filesystem layout logic; policy and audit modules load only for the selected owner.
|
|
242
|
+
|
|
243
|
+
Run `belay upgrade --scope global` for a pre-router global Cursor install, then run `belay doctor`.
|
|
244
|
+
A Project upgrade also refreshes an exactly recognized managed global install; doctor reports old
|
|
245
|
+
global generations, origin mismatches, incomplete owners, and managed entries that have not gained
|
|
246
|
+
`failClosed: true`, while a healthy global source shadowed by Project precedence is only a note.
|
|
247
|
+
This mechanism resolves competing sources for the same canonical event; it does not combine
|
|
248
|
+
distinct events such as `beforeShellExecution` and `preToolUse: Shell`, and it does not merge
|
|
249
|
+
repeated deliveries to the effective owner. See
|
|
250
|
+
[ADR-008](./docs/adr/ADR-008-cursor-hook-source-precedence.md).
|
|
217
251
|
|
|
218
252
|
**Skill-only.** The skill is just a UX layer (slash commands + guidance) and does
|
|
219
253
|
**not** enable gating on its own. Install from [skills.sh](https://skills.sh/guilz-dev/belay)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export declare const CODEX_HOOKS_BEGIN = "# --- BELAY MANAGED HOOKS BEGIN (managed by belay; do not edit) ---";
|
|
2
2
|
export declare const CODEX_HOOKS_END = "# --- BELAY MANAGED HOOKS END ---";
|
|
3
|
+
export declare function codexHooksTomlIncludesCommand(content: string, command: string): boolean;
|
|
3
4
|
/**
|
|
4
5
|
* Render belay's Codex lifecycle hooks as a marker-delimited TOML block for `.codex/config.toml`.
|
|
5
6
|
* The block is replaced wholesale on re-init/upgrade (see mergeCodexHooksToml), so we avoid a
|
|
@@ -12,6 +12,9 @@ const CODEX_HOOK_SPECS = [
|
|
|
12
12
|
function tomlString(value) {
|
|
13
13
|
return `"${value.replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`;
|
|
14
14
|
}
|
|
15
|
+
export function codexHooksTomlIncludesCommand(content, command) {
|
|
16
|
+
return content.includes(`command = ${tomlString(command)}`);
|
|
17
|
+
}
|
|
15
18
|
function runnerCommand(platform, hooksDir, repoRoot, hookName, ...args) {
|
|
16
19
|
return buildRunnerInvocation(platform, hooksDir, repoRoot, hookName, ...args);
|
|
17
20
|
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
export declare function resolveCodexActionCwd(payload: Record<string, unknown>, fallbackCwd?: string, options?: {
|
|
2
|
+
includeToolInputCwd?: boolean;
|
|
3
|
+
}): string;
|
|
1
4
|
export declare function runBeforeSubmitPromptHook(): Promise<void>;
|
|
2
5
|
export declare function runToolGateHook(eventName: string): Promise<void>;
|
|
3
6
|
export declare function runShellGateHook(): Promise<void>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
1
2
|
import process from 'node:process';
|
|
2
3
|
import { codexLayout } from '../layouts/codex.js';
|
|
3
4
|
import { appendObservedAudit, createDefaultGateRuntimeDeps, evaluateGatedAction, gateUnmappedToolVerdict, gateVerdictToCodexPreToolUseResponse, gateVerdictToCodexUserPromptResponse, processApprovalPrompt, resolveGateConfig, } from '../shared/gate-runtime.js';
|
|
@@ -21,6 +22,22 @@ async function readStdinJson() {
|
|
|
21
22
|
function jsonResponse(value) {
|
|
22
23
|
process.stdout.write(`${JSON.stringify(value)}\n`);
|
|
23
24
|
}
|
|
25
|
+
function nonEmptyPathString(value) {
|
|
26
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
27
|
+
}
|
|
28
|
+
export function resolveCodexActionCwd(payload, fallbackCwd = process.cwd(), options = {}) {
|
|
29
|
+
const toolInput = payload.tool_input !== null && typeof payload.tool_input === 'object'
|
|
30
|
+
? payload.tool_input
|
|
31
|
+
: undefined;
|
|
32
|
+
const nestedCwd = options.includeToolInputCwd
|
|
33
|
+
? (nonEmptyPathString(toolInput?.working_directory) ?? nonEmptyPathString(toolInput?.cwd))
|
|
34
|
+
: undefined;
|
|
35
|
+
const payloadCwd = nonEmptyPathString(payload.cwd);
|
|
36
|
+
const workspaceRoot = Array.isArray(payload.workspace_roots)
|
|
37
|
+
? payload.workspace_roots.map(nonEmptyPathString).find(Boolean)
|
|
38
|
+
: undefined;
|
|
39
|
+
return path.resolve(nestedCwd ?? payloadCwd ?? workspaceRoot ?? fallbackCwd);
|
|
40
|
+
}
|
|
24
41
|
async function loadRuntimeContext(cwd) {
|
|
25
42
|
const repoRoot = findRepoRoot(cwd, codexLayout);
|
|
26
43
|
const configPath = codexLayout.configPath(repoRoot);
|
|
@@ -137,7 +154,8 @@ export async function runBeforeSubmitPromptHook() {
|
|
|
137
154
|
try {
|
|
138
155
|
const payload = await readStdinJson();
|
|
139
156
|
const prompt = String(payload.prompt ?? payload.user_message ?? '');
|
|
140
|
-
const
|
|
157
|
+
const cwd = resolveCodexActionCwd(payload);
|
|
158
|
+
const ctx = await loadRuntimeContext(cwd);
|
|
141
159
|
const deps = createDefaultGateRuntimeDeps();
|
|
142
160
|
const result = await processApprovalPrompt(ctx, deps, prompt);
|
|
143
161
|
jsonResponse(gateVerdictToCodexUserPromptResponse(result));
|
|
@@ -155,9 +173,10 @@ export async function runBeforeSubmitPromptHook() {
|
|
|
155
173
|
export async function runToolGateHook(eventName) {
|
|
156
174
|
try {
|
|
157
175
|
const payload = await readStdinJson();
|
|
158
|
-
const cwd = process.cwd();
|
|
159
176
|
const toolName = String(payload.tool_name ?? payload.toolName ?? '');
|
|
160
177
|
const kind = resolveCodexGateKind(eventName, toolName);
|
|
178
|
+
const includeToolInputCwd = (eventName === 'PreToolUse' || eventName === 'preToolUse') && kind === 'shell';
|
|
179
|
+
const cwd = resolveCodexActionCwd(payload, process.cwd(), { includeToolInputCwd });
|
|
161
180
|
const ctx = await loadRuntimeContext(cwd);
|
|
162
181
|
const deps = createDefaultGateRuntimeDeps();
|
|
163
182
|
if (!kind) {
|
|
@@ -201,7 +220,7 @@ export async function runShellGateHook() {
|
|
|
201
220
|
try {
|
|
202
221
|
const payload = await readStdinJson();
|
|
203
222
|
const command = extractString(payload.tool_input, 'command') || String(payload.command ?? '');
|
|
204
|
-
const cwd = process.cwd();
|
|
223
|
+
const cwd = resolveCodexActionCwd(payload, process.cwd(), { includeToolInputCwd: true });
|
|
205
224
|
const ctx = await loadRuntimeContext(cwd);
|
|
206
225
|
const deps = createDefaultGateRuntimeDeps();
|
|
207
226
|
const verdict = await evaluateGatedAction(ctx, deps, {
|
|
@@ -227,7 +246,11 @@ export async function runShellGateHook() {
|
|
|
227
246
|
export async function runAuditHook(eventName) {
|
|
228
247
|
try {
|
|
229
248
|
const payload = await readStdinJson();
|
|
230
|
-
const
|
|
249
|
+
const toolName = String(payload.tool_name ?? payload.toolName ?? '');
|
|
250
|
+
const kind = resolveCodexGateKind(eventName, toolName);
|
|
251
|
+
const includeToolInputCwd = (eventName === 'PreToolUse' || eventName === 'preToolUse') && kind === 'shell';
|
|
252
|
+
const cwd = resolveCodexActionCwd(payload, process.cwd(), { includeToolInputCwd });
|
|
253
|
+
const ctx = await loadRuntimeContext(cwd);
|
|
231
254
|
const deps = createDefaultGateRuntimeDeps();
|
|
232
255
|
await appendObservedAudit(ctx, deps, eventName, payload);
|
|
233
256
|
jsonResponse({});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type CursorActionCwdSource = 'tool_input.working_directory' | 'cwd' | 'workspace_roots' | 'fallback';
|
|
2
|
+
export interface CursorActionCwdResolution {
|
|
3
|
+
cwd: string;
|
|
4
|
+
source: CursorActionCwdSource;
|
|
5
|
+
fromPayload: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare function resolveCursorActionCwdDetails(payload: Record<string, unknown>, fallback?: string): CursorActionCwdResolution;
|
|
8
|
+
export declare function isGlobalCursorHookRuntime(): boolean;
|
|
9
|
+
export declare function isUnsafeGlobalHookFallback(resolution: CursorActionCwdResolution, globalRuntime?: boolean): boolean;
|
|
10
|
+
export declare const GLOBAL_HOOK_WORKSPACE_MISSING_MESSAGE: string;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
function nonEmptyPathString(value) {
|
|
5
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
6
|
+
}
|
|
7
|
+
export function resolveCursorActionCwdDetails(payload, fallback = process.cwd()) {
|
|
8
|
+
const toolInput = payload.tool_input;
|
|
9
|
+
const toolInputWorkingDirectory = toolInput !== null && typeof toolInput === 'object' && !Array.isArray(toolInput)
|
|
10
|
+
? nonEmptyPathString(toolInput.working_directory)
|
|
11
|
+
: undefined;
|
|
12
|
+
const topLevelCwd = nonEmptyPathString(payload.cwd);
|
|
13
|
+
const workspaceRoot = Array.isArray(payload.workspace_roots)
|
|
14
|
+
? payload.workspace_roots.map(nonEmptyPathString).find(Boolean)
|
|
15
|
+
: undefined;
|
|
16
|
+
if (toolInputWorkingDirectory) {
|
|
17
|
+
return {
|
|
18
|
+
cwd: path.resolve(toolInputWorkingDirectory),
|
|
19
|
+
source: 'tool_input.working_directory',
|
|
20
|
+
fromPayload: true,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
if (topLevelCwd) {
|
|
24
|
+
return {
|
|
25
|
+
cwd: path.resolve(topLevelCwd),
|
|
26
|
+
source: 'cwd',
|
|
27
|
+
fromPayload: true,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
if (workspaceRoot) {
|
|
31
|
+
return {
|
|
32
|
+
cwd: path.resolve(workspaceRoot),
|
|
33
|
+
source: 'workspace_roots',
|
|
34
|
+
fromPayload: true,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
cwd: path.resolve(fallback),
|
|
39
|
+
source: 'fallback',
|
|
40
|
+
fromPayload: false,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function isGlobalCursorHookRuntime() {
|
|
44
|
+
try {
|
|
45
|
+
const runtimePath = path.resolve(fileURLToPath(import.meta.url));
|
|
46
|
+
const globalRuntimeDir = path.resolve(path.join(os.homedir(), '.cursor', 'belay', 'runtime'));
|
|
47
|
+
return (runtimePath === globalRuntimeDir || runtimePath.startsWith(`${globalRuntimeDir}${path.sep}`));
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export function isUnsafeGlobalHookFallback(resolution, globalRuntime = isGlobalCursorHookRuntime()) {
|
|
54
|
+
return !resolution.fromPayload && globalRuntime;
|
|
55
|
+
}
|
|
56
|
+
export const GLOBAL_HOOK_WORKSPACE_MISSING_MESSAGE = 'belay could not determine the workspace for this action from the Cursor hook payload. ' +
|
|
57
|
+
'Open the target project workspace and retry, or run belay doctor. ' +
|
|
58
|
+
'To stop global hooks: belay uninstall --scope global';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export const CURSOR_DISPATCHER_GENERATION = 'cursor-owner-router-v1';
|
|
2
|
+
export const CURSOR_DISPATCHER_GENERATION_HEADER = `export const BELAY_CURSOR_DISPATCHER_GENERATION = ${JSON.stringify(CURSOR_DISPATCHER_GENERATION)};\n`;
|
|
3
|
+
export function hasCurrentCursorDispatcherGeneration(source) {
|
|
4
|
+
return source.startsWith(CURSOR_DISPATCHER_GENERATION_HEADER);
|
|
5
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type CursorHookKind, type CursorHookOrigin } from './hook-router.js';
|
|
2
|
+
export interface DispatchCursorHookParams {
|
|
3
|
+
origin: CursorHookOrigin;
|
|
4
|
+
kind: CursorHookKind;
|
|
5
|
+
eventName: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function dispatchCursorHook(params: DispatchCursorHookParams): Promise<void>;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
2
|
+
import { routeCursorHook } from './hook-router.js';
|
|
3
|
+
async function readStdinPayload() {
|
|
4
|
+
const chunks = [];
|
|
5
|
+
for await (const chunk of process.stdin) {
|
|
6
|
+
chunks.push(typeof chunk === 'string' ? chunk : chunk.toString('utf8'));
|
|
7
|
+
}
|
|
8
|
+
const raw = chunks.join('').trim();
|
|
9
|
+
if (!raw) {
|
|
10
|
+
return { ok: false };
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
const parsed = JSON.parse(raw);
|
|
14
|
+
return parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
15
|
+
? { ok: true, payload: parsed }
|
|
16
|
+
: { ok: false };
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return { ok: false };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function neutralResponse(kind) {
|
|
23
|
+
if (kind === 'before-submit') {
|
|
24
|
+
return { continue: true };
|
|
25
|
+
}
|
|
26
|
+
if (kind === 'audit') {
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
29
|
+
return { permission: 'allow' };
|
|
30
|
+
}
|
|
31
|
+
function failClosedResponse(kind, message) {
|
|
32
|
+
if (kind === 'audit') {
|
|
33
|
+
console.error(`belay audit hook skipped: ${message}`);
|
|
34
|
+
return {};
|
|
35
|
+
}
|
|
36
|
+
if (kind === 'before-submit') {
|
|
37
|
+
return { continue: false, user_message: message };
|
|
38
|
+
}
|
|
39
|
+
return { permission: 'deny', user_message: message };
|
|
40
|
+
}
|
|
41
|
+
function isRecord(value) {
|
|
42
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
43
|
+
}
|
|
44
|
+
function isNonEmptyString(value) {
|
|
45
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
46
|
+
}
|
|
47
|
+
function isRecognizedPayload(params, payload) {
|
|
48
|
+
if (params.kind === 'before-submit') {
|
|
49
|
+
return params.eventName === 'beforeSubmitPrompt' && typeof payload.prompt === 'string';
|
|
50
|
+
}
|
|
51
|
+
if (params.kind === 'shell-gate') {
|
|
52
|
+
return params.eventName === 'beforeShellExecution' && typeof payload.command === 'string';
|
|
53
|
+
}
|
|
54
|
+
if (params.kind === 'tool-gate') {
|
|
55
|
+
if (params.eventName === 'preToolUse' || params.eventName === 'PreToolUse') {
|
|
56
|
+
return isNonEmptyString(payload.tool_name) && isRecord(payload.tool_input);
|
|
57
|
+
}
|
|
58
|
+
return (params.eventName === 'subagentStart' &&
|
|
59
|
+
isNonEmptyString(payload.subagent_type) &&
|
|
60
|
+
typeof payload.task === 'string');
|
|
61
|
+
}
|
|
62
|
+
if (params.eventName === 'postToolUse') {
|
|
63
|
+
return (isNonEmptyString(payload.tool_name) &&
|
|
64
|
+
isRecord(payload.tool_input) &&
|
|
65
|
+
typeof payload.tool_output === 'string');
|
|
66
|
+
}
|
|
67
|
+
if (params.eventName === 'postToolUseFailure') {
|
|
68
|
+
return (isNonEmptyString(payload.tool_name) &&
|
|
69
|
+
isRecord(payload.tool_input) &&
|
|
70
|
+
typeof payload.error_message === 'string' &&
|
|
71
|
+
typeof payload.failure_type === 'string');
|
|
72
|
+
}
|
|
73
|
+
if (params.eventName === 'stop') {
|
|
74
|
+
return typeof payload.status === 'string' && typeof payload.loop_count === 'number';
|
|
75
|
+
}
|
|
76
|
+
if (params.eventName === 'sessionEnd') {
|
|
77
|
+
return isNonEmptyString(payload.session_id) && typeof payload.reason === 'string';
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
async function executeCoreHandler(params, payload) {
|
|
82
|
+
const coreModulePath = './core.mjs';
|
|
83
|
+
const core = (await import(coreModulePath));
|
|
84
|
+
if (params.kind === 'before-submit') {
|
|
85
|
+
return core.handleBeforeSubmitPromptHook(payload);
|
|
86
|
+
}
|
|
87
|
+
if (params.kind === 'shell-gate') {
|
|
88
|
+
return core.handleShellGateHook(payload);
|
|
89
|
+
}
|
|
90
|
+
if (params.kind === 'tool-gate') {
|
|
91
|
+
return core.handleToolGateHook(params.eventName, payload);
|
|
92
|
+
}
|
|
93
|
+
return core.handleAuditHook(params.eventName, payload);
|
|
94
|
+
}
|
|
95
|
+
async function dispatchCursorHookResponse(params) {
|
|
96
|
+
const input = await readStdinPayload();
|
|
97
|
+
if (!input.ok) {
|
|
98
|
+
return failClosedResponse(params.kind, 'belay received malformed Cursor hook input.');
|
|
99
|
+
}
|
|
100
|
+
if (!isRecognizedPayload(params, input.payload)) {
|
|
101
|
+
return neutralResponse(params.kind);
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
const route = routeCursorHook({
|
|
105
|
+
origin: params.origin,
|
|
106
|
+
kind: params.kind,
|
|
107
|
+
eventName: params.eventName,
|
|
108
|
+
payload: input.payload,
|
|
109
|
+
});
|
|
110
|
+
if (route.decision === 'neutral') {
|
|
111
|
+
return neutralResponse(params.kind);
|
|
112
|
+
}
|
|
113
|
+
if (route.decision === 'fail_closed') {
|
|
114
|
+
return failClosedResponse(params.kind, route.message);
|
|
115
|
+
}
|
|
116
|
+
return await executeCoreHandler(params, input.payload);
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
120
|
+
return failClosedResponse(params.kind, `belay failed while dispatching this Cursor hook: ${detail}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
export async function dispatchCursorHook(params) {
|
|
124
|
+
const response = await dispatchCursorHookResponse(params);
|
|
125
|
+
process.stdout.write(`${JSON.stringify(response)}\n`);
|
|
126
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type CursorHookOrigin = {
|
|
2
|
+
scope: 'project';
|
|
3
|
+
repoRoot: string;
|
|
4
|
+
} | {
|
|
5
|
+
scope: 'global';
|
|
6
|
+
};
|
|
7
|
+
export type CursorHookKind = 'before-submit' | 'shell-gate' | 'tool-gate' | 'audit';
|
|
8
|
+
export type CursorHookRoute = {
|
|
9
|
+
decision: 'execute';
|
|
10
|
+
repoRoot: string;
|
|
11
|
+
} | {
|
|
12
|
+
decision: 'neutral';
|
|
13
|
+
} | {
|
|
14
|
+
decision: 'fail_closed';
|
|
15
|
+
message: string;
|
|
16
|
+
};
|
|
17
|
+
export interface RouteCursorHookParams {
|
|
18
|
+
origin: CursorHookOrigin;
|
|
19
|
+
kind: CursorHookKind;
|
|
20
|
+
eventName?: string;
|
|
21
|
+
payload: Record<string, unknown>;
|
|
22
|
+
}
|
|
23
|
+
export declare function routeCursorHook(params: RouteCursorHookParams): CursorHookRoute;
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { accessSync, constants, existsSync, readFileSync, realpathSync, statSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { resolveCursorActionCwdDetails } from './cwd-resolution.js';
|
|
4
|
+
import { cursorRoutingConfigPath, cursorRoutingHooksDir, cursorRoutingHooksSettingsPath, cursorRoutingRuntimeDir, findCursorRoutingRepoRoot, } from './routing-layout.js';
|
|
5
|
+
function canonicalExistingPath(value) {
|
|
6
|
+
if (!existsSync(value)) {
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
try {
|
|
10
|
+
return realpathSync(value);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function hookScriptFor(kind) {
|
|
17
|
+
if (kind === 'before-submit') {
|
|
18
|
+
return 'belay-before-submit.mjs';
|
|
19
|
+
}
|
|
20
|
+
if (kind === 'shell-gate') {
|
|
21
|
+
return 'belay-shell-gate.mjs';
|
|
22
|
+
}
|
|
23
|
+
if (kind === 'tool-gate') {
|
|
24
|
+
return 'belay-tool-gate.mjs';
|
|
25
|
+
}
|
|
26
|
+
return 'belay-audit.mjs';
|
|
27
|
+
}
|
|
28
|
+
function hookCommandFor(kind) {
|
|
29
|
+
return hookScriptFor(kind).replace(/\.mjs$/, '');
|
|
30
|
+
}
|
|
31
|
+
function eventNameFor(params) {
|
|
32
|
+
if (params.eventName) {
|
|
33
|
+
return params.eventName;
|
|
34
|
+
}
|
|
35
|
+
if (params.kind === 'before-submit') {
|
|
36
|
+
return 'beforeSubmitPrompt';
|
|
37
|
+
}
|
|
38
|
+
if (params.kind === 'shell-gate') {
|
|
39
|
+
return 'beforeShellExecution';
|
|
40
|
+
}
|
|
41
|
+
if (params.kind === 'tool-gate') {
|
|
42
|
+
return typeof params.payload.subagent_type === 'string' ? 'subagentStart' : 'preToolUse';
|
|
43
|
+
}
|
|
44
|
+
if (typeof params.payload.error_message === 'string') {
|
|
45
|
+
return 'postToolUseFailure';
|
|
46
|
+
}
|
|
47
|
+
if (typeof params.payload.status === 'string') {
|
|
48
|
+
return 'stop';
|
|
49
|
+
}
|
|
50
|
+
if (typeof params.payload.session_id === 'string') {
|
|
51
|
+
return 'sessionEnd';
|
|
52
|
+
}
|
|
53
|
+
return 'postToolUse';
|
|
54
|
+
}
|
|
55
|
+
function selectedRunnerPath(hooksDir) {
|
|
56
|
+
const runnerPath = path.join(hooksDir, process.platform === 'win32' ? 'belay-runner.ps1' : 'belay-runner');
|
|
57
|
+
const canonicalPath = canonicalExistingPath(runnerPath);
|
|
58
|
+
if (!canonicalPath) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
if (!statSync(canonicalPath).isFile()) {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
accessSync(canonicalPath, process.platform === 'win32' ? constants.R_OK : constants.X_OK);
|
|
66
|
+
return canonicalPath;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function isRegularFile(filePath) {
|
|
73
|
+
const canonicalPath = canonicalExistingPath(filePath);
|
|
74
|
+
if (!canonicalPath) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
return statSync(canonicalPath).isFile();
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function expectedHookArgs(kind, eventName) {
|
|
85
|
+
return kind === 'tool-gate' || kind === 'audit' ? [eventName] : [];
|
|
86
|
+
}
|
|
87
|
+
function commandInvokesProjectHook(command, runnerPath, kind, eventName) {
|
|
88
|
+
const hookCommand = hookCommandFor(kind);
|
|
89
|
+
const args = expectedHookArgs(kind, eventName);
|
|
90
|
+
if (process.platform !== 'win32') {
|
|
91
|
+
const expected = [`'${runnerPath.replaceAll("'", "'\\''")}'`, hookCommand, ...args].join(' ');
|
|
92
|
+
return command === expected;
|
|
93
|
+
}
|
|
94
|
+
const encoded = command.match(/^"[^"\r\n]+" -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand ([A-Za-z0-9+/=]+)$/)?.[1];
|
|
95
|
+
if (!encoded) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
const quotePowerShellLiteral = (value) => `'${value.replaceAll("'", "''")}'`;
|
|
99
|
+
const expected = [
|
|
100
|
+
'&',
|
|
101
|
+
quotePowerShellLiteral(runnerPath),
|
|
102
|
+
...[hookCommand, ...args].map(quotePowerShellLiteral),
|
|
103
|
+
].join(' ');
|
|
104
|
+
return Buffer.from(encoded, 'base64').toString('utf16le') === expected;
|
|
105
|
+
}
|
|
106
|
+
function expectedMatcher(eventName, payload) {
|
|
107
|
+
if (eventName === 'preToolUse') {
|
|
108
|
+
return typeof payload.tool_name === 'string' ? payload.tool_name : undefined;
|
|
109
|
+
}
|
|
110
|
+
if (eventName === 'subagentStart') {
|
|
111
|
+
return typeof payload.subagent_type === 'string' ? payload.subagent_type : undefined;
|
|
112
|
+
}
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
function hasManagedProjectHookEntry(repoRoot, runnerPath, params) {
|
|
116
|
+
const eventName = eventNameFor(params);
|
|
117
|
+
const matcher = expectedMatcher(eventName, params.payload);
|
|
118
|
+
let parsed;
|
|
119
|
+
try {
|
|
120
|
+
parsed = JSON.parse(readFileSync(cursorRoutingHooksSettingsPath(repoRoot), 'utf8'));
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
const hooks = parsed.hooks;
|
|
129
|
+
if (hooks === null || typeof hooks !== 'object' || Array.isArray(hooks)) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
const entries = hooks[eventName];
|
|
133
|
+
if (!Array.isArray(entries)) {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
return entries.some((entry) => {
|
|
137
|
+
if (entry === null || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
const definition = entry;
|
|
141
|
+
return (definition.failClosed === true &&
|
|
142
|
+
definition.matcher === matcher &&
|
|
143
|
+
typeof definition.command === 'string' &&
|
|
144
|
+
commandInvokesProjectHook(definition.command, runnerPath, params.kind, eventName));
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
function hasMatchingProjectShim(repoRoot, kind) {
|
|
148
|
+
let source;
|
|
149
|
+
try {
|
|
150
|
+
source = readFileSync(path.join(cursorRoutingHooksDir(repoRoot), hookScriptFor(kind)), 'utf8');
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
return (source.includes("from '../belay/runtime/dispatcher.mjs'") &&
|
|
156
|
+
source.includes(`origin: ${JSON.stringify({ scope: 'project', repoRoot })}`));
|
|
157
|
+
}
|
|
158
|
+
function hasCallableProjectOwner(repoRoot, params) {
|
|
159
|
+
const hooksDir = cursorRoutingHooksDir(repoRoot);
|
|
160
|
+
const runnerPath = selectedRunnerPath(hooksDir);
|
|
161
|
+
return Boolean(runnerPath &&
|
|
162
|
+
isRegularFile(path.join(cursorRoutingRuntimeDir(repoRoot), 'dispatcher.mjs')) &&
|
|
163
|
+
hasMatchingProjectShim(repoRoot, params.kind) &&
|
|
164
|
+
hasManagedProjectHookEntry(repoRoot, runnerPath, params));
|
|
165
|
+
}
|
|
166
|
+
function payloadForKind(payload, kind) {
|
|
167
|
+
if (kind === 'before-submit' || kind === 'shell-gate') {
|
|
168
|
+
return payload;
|
|
169
|
+
}
|
|
170
|
+
if (kind === 'tool-gate' && payload.tool_name === 'Shell') {
|
|
171
|
+
return payload;
|
|
172
|
+
}
|
|
173
|
+
const toolInput = payload.tool_input;
|
|
174
|
+
if (toolInput === null || typeof toolInput !== 'object' || Array.isArray(toolInput)) {
|
|
175
|
+
return payload;
|
|
176
|
+
}
|
|
177
|
+
const { working_directory: _workingDirectory, ...withoutWorkingDirectory } = toolInput;
|
|
178
|
+
return { ...payload, tool_input: withoutWorkingDirectory };
|
|
179
|
+
}
|
|
180
|
+
function selectedActionPath(payload) {
|
|
181
|
+
const toolInput = payload.tool_input;
|
|
182
|
+
if (toolInput !== null && typeof toolInput === 'object' && !Array.isArray(toolInput)) {
|
|
183
|
+
const workingDirectory = toolInput.working_directory;
|
|
184
|
+
if (typeof workingDirectory === 'string' && workingDirectory.trim()) {
|
|
185
|
+
return workingDirectory.trim();
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (typeof payload.cwd === 'string' && payload.cwd.trim()) {
|
|
189
|
+
return payload.cwd.trim();
|
|
190
|
+
}
|
|
191
|
+
if (!Array.isArray(payload.workspace_roots)) {
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
for (const root of payload.workspace_roots) {
|
|
195
|
+
if (typeof root === 'string' && root.trim()) {
|
|
196
|
+
return root.trim();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return undefined;
|
|
200
|
+
}
|
|
201
|
+
function readInstallScope(repoRoot) {
|
|
202
|
+
const configPath = cursorRoutingConfigPath(repoRoot);
|
|
203
|
+
let source;
|
|
204
|
+
try {
|
|
205
|
+
source = readFileSync(configPath, 'utf8');
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
return error.code === 'ENOENT' ? 'missing' : 'project';
|
|
209
|
+
}
|
|
210
|
+
try {
|
|
211
|
+
const parsed = JSON.parse(source);
|
|
212
|
+
if (parsed !== null &&
|
|
213
|
+
typeof parsed === 'object' &&
|
|
214
|
+
!Array.isArray(parsed) &&
|
|
215
|
+
parsed.installScope === 'global') {
|
|
216
|
+
return 'global';
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
// A present config that cannot be read or parsed must retain Project ownership so the
|
|
221
|
+
// selected Project hook reaches its fail-closed core path. Only a missing config is neutral.
|
|
222
|
+
}
|
|
223
|
+
return 'project';
|
|
224
|
+
}
|
|
225
|
+
export function routeCursorHook(params) {
|
|
226
|
+
const relevantPayload = payloadForKind(params.payload, params.kind);
|
|
227
|
+
const selectedPath = selectedActionPath(relevantPayload);
|
|
228
|
+
if (!selectedPath || !path.isAbsolute(selectedPath)) {
|
|
229
|
+
return { decision: 'fail_closed', message: 'belay could not determine the workspace.' };
|
|
230
|
+
}
|
|
231
|
+
const resolution = resolveCursorActionCwdDetails(relevantPayload, '/');
|
|
232
|
+
const canonicalCwd = resolution.fromPayload ? canonicalExistingPath(resolution.cwd) : undefined;
|
|
233
|
+
if (!canonicalCwd) {
|
|
234
|
+
return { decision: 'fail_closed', message: 'belay could not determine the workspace.' };
|
|
235
|
+
}
|
|
236
|
+
const repoRoot = canonicalExistingPath(findCursorRoutingRepoRoot(canonicalCwd));
|
|
237
|
+
if (!repoRoot) {
|
|
238
|
+
return { decision: 'neutral' };
|
|
239
|
+
}
|
|
240
|
+
const installScope = readInstallScope(repoRoot);
|
|
241
|
+
if (installScope === 'missing') {
|
|
242
|
+
return { decision: 'neutral' };
|
|
243
|
+
}
|
|
244
|
+
if (installScope === 'global') {
|
|
245
|
+
return params.origin.scope === 'global'
|
|
246
|
+
? { decision: 'execute', repoRoot }
|
|
247
|
+
: { decision: 'neutral' };
|
|
248
|
+
}
|
|
249
|
+
if (params.origin.scope === 'project' &&
|
|
250
|
+
canonicalExistingPath(params.origin.repoRoot) !== repoRoot) {
|
|
251
|
+
return { decision: 'neutral' };
|
|
252
|
+
}
|
|
253
|
+
const callableProjectOwner = hasCallableProjectOwner(repoRoot, params);
|
|
254
|
+
if (params.origin.scope === 'global') {
|
|
255
|
+
return callableProjectOwner
|
|
256
|
+
? { decision: 'neutral' }
|
|
257
|
+
: {
|
|
258
|
+
decision: 'fail_closed',
|
|
259
|
+
message: 'belay project hook owner is unavailable; the global sentinel blocked this action.',
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
const complete = callableProjectOwner && isRegularFile(path.join(cursorRoutingRuntimeDir(repoRoot), 'core.mjs'));
|
|
263
|
+
return complete
|
|
264
|
+
? { decision: 'execute', repoRoot }
|
|
265
|
+
: { decision: 'fail_closed', message: 'belay project hook installation is incomplete.' };
|
|
266
|
+
}
|
|
@@ -3,5 +3,10 @@ import type { HookEntry, HooksFile } from '../../types.js';
|
|
|
3
3
|
export declare function managedShellPreToolUseEntry(platform: NodeJS.Platform, hooksDir: string, repoRoot: string): HookEntry;
|
|
4
4
|
/** @deprecated Use {@link managedShellPreToolUseEntry}. */
|
|
5
5
|
export declare const legacyManagedShellPreToolUseEntry: typeof managedShellPreToolUseEntry;
|
|
6
|
+
export declare function stripCursorHooksFile(current: HooksFile, platform: NodeJS.Platform, hooksDir: string, repoRoot: string): HooksFile;
|
|
6
7
|
export declare function mergeCursorHooksFile(current: HooksFile, platform: NodeJS.Platform, hooksDir: string, repoRoot: string): HooksFile;
|
|
8
|
+
export declare function legacyManagedShellPreToolUseVariants(platform: NodeJS.Platform, hooksDir: string, repoRoot: string): HookEntry[];
|
|
9
|
+
export declare function hasLegacyCursorDoubleShellGates(hooks: HooksFile, platform: NodeJS.Platform, hooksDir: string, repoRoot: string): boolean;
|
|
10
|
+
/** @deprecated Use {@link hasLegacyCursorDoubleShellGates}. */
|
|
7
11
|
export declare function hasDuplicateCursorShellGates(hooks: HooksFile, platform: NodeJS.Platform, hooksDir: string, repoRoot: string): boolean;
|
|
12
|
+
export declare function hasManagedCursorHookEntries(hooks: HooksFile, platform: NodeJS.Platform, hooksDir: string, repoRoot: string): boolean;
|