@guilz-dev/belay 0.9.3 → 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.
Files changed (83) hide show
  1. package/README.md +35 -1
  2. package/dist/adapters/codex/hooks.d.ts +1 -0
  3. package/dist/adapters/codex/hooks.js +3 -0
  4. package/dist/adapters/cursor/dispatcher-generation.d.ts +3 -0
  5. package/dist/adapters/cursor/dispatcher-generation.js +5 -0
  6. package/dist/adapters/cursor/hook-dispatch-entry.d.ts +7 -0
  7. package/dist/adapters/cursor/hook-dispatch-entry.js +126 -0
  8. package/dist/adapters/cursor/hook-router.d.ts +23 -0
  9. package/dist/adapters/cursor/hook-router.js +266 -0
  10. package/dist/adapters/cursor/hooks.d.ts +4 -0
  11. package/dist/adapters/cursor/hooks.js +99 -8
  12. package/dist/adapters/cursor/routing-layout.d.ts +5 -0
  13. package/dist/adapters/cursor/routing-layout.js +29 -0
  14. package/dist/adapters/cursor/runtime-entry.d.ts +17 -0
  15. package/dist/adapters/cursor/runtime-entry.js +39 -39
  16. package/dist/adapters/layouts/scope.d.ts +5 -0
  17. package/dist/adapters/layouts/scope.js +71 -0
  18. package/dist/adapters/shared/gate-runtime.js +1 -0
  19. package/dist/bundle/claude-runtime.mjs +1932 -1740
  20. package/dist/bundle/codex-runtime.mjs +1934 -1742
  21. package/dist/bundle/cursor-dispatcher.mjs +443 -0
  22. package/dist/bundle/cursor-runtime.mjs +2017 -1821
  23. package/dist/commands/doctor.js +191 -15
  24. package/dist/commands/health-snapshot.js +9 -4
  25. package/dist/commands/recovery-checkpoints.d.ts +3 -3
  26. package/dist/config-io.js +11 -2
  27. package/dist/core/audit-legacy-archive.js +43 -25
  28. package/dist/core/classify-tool.js +16 -8
  29. package/dist/core/effect-ir/argv-delegate.d.ts +9 -0
  30. package/dist/core/effect-ir/argv-delegate.js +42 -0
  31. package/dist/core/effect-ir/shell-lower/argv-delegate-gate.d.ts +3 -0
  32. package/dist/core/effect-ir/shell-lower/argv-delegate-gate.js +40 -0
  33. package/dist/core/effect-ir/shell-lower/augment.d.ts +4 -0
  34. package/dist/core/effect-ir/shell-lower/augment.js +97 -0
  35. package/dist/core/effect-ir/shell-lower/context.d.ts +10 -0
  36. package/dist/core/effect-ir/shell-lower/context.js +1 -0
  37. package/dist/core/effect-ir/shell-lower/decode-process.d.ts +9 -0
  38. package/dist/core/effect-ir/shell-lower/decode-process.js +201 -0
  39. package/dist/core/effect-ir/shell-lower/decoders/belay.d.ts +2 -0
  40. package/dist/core/effect-ir/shell-lower/decoders/belay.js +29 -0
  41. package/dist/core/effect-ir/shell-lower/decoders/builtins.d.ts +7 -0
  42. package/dist/core/effect-ir/shell-lower/decoders/builtins.js +194 -0
  43. package/dist/core/effect-ir/shell-lower/decoders/decode-package-exec.d.ts +4 -0
  44. package/dist/core/effect-ir/shell-lower/decoders/decode-package-exec.js +68 -0
  45. package/dist/core/effect-ir/shell-lower/decoders/docker.d.ts +3 -0
  46. package/dist/core/effect-ir/shell-lower/decoders/docker.js +43 -0
  47. package/dist/core/effect-ir/shell-lower/decoders/filesystem.d.ts +9 -0
  48. package/dist/core/effect-ir/shell-lower/decoders/filesystem.js +217 -0
  49. package/dist/core/effect-ir/shell-lower/decoders/prisma.d.ts +2 -0
  50. package/dist/core/effect-ir/shell-lower/decoders/prisma.js +54 -0
  51. package/dist/core/effect-ir/shell-lower/decoders/ruby.d.ts +11 -0
  52. package/dist/core/effect-ir/shell-lower/decoders/ruby.js +147 -0
  53. package/dist/core/effect-ir/shell-lower/decoders/toolchain.d.ts +7 -0
  54. package/dist/core/effect-ir/shell-lower/decoders/toolchain.js +155 -0
  55. package/dist/core/effect-ir/shell-lower/requirement.d.ts +12 -0
  56. package/dist/core/effect-ir/shell-lower/requirement.js +65 -0
  57. package/dist/core/effect-ir/shell-lower/segment.d.ts +11 -0
  58. package/dist/core/effect-ir/shell-lower/segment.js +82 -0
  59. package/dist/core/effect-ir/shell-lower/tokens.d.ts +26 -0
  60. package/dist/core/effect-ir/shell-lower/tokens.js +183 -0
  61. package/dist/core/effect-ir/shell-lower.d.ts +2 -7
  62. package/dist/core/effect-ir/shell-lower.js +60 -1502
  63. package/dist/core/glob.js +32 -16
  64. package/dist/core/integrity.d.ts +2 -2
  65. package/dist/core/integrity.js +52 -12
  66. package/dist/core/replay-scrub.d.ts +2 -0
  67. package/dist/core/replay-scrub.js +8 -0
  68. package/dist/core/shell-substitution.js +2 -2
  69. package/dist/core/shell-unparseable.js +3 -1
  70. package/dist/defaults.d.ts +7 -3
  71. package/dist/defaults.js +29 -22
  72. package/dist/installer/bootstrap.d.ts +1 -0
  73. package/dist/installer/bootstrap.js +2 -2
  74. package/dist/installer/runtime-artifacts.js +43 -13
  75. package/dist/installer.js +44 -9
  76. package/dist/node-resolution.d.ts +1 -0
  77. package/dist/node-resolution.js +61 -0
  78. package/dist/templates.d.ts +7 -4
  79. package/dist/templates.js +42 -4
  80. package/dist/types.d.ts +1 -0
  81. package/dist/version.d.ts +1 -1
  82. package/dist/version.js +1 -1
  83. package/package.json +1 -1
