@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
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
// agent-belay cursor hook dispatcher bundle
|
|
2
|
+
|
|
3
|
+
// src/adapters/cursor/hook-dispatch-entry.ts
|
|
4
|
+
import process2 from "node:process";
|
|
5
|
+
|
|
6
|
+
// src/adapters/cursor/hook-router.ts
|
|
7
|
+
import { accessSync, constants, existsSync as existsSync2, readFileSync, realpathSync, statSync } from "node:fs";
|
|
8
|
+
import path3 from "node:path";
|
|
9
|
+
|
|
10
|
+
// src/adapters/cursor/cwd-resolution.ts
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
function nonEmptyPathString(value) {
|
|
13
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
14
|
+
}
|
|
15
|
+
function resolveCursorActionCwdDetails(payload, fallback = process.cwd()) {
|
|
16
|
+
const toolInput = payload.tool_input;
|
|
17
|
+
const toolInputWorkingDirectory = toolInput !== null && typeof toolInput === "object" && !Array.isArray(toolInput) ? nonEmptyPathString(toolInput.working_directory) : void 0;
|
|
18
|
+
const topLevelCwd = nonEmptyPathString(payload.cwd);
|
|
19
|
+
const workspaceRoot = Array.isArray(payload.workspace_roots) ? payload.workspace_roots.map(nonEmptyPathString).find(Boolean) : void 0;
|
|
20
|
+
if (toolInputWorkingDirectory) {
|
|
21
|
+
return {
|
|
22
|
+
cwd: path.resolve(toolInputWorkingDirectory),
|
|
23
|
+
source: "tool_input.working_directory",
|
|
24
|
+
fromPayload: true
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
if (topLevelCwd) {
|
|
28
|
+
return {
|
|
29
|
+
cwd: path.resolve(topLevelCwd),
|
|
30
|
+
source: "cwd",
|
|
31
|
+
fromPayload: true
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
if (workspaceRoot) {
|
|
35
|
+
return {
|
|
36
|
+
cwd: path.resolve(workspaceRoot),
|
|
37
|
+
source: "workspace_roots",
|
|
38
|
+
fromPayload: true
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
cwd: path.resolve(fallback),
|
|
43
|
+
source: "fallback",
|
|
44
|
+
fromPayload: false
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/adapters/cursor/routing-layout.ts
|
|
49
|
+
import { existsSync } from "node:fs";
|
|
50
|
+
import path2 from "node:path";
|
|
51
|
+
function cursorRoutingConfigPath(repoRoot) {
|
|
52
|
+
return path2.join(repoRoot, ".cursor", "belay.config.json");
|
|
53
|
+
}
|
|
54
|
+
function cursorRoutingHooksDir(repoRoot) {
|
|
55
|
+
return path2.join(repoRoot, ".cursor", "hooks");
|
|
56
|
+
}
|
|
57
|
+
function cursorRoutingHooksSettingsPath(repoRoot) {
|
|
58
|
+
return path2.join(repoRoot, ".cursor", "hooks.json");
|
|
59
|
+
}
|
|
60
|
+
function cursorRoutingRuntimeDir(repoRoot) {
|
|
61
|
+
return path2.join(repoRoot, ".cursor", "belay", "runtime");
|
|
62
|
+
}
|
|
63
|
+
function findCursorRoutingRepoRoot(startPath) {
|
|
64
|
+
const resolvedStart = path2.resolve(startPath);
|
|
65
|
+
let current = resolvedStart;
|
|
66
|
+
while (true) {
|
|
67
|
+
if (existsSync(path2.join(current, ".git")) || existsSync(path2.join(current, ".cursor")) && existsSync(cursorRoutingConfigPath(current))) {
|
|
68
|
+
return current;
|
|
69
|
+
}
|
|
70
|
+
const parent = path2.dirname(current);
|
|
71
|
+
if (parent === current) {
|
|
72
|
+
return resolvedStart;
|
|
73
|
+
}
|
|
74
|
+
current = parent;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/adapters/cursor/hook-router.ts
|
|
79
|
+
function canonicalExistingPath(value) {
|
|
80
|
+
if (!existsSync2(value)) {
|
|
81
|
+
return void 0;
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
return realpathSync(value);
|
|
85
|
+
} catch {
|
|
86
|
+
return void 0;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function hookScriptFor(kind) {
|
|
90
|
+
if (kind === "before-submit") {
|
|
91
|
+
return "belay-before-submit.mjs";
|
|
92
|
+
}
|
|
93
|
+
if (kind === "shell-gate") {
|
|
94
|
+
return "belay-shell-gate.mjs";
|
|
95
|
+
}
|
|
96
|
+
if (kind === "tool-gate") {
|
|
97
|
+
return "belay-tool-gate.mjs";
|
|
98
|
+
}
|
|
99
|
+
return "belay-audit.mjs";
|
|
100
|
+
}
|
|
101
|
+
function hookCommandFor(kind) {
|
|
102
|
+
return hookScriptFor(kind).replace(/\.mjs$/, "");
|
|
103
|
+
}
|
|
104
|
+
function eventNameFor(params) {
|
|
105
|
+
if (params.eventName) {
|
|
106
|
+
return params.eventName;
|
|
107
|
+
}
|
|
108
|
+
if (params.kind === "before-submit") {
|
|
109
|
+
return "beforeSubmitPrompt";
|
|
110
|
+
}
|
|
111
|
+
if (params.kind === "shell-gate") {
|
|
112
|
+
return "beforeShellExecution";
|
|
113
|
+
}
|
|
114
|
+
if (params.kind === "tool-gate") {
|
|
115
|
+
return typeof params.payload.subagent_type === "string" ? "subagentStart" : "preToolUse";
|
|
116
|
+
}
|
|
117
|
+
if (typeof params.payload.error_message === "string") {
|
|
118
|
+
return "postToolUseFailure";
|
|
119
|
+
}
|
|
120
|
+
if (typeof params.payload.status === "string") {
|
|
121
|
+
return "stop";
|
|
122
|
+
}
|
|
123
|
+
if (typeof params.payload.session_id === "string") {
|
|
124
|
+
return "sessionEnd";
|
|
125
|
+
}
|
|
126
|
+
return "postToolUse";
|
|
127
|
+
}
|
|
128
|
+
function selectedRunnerPath(hooksDir) {
|
|
129
|
+
const runnerPath = path3.join(
|
|
130
|
+
hooksDir,
|
|
131
|
+
process.platform === "win32" ? "belay-runner.ps1" : "belay-runner"
|
|
132
|
+
);
|
|
133
|
+
const canonicalPath = canonicalExistingPath(runnerPath);
|
|
134
|
+
if (!canonicalPath) {
|
|
135
|
+
return void 0;
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
if (!statSync(canonicalPath).isFile()) {
|
|
139
|
+
return void 0;
|
|
140
|
+
}
|
|
141
|
+
accessSync(canonicalPath, process.platform === "win32" ? constants.R_OK : constants.X_OK);
|
|
142
|
+
return canonicalPath;
|
|
143
|
+
} catch {
|
|
144
|
+
return void 0;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function isRegularFile(filePath) {
|
|
148
|
+
const canonicalPath = canonicalExistingPath(filePath);
|
|
149
|
+
if (!canonicalPath) {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
return statSync(canonicalPath).isFile();
|
|
154
|
+
} catch {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function expectedHookArgs(kind, eventName) {
|
|
159
|
+
return kind === "tool-gate" || kind === "audit" ? [eventName] : [];
|
|
160
|
+
}
|
|
161
|
+
function commandInvokesProjectHook(command, runnerPath, kind, eventName) {
|
|
162
|
+
const hookCommand = hookCommandFor(kind);
|
|
163
|
+
const args = expectedHookArgs(kind, eventName);
|
|
164
|
+
if (process.platform !== "win32") {
|
|
165
|
+
const expected2 = [`'${runnerPath.replaceAll("'", "'\\''")}'`, hookCommand, ...args].join(" ");
|
|
166
|
+
return command === expected2;
|
|
167
|
+
}
|
|
168
|
+
const encoded = command.match(
|
|
169
|
+
/^"[^"\r\n]+" -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand ([A-Za-z0-9+/=]+)$/
|
|
170
|
+
)?.[1];
|
|
171
|
+
if (!encoded) {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
const quotePowerShellLiteral = (value) => `'${value.replaceAll("'", "''")}'`;
|
|
175
|
+
const expected = [
|
|
176
|
+
"&",
|
|
177
|
+
quotePowerShellLiteral(runnerPath),
|
|
178
|
+
...[hookCommand, ...args].map(quotePowerShellLiteral)
|
|
179
|
+
].join(" ");
|
|
180
|
+
return Buffer.from(encoded, "base64").toString("utf16le") === expected;
|
|
181
|
+
}
|
|
182
|
+
function expectedMatcher(eventName, payload) {
|
|
183
|
+
if (eventName === "preToolUse") {
|
|
184
|
+
return typeof payload.tool_name === "string" ? payload.tool_name : void 0;
|
|
185
|
+
}
|
|
186
|
+
if (eventName === "subagentStart") {
|
|
187
|
+
return typeof payload.subagent_type === "string" ? payload.subagent_type : void 0;
|
|
188
|
+
}
|
|
189
|
+
return void 0;
|
|
190
|
+
}
|
|
191
|
+
function hasManagedProjectHookEntry(repoRoot, runnerPath, params) {
|
|
192
|
+
const eventName = eventNameFor(params);
|
|
193
|
+
const matcher = expectedMatcher(eventName, params.payload);
|
|
194
|
+
let parsed;
|
|
195
|
+
try {
|
|
196
|
+
parsed = JSON.parse(readFileSync(cursorRoutingHooksSettingsPath(repoRoot), "utf8"));
|
|
197
|
+
} catch {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
const hooks = parsed.hooks;
|
|
204
|
+
if (hooks === null || typeof hooks !== "object" || Array.isArray(hooks)) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
const entries = hooks[eventName];
|
|
208
|
+
if (!Array.isArray(entries)) {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
return entries.some((entry) => {
|
|
212
|
+
if (entry === null || typeof entry !== "object" || Array.isArray(entry)) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
const definition = entry;
|
|
216
|
+
return definition.failClosed === true && definition.matcher === matcher && typeof definition.command === "string" && commandInvokesProjectHook(definition.command, runnerPath, params.kind, eventName);
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
function hasMatchingProjectShim(repoRoot, kind) {
|
|
220
|
+
let source;
|
|
221
|
+
try {
|
|
222
|
+
source = readFileSync(path3.join(cursorRoutingHooksDir(repoRoot), hookScriptFor(kind)), "utf8");
|
|
223
|
+
} catch {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
return source.includes("from '../belay/runtime/dispatcher.mjs'") && source.includes(`origin: ${JSON.stringify({ scope: "project", repoRoot })}`);
|
|
227
|
+
}
|
|
228
|
+
function hasCallableProjectOwner(repoRoot, params) {
|
|
229
|
+
const hooksDir = cursorRoutingHooksDir(repoRoot);
|
|
230
|
+
const runnerPath = selectedRunnerPath(hooksDir);
|
|
231
|
+
return Boolean(
|
|
232
|
+
runnerPath && isRegularFile(path3.join(cursorRoutingRuntimeDir(repoRoot), "dispatcher.mjs")) && hasMatchingProjectShim(repoRoot, params.kind) && hasManagedProjectHookEntry(repoRoot, runnerPath, params)
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
function payloadForKind(payload, kind) {
|
|
236
|
+
if (kind === "before-submit" || kind === "shell-gate") {
|
|
237
|
+
return payload;
|
|
238
|
+
}
|
|
239
|
+
if (kind === "tool-gate" && payload.tool_name === "Shell") {
|
|
240
|
+
return payload;
|
|
241
|
+
}
|
|
242
|
+
const toolInput = payload.tool_input;
|
|
243
|
+
if (toolInput === null || typeof toolInput !== "object" || Array.isArray(toolInput)) {
|
|
244
|
+
return payload;
|
|
245
|
+
}
|
|
246
|
+
const { working_directory: _workingDirectory, ...withoutWorkingDirectory } = toolInput;
|
|
247
|
+
return { ...payload, tool_input: withoutWorkingDirectory };
|
|
248
|
+
}
|
|
249
|
+
function selectedActionPath(payload) {
|
|
250
|
+
const toolInput = payload.tool_input;
|
|
251
|
+
if (toolInput !== null && typeof toolInput === "object" && !Array.isArray(toolInput)) {
|
|
252
|
+
const workingDirectory = toolInput.working_directory;
|
|
253
|
+
if (typeof workingDirectory === "string" && workingDirectory.trim()) {
|
|
254
|
+
return workingDirectory.trim();
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (typeof payload.cwd === "string" && payload.cwd.trim()) {
|
|
258
|
+
return payload.cwd.trim();
|
|
259
|
+
}
|
|
260
|
+
if (!Array.isArray(payload.workspace_roots)) {
|
|
261
|
+
return void 0;
|
|
262
|
+
}
|
|
263
|
+
for (const root of payload.workspace_roots) {
|
|
264
|
+
if (typeof root === "string" && root.trim()) {
|
|
265
|
+
return root.trim();
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return void 0;
|
|
269
|
+
}
|
|
270
|
+
function readInstallScope(repoRoot) {
|
|
271
|
+
const configPath = cursorRoutingConfigPath(repoRoot);
|
|
272
|
+
let source;
|
|
273
|
+
try {
|
|
274
|
+
source = readFileSync(configPath, "utf8");
|
|
275
|
+
} catch (error) {
|
|
276
|
+
return error.code === "ENOENT" ? "missing" : "project";
|
|
277
|
+
}
|
|
278
|
+
try {
|
|
279
|
+
const parsed = JSON.parse(source);
|
|
280
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) && parsed.installScope === "global") {
|
|
281
|
+
return "global";
|
|
282
|
+
}
|
|
283
|
+
} catch {
|
|
284
|
+
}
|
|
285
|
+
return "project";
|
|
286
|
+
}
|
|
287
|
+
function routeCursorHook(params) {
|
|
288
|
+
const relevantPayload = payloadForKind(params.payload, params.kind);
|
|
289
|
+
const selectedPath = selectedActionPath(relevantPayload);
|
|
290
|
+
if (!selectedPath || !path3.isAbsolute(selectedPath)) {
|
|
291
|
+
return { decision: "fail_closed", message: "belay could not determine the workspace." };
|
|
292
|
+
}
|
|
293
|
+
const resolution = resolveCursorActionCwdDetails(relevantPayload, "/");
|
|
294
|
+
const canonicalCwd = resolution.fromPayload ? canonicalExistingPath(resolution.cwd) : void 0;
|
|
295
|
+
if (!canonicalCwd) {
|
|
296
|
+
return { decision: "fail_closed", message: "belay could not determine the workspace." };
|
|
297
|
+
}
|
|
298
|
+
const repoRoot = canonicalExistingPath(findCursorRoutingRepoRoot(canonicalCwd));
|
|
299
|
+
if (!repoRoot) {
|
|
300
|
+
return { decision: "neutral" };
|
|
301
|
+
}
|
|
302
|
+
const installScope = readInstallScope(repoRoot);
|
|
303
|
+
if (installScope === "missing") {
|
|
304
|
+
return { decision: "neutral" };
|
|
305
|
+
}
|
|
306
|
+
if (installScope === "global") {
|
|
307
|
+
return params.origin.scope === "global" ? { decision: "execute", repoRoot } : { decision: "neutral" };
|
|
308
|
+
}
|
|
309
|
+
if (params.origin.scope === "project" && canonicalExistingPath(params.origin.repoRoot) !== repoRoot) {
|
|
310
|
+
return { decision: "neutral" };
|
|
311
|
+
}
|
|
312
|
+
const callableProjectOwner = hasCallableProjectOwner(repoRoot, params);
|
|
313
|
+
if (params.origin.scope === "global") {
|
|
314
|
+
return callableProjectOwner ? { decision: "neutral" } : {
|
|
315
|
+
decision: "fail_closed",
|
|
316
|
+
message: "belay project hook owner is unavailable; the global sentinel blocked this action."
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
const complete = callableProjectOwner && isRegularFile(path3.join(cursorRoutingRuntimeDir(repoRoot), "core.mjs"));
|
|
320
|
+
return complete ? { decision: "execute", repoRoot } : { decision: "fail_closed", message: "belay project hook installation is incomplete." };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// src/adapters/cursor/hook-dispatch-entry.ts
|
|
324
|
+
async function readStdinPayload() {
|
|
325
|
+
const chunks = [];
|
|
326
|
+
for await (const chunk of process2.stdin) {
|
|
327
|
+
chunks.push(typeof chunk === "string" ? chunk : chunk.toString("utf8"));
|
|
328
|
+
}
|
|
329
|
+
const raw = chunks.join("").trim();
|
|
330
|
+
if (!raw) {
|
|
331
|
+
return { ok: false };
|
|
332
|
+
}
|
|
333
|
+
try {
|
|
334
|
+
const parsed = JSON.parse(raw);
|
|
335
|
+
return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? { ok: true, payload: parsed } : { ok: false };
|
|
336
|
+
} catch {
|
|
337
|
+
return { ok: false };
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
function neutralResponse(kind) {
|
|
341
|
+
if (kind === "before-submit") {
|
|
342
|
+
return { continue: true };
|
|
343
|
+
}
|
|
344
|
+
if (kind === "audit") {
|
|
345
|
+
return {};
|
|
346
|
+
}
|
|
347
|
+
return { permission: "allow" };
|
|
348
|
+
}
|
|
349
|
+
function failClosedResponse(kind, message) {
|
|
350
|
+
if (kind === "audit") {
|
|
351
|
+
console.error(`belay audit hook skipped: ${message}`);
|
|
352
|
+
return {};
|
|
353
|
+
}
|
|
354
|
+
if (kind === "before-submit") {
|
|
355
|
+
return { continue: false, user_message: message };
|
|
356
|
+
}
|
|
357
|
+
return { permission: "deny", user_message: message };
|
|
358
|
+
}
|
|
359
|
+
function isRecord(value) {
|
|
360
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
361
|
+
}
|
|
362
|
+
function isNonEmptyString(value) {
|
|
363
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
364
|
+
}
|
|
365
|
+
function isRecognizedPayload(params, payload) {
|
|
366
|
+
if (params.kind === "before-submit") {
|
|
367
|
+
return params.eventName === "beforeSubmitPrompt" && typeof payload.prompt === "string";
|
|
368
|
+
}
|
|
369
|
+
if (params.kind === "shell-gate") {
|
|
370
|
+
return params.eventName === "beforeShellExecution" && typeof payload.command === "string";
|
|
371
|
+
}
|
|
372
|
+
if (params.kind === "tool-gate") {
|
|
373
|
+
if (params.eventName === "preToolUse" || params.eventName === "PreToolUse") {
|
|
374
|
+
return isNonEmptyString(payload.tool_name) && isRecord(payload.tool_input);
|
|
375
|
+
}
|
|
376
|
+
return params.eventName === "subagentStart" && isNonEmptyString(payload.subagent_type) && typeof payload.task === "string";
|
|
377
|
+
}
|
|
378
|
+
if (params.eventName === "postToolUse") {
|
|
379
|
+
return isNonEmptyString(payload.tool_name) && isRecord(payload.tool_input) && typeof payload.tool_output === "string";
|
|
380
|
+
}
|
|
381
|
+
if (params.eventName === "postToolUseFailure") {
|
|
382
|
+
return isNonEmptyString(payload.tool_name) && isRecord(payload.tool_input) && typeof payload.error_message === "string" && typeof payload.failure_type === "string";
|
|
383
|
+
}
|
|
384
|
+
if (params.eventName === "stop") {
|
|
385
|
+
return typeof payload.status === "string" && typeof payload.loop_count === "number";
|
|
386
|
+
}
|
|
387
|
+
if (params.eventName === "sessionEnd") {
|
|
388
|
+
return isNonEmptyString(payload.session_id) && typeof payload.reason === "string";
|
|
389
|
+
}
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
async function executeCoreHandler(params, payload) {
|
|
393
|
+
const coreModulePath = "./core.mjs";
|
|
394
|
+
const core = await import(coreModulePath);
|
|
395
|
+
if (params.kind === "before-submit") {
|
|
396
|
+
return core.handleBeforeSubmitPromptHook(payload);
|
|
397
|
+
}
|
|
398
|
+
if (params.kind === "shell-gate") {
|
|
399
|
+
return core.handleShellGateHook(payload);
|
|
400
|
+
}
|
|
401
|
+
if (params.kind === "tool-gate") {
|
|
402
|
+
return core.handleToolGateHook(params.eventName, payload);
|
|
403
|
+
}
|
|
404
|
+
return core.handleAuditHook(params.eventName, payload);
|
|
405
|
+
}
|
|
406
|
+
async function dispatchCursorHookResponse(params) {
|
|
407
|
+
const input = await readStdinPayload();
|
|
408
|
+
if (!input.ok) {
|
|
409
|
+
return failClosedResponse(params.kind, "belay received malformed Cursor hook input.");
|
|
410
|
+
}
|
|
411
|
+
if (!isRecognizedPayload(params, input.payload)) {
|
|
412
|
+
return neutralResponse(params.kind);
|
|
413
|
+
}
|
|
414
|
+
try {
|
|
415
|
+
const route = routeCursorHook({
|
|
416
|
+
origin: params.origin,
|
|
417
|
+
kind: params.kind,
|
|
418
|
+
eventName: params.eventName,
|
|
419
|
+
payload: input.payload
|
|
420
|
+
});
|
|
421
|
+
if (route.decision === "neutral") {
|
|
422
|
+
return neutralResponse(params.kind);
|
|
423
|
+
}
|
|
424
|
+
if (route.decision === "fail_closed") {
|
|
425
|
+
return failClosedResponse(params.kind, route.message);
|
|
426
|
+
}
|
|
427
|
+
return await executeCoreHandler(params, input.payload);
|
|
428
|
+
} catch (error) {
|
|
429
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
430
|
+
return failClosedResponse(
|
|
431
|
+
params.kind,
|
|
432
|
+
`belay failed while dispatching this Cursor hook: ${detail}`
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
async function dispatchCursorHook(params) {
|
|
437
|
+
const response = await dispatchCursorHookResponse(params);
|
|
438
|
+
process2.stdout.write(`${JSON.stringify(response)}
|
|
439
|
+
`);
|
|
440
|
+
}
|
|
441
|
+
export {
|
|
442
|
+
dispatchCursorHook
|
|
443
|
+
};
|