@@ -0,0 +1,29 @@
1
+ import { existsSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ export function cursorRoutingConfigPath(repoRoot) {
4
+ return path.join(repoRoot, '.cursor', 'belay.config.json');
5
+ }
6
+ export function cursorRoutingHooksDir(repoRoot) {
7
+ return path.join(repoRoot, '.cursor', 'hooks');
8
+ }
9
+ export function cursorRoutingHooksSettingsPath(repoRoot) {
10
+ return path.join(repoRoot, '.cursor', 'hooks.json');
11
+ }
12
+ export function cursorRoutingRuntimeDir(repoRoot) {
13
+ return path.join(repoRoot, '.cursor', 'belay', 'runtime');
14
+ }
15
+ export function findCursorRoutingRepoRoot(startPath) {
16
+ const resolvedStart = path.resolve(startPath);
17
+ let current = resolvedStart;
18
+ while (true) {
19
+ if (existsSync(path.join(current, '.git')) ||
20
+ (existsSync(path.join(current, '.cursor')) && existsSync(cursorRoutingConfigPath(current)))) {
21
+ return current;
22
+ }
23
+ const parent = path.dirname(current);
24
+ if (parent === current) {
25
+ return resolvedStart;
26
+ }
27
+ current = parent;
28
+ }
29
+ }
@@ -1,5 +1,22 @@
1
1
  export declare function resolveCursorActionCwd(payload: Record<string, unknown>, fallback?: string): string;
2
+ export declare function handleBeforeSubmitPromptHook(payload: Record<string, unknown>): Promise<{
3
+ replay?: import("../../core/approval-replay.js").ApprovalReplayHint | undefined;
4
+ user_message?: string | undefined;
5
+ continue: boolean;
6
+ }>;
2
7
  export declare function runBeforeSubmitPromptHook(): Promise<void>;
8
+ export declare function handleShellGateHook(payload: Record<string, unknown>): Promise<import("../../core/gate-contract.js").GatePermissionResponse | {
9
+ permission: string;
10
+ user_message: string;
11
+ }>;
3
12
  export declare function runShellGateHook(): Promise<void>;
13
+ export declare function handleToolGateHook(eventName: string, payload: Record<string, unknown>): Promise<import("../../core/gate-contract.js").GatePermissionResponse | {
14
+ permission: string;
15
+ user_message: string;
16
+ } | {
17
+ permission: string;
18
+ user_message?: undefined;
19
+ }>;
4
20
  export declare function runToolGateHook(eventName: string): Promise<void>;
21
+ export declare function handleAuditHook(eventName: string, payload: Record<string, unknown>): Promise<{}>;
5
22
  export declare function runAuditHook(eventName: string): Promise<void>;
@@ -104,45 +104,44 @@ function isSubagentEvent(payload, eventName) {
104
104
  function isFileMutationTool(toolName) {
105
105
  return toolName === 'Write' || toolName === 'StrReplace' || toolName === 'Delete';
106
106
  }
107
- export async function runBeforeSubmitPromptHook() {
107
+ export async function handleBeforeSubmitPromptHook(payload) {
108
108
  try {
109
- const payload = await readStdinJson();
110
109
  const prompt = String(payload.prompt ?? '');
111
110
  const resolution = resolveCursorActionCwdDetails(payload);
112
111
  if (isUnsafeGlobalHookFallback(resolution)) {
113
- jsonResponse({
112
+ return {
114
113
  continue: false,
115
114
  user_message: GLOBAL_HOOK_WORKSPACE_MISSING_MESSAGE,
116
- });
117
- return;
115
+ };
118
116
  }
119
117
  const ctx = await loadRuntimeContext(resolution.cwd);
120
118
  const deps = createDefaultGateRuntimeDeps();
121
119
  const result = await processApprovalPromptWithRepoFallback(ctx, deps, prompt, payload, resolution);
122
- jsonResponse({
120
+ return {
123
121
  continue: result.continue,
124
122
  ...(result.user_message ? { user_message: result.user_message } : {}),
125
123
  ...(result.replay ? { replay: result.replay } : {}),
126
- });
124
+ };
127
125
  }
128
126
  catch {
129
- jsonResponse({
127
+ return {
130
128
  continue: false,
131
129
  user_message: 'belay failed while processing approval state. Run belay doctor, then retry.',
132
- });
130
+ };
133
131
  }
134
132
  }
135
- export async function runShellGateHook() {
133
+ export async function runBeforeSubmitPromptHook() {
134
+ jsonResponse(await handleBeforeSubmitPromptHook(await readStdinJson()));
135
+ }
136
+ export async function handleShellGateHook(payload) {
136
137
  try {
137
- const payload = await readStdinJson();
138
138
  const command = String(payload.command ?? '').trim();
139
139
  const resolution = resolveCursorActionCwdDetails(payload);
140
140
  if (isUnsafeGlobalHookFallback(resolution)) {
141
- jsonResponse({
141
+ return {
142
142
  permission: 'deny',
143
143
  user_message: GLOBAL_HOOK_WORKSPACE_MISSING_MESSAGE,
144
- });
145
- return;
144
+ };
146
145
  }
147
146
  const cwd = resolution.cwd;
148
147
  const ctx = await loadRuntimeContext(cwd);
@@ -154,26 +153,27 @@ export async function runShellGateHook() {
154
153
  payload,
155
154
  sourceEvent: 'beforeShellExecution',
156
155
  });
157
- jsonResponse(gateVerdictToCursorResponse(verdict));
156
+ return gateVerdictToCursorResponse(verdict);
158
157
  }
159
158
  catch {
160
- jsonResponse({
159
+ return {
161
160
  permission: 'deny',
162
161
  user_message: 'belay failed while classifying this shell command. Run belay doctor, then retry.',
163
- });
162
+ };
164
163
  }
165
164
  }
166
- export async function runToolGateHook(eventName) {
165
+ export async function runShellGateHook() {
166
+ jsonResponse(await handleShellGateHook(await readStdinJson()));
167
+ }
168
+ export async function handleToolGateHook(eventName, payload) {
167
169
  try {
168
- const payload = await readStdinJson();
169
170
  const toolName = String(payload.tool_name ?? '');
170
171
  const resolution = resolveCursorToolActionCwdDetails(payload, process.cwd(), eventName, toolName);
171
172
  if (isUnsafeGlobalHookFallback(resolution)) {
172
- jsonResponse({
173
+ return {
173
174
  permission: 'deny',
174
175
  user_message: GLOBAL_HOOK_WORKSPACE_MISSING_MESSAGE,
175
- });
176
- return;
176
+ };
177
177
  }
178
178
  const cwd = resolution.cwd;
179
179
  const ctx = await loadRuntimeContext(cwd);
@@ -185,8 +185,7 @@ export async function runToolGateHook(eventName) {
185
185
  payload,
186
186
  sourceEvent: eventName,
187
187
  });
188
- jsonResponse(gateVerdictToCursorResponse(verdict));
189
- return;
188
+ return gateVerdictToCursorResponse(verdict);
190
189
  }
191
190
  if (toolName === 'Shell') {
192
191
  const verdict = await evaluateGatedAction(ctx, deps, {
@@ -196,8 +195,7 @@ export async function runToolGateHook(eventName) {
196
195
  toolName,
197
196
  sourceEvent: eventName,
198
197
  });
199
- jsonResponse(gateVerdictToCursorResponse(verdict));
200
- return;
198
+ return gateVerdictToCursorResponse(verdict);
201
199
  }
202
200
  if (isFileMutationTool(toolName)) {
203
201
  const verdict = await evaluateGatedAction(ctx, deps, {
@@ -207,8 +205,7 @@ export async function runToolGateHook(eventName) {
207
205
  toolName,
208
206
  sourceEvent: eventName,
209
207
  });
210
- jsonResponse(gateVerdictToCursorResponse(verdict));
211
- return;
208
+ return gateVerdictToCursorResponse(verdict);
212
209
  }
213
210
  if (payload.tool_name === 'Task') {
214
211
  const verdict = await evaluateGatedAction(ctx, deps, {
@@ -217,34 +214,37 @@ export async function runToolGateHook(eventName) {
217
214
  payload,
218
215
  sourceEvent: eventName,
219
216
  });
220
- jsonResponse(gateVerdictToCursorResponse(verdict));
221
- return;
217
+ return gateVerdictToCursorResponse(verdict);
222
218
  }
223
- jsonResponse({ permission: 'allow' });
219
+ return { permission: 'allow' };
224
220
  }
225
221
  catch {
226
- jsonResponse({
222
+ return {
227
223
  permission: 'deny',
228
224
  user_message: 'belay failed while classifying this tool action. Run belay doctor, then retry.',
229
- });
225
+ };
230
226
  }
231
227
  }
232
- export async function runAuditHook(eventName) {
228
+ export async function runToolGateHook(eventName) {
229
+ jsonResponse(await handleToolGateHook(eventName, await readStdinJson()));
230
+ }
231
+ export async function handleAuditHook(eventName, payload) {
233
232
  try {
234
- const payload = await readStdinJson();
235
233
  const toolName = String(payload.tool_name ?? '');
236
234
  const resolution = resolveCursorToolActionCwdDetails(payload, process.cwd(), eventName, toolName);
237
235
  if (isUnsafeGlobalHookFallback(resolution)) {
238
- jsonResponse({});
239
- return;
236
+ return {};
240
237
  }
241
238
  const ctx = await loadRuntimeContext(resolution.cwd);
242
239
  const deps = createDefaultGateRuntimeDeps();
243
240
  await appendObservedAudit(ctx, deps, eventName, payload);
244
- jsonResponse({});
241
+ return {};
245
242
  }
246
243
  catch (error) {
247
244
  console.error('belay audit hook failed:', error instanceof Error ? error.message : String(error));
248
- jsonResponse({});
245
+ return {};
249
246
  }
250
247
  }
248
+ export async function runAuditHook(eventName) {
249
+ jsonResponse(await handleAuditHook(eventName, await readStdinJson()));
250
+ }
@@ -11,8 +11,13 @@ export interface ScopedPaths {
11
11
  skillsDir: string;
12
12
  commandsDir?: string;
13
13
  }
14
+ export declare function resolveTrustedWindowsPowerShellPath(systemRoot?: string | undefined): string;
14
15
  export declare function isPathInside(child: string, parent: string): boolean;
15
16
  export declare function buildRunnerInvocation(platform: NodeJS.Platform, hooksDir: string, repoRoot: string, hookScript: string, ...args: string[]): string;
17
+ export declare function buildAbsoluteRunnerInvocation(platform: NodeJS.Platform, hooksDir: string, hookScript: string, ...args: string[]): string;
18
+ export declare function buildLegacyBarePowerShellRunnerInvocation(platform: NodeJS.Platform, hooksDir: string, hookScript: string, ...args: string[]): string;
19
+ export declare function buildLegacyQuotedAbsoluteRunnerInvocation(platform: NodeJS.Platform, hooksDir: string, hookScript: string, ...args: string[]): string;
20
+ export declare function buildLegacyAbsoluteRunnerInvocation(platform: NodeJS.Platform, hooksDir: string, hookScript: string, ...args: string[]): string;
16
21
  export declare function resolveScopedPaths(layout: AdapterLayout, scope: InstallScope, repoRoot: string): ScopedPaths;
17
22
  export declare function resolveInstallScope(options: {
18
23
  scope?: InstallScope;
@@ -1,6 +1,50 @@
1
+ import { existsSync, realpathSync } from 'node:fs';
1
2
  import os from 'node:os';
2
3
  import path from 'node:path';
3
4
  import { isPathOutsideRoot } from '../../core/path-utils.js';
5
+ function canonicalizePotentialPath(inputPath) {
6
+ const unresolvedSegments = [];
7
+ let existingAncestor = path.resolve(inputPath);
8
+ while (!existsSync(existingAncestor)) {
9
+ const parent = path.dirname(existingAncestor);
10
+ if (parent === existingAncestor) {
11
+ break;
12
+ }
13
+ unresolvedSegments.unshift(path.basename(existingAncestor));
14
+ existingAncestor = parent;
15
+ }
16
+ return existsSync(existingAncestor)
17
+ ? path.join(realpathSync(existingAncestor), ...unresolvedSegments)
18
+ : path.resolve(inputPath);
19
+ }
20
+ function quotePowerShellLiteral(value) {
21
+ return `'${value.replaceAll("'", "''")}'`;
22
+ }
23
+ export function resolveTrustedWindowsPowerShellPath(systemRoot = process.env.SystemRoot || process.env.WINDIR) {
24
+ if (!systemRoot || !path.win32.isAbsolute(systemRoot)) {
25
+ throw new Error('A trusted Windows system root must be an absolute drive path.');
26
+ }
27
+ const hasControlCharacter = [...systemRoot].some((character) => character.charCodeAt(0) < 32);
28
+ if (systemRoot !== systemRoot.trim() || hasControlCharacter || /[%!?*"&|<>^]/.test(systemRoot)) {
29
+ throw new Error('The trusted Windows system root contains unsafe path or cmd.exe expansion characters.');
30
+ }
31
+ if (systemRoot.slice(2).includes(':')) {
32
+ throw new Error('The trusted Windows system root contains an invalid drive path.');
33
+ }
34
+ const normalizedRoot = path.win32.normalize(systemRoot);
35
+ if (!/^[A-Za-z]:\\$/.test(path.win32.parse(normalizedRoot).root)) {
36
+ throw new Error('A trusted Windows system root must be an absolute drive path.');
37
+ }
38
+ return path.win32.join(normalizedRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
39
+ }
40
+ function buildEncodedPowerShellRunnerInvocation(executable, runnerPath, hookScript, args) {
41
+ const encodedCommand = Buffer.from([
42
+ '&',
43
+ quotePowerShellLiteral(runnerPath),
44
+ ...[hookScript, ...args].map(quotePowerShellLiteral),
45
+ ].join(' '), 'utf16le').toString('base64');
46
+ return `${executable} -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand ${encodedCommand}`;
47
+ }
4
48
  function agentHomeDir(adapter) {
5
49
  const home = os.homedir();
6
50
  if (adapter === 'cursor') {
@@ -38,6 +82,33 @@ export function buildRunnerInvocation(platform, hooksDir, repoRoot, hookScript,
38
82
  : runnerAbs;
39
83
  return [runnerRef, hookScript, ...args].join(' ');
40
84
  }
85
+ export function buildAbsoluteRunnerInvocation(platform, hooksDir, hookScript, ...args) {
86
+ const runnerFile = platform === 'win32' ? 'belay-runner.ps1' : 'belay-runner';
87
+ const canonicalHooksDir = canonicalizePotentialPath(hooksDir);
88
+ const runnerPath = path.join(canonicalHooksDir, runnerFile);
89
+ if (platform === 'win32') {
90
+ const powerShellPath = resolveTrustedWindowsPowerShellPath();
91
+ return buildEncodedPowerShellRunnerInvocation(`"${powerShellPath}"`, runnerPath, hookScript, args);
92
+ }
93
+ return [`'${runnerPath.replaceAll("'", "'\\''")}'`, hookScript, ...args].join(' ');
94
+ }
95
+ export function buildLegacyBarePowerShellRunnerInvocation(platform, hooksDir, hookScript, ...args) {
96
+ if (platform !== 'win32') {
97
+ return buildAbsoluteRunnerInvocation(platform, hooksDir, hookScript, ...args);
98
+ }
99
+ const runnerPath = path.join(canonicalizePotentialPath(hooksDir), 'belay-runner.ps1');
100
+ return buildEncodedPowerShellRunnerInvocation('powershell.exe', runnerPath, hookScript, args);
101
+ }
102
+ export function buildLegacyQuotedAbsoluteRunnerInvocation(platform, hooksDir, hookScript, ...args) {
103
+ const runnerFile = platform === 'win32' ? 'belay-runner.cmd' : 'belay-runner';
104
+ const runnerPath = path.join(canonicalizePotentialPath(hooksDir), runnerFile);
105
+ const quotedRunnerPath = platform === 'win32' ? `"${runnerPath}"` : `'${runnerPath.replaceAll("'", "'\\''")}'`;
106
+ return [quotedRunnerPath, hookScript, ...args].join(' ');
107
+ }
108
+ export function buildLegacyAbsoluteRunnerInvocation(platform, hooksDir, hookScript, ...args) {
109
+ const runnerFile = platform === 'win32' ? 'belay-runner.cmd' : 'belay-runner';
110
+ return [path.resolve(hooksDir, runnerFile), hookScript, ...args].join(' ');
111
+ }
41
112
  export function resolveScopedPaths(layout, scope, repoRoot) {
42
113
  const resolvedRepo = path.resolve(repoRoot);
43
114
  const adapter = layout.name;
@@ -910,6 +910,7 @@ async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {})
910
910
  event: auditEvent,
911
911
  sourceEvent,
912
912
  kind,
913
+ repoLabel: path.basename(ctx.repoRoot),
913
914
  ...(auditExtras.toolInvocationCorrelationId
914
915
  ? { toolInvocationCorrelationId: auditExtras.toolInvocationCorrelationId }
915
916
  : {}),