@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.
Files changed (118) hide show
  1. package/README.md +36 -2
  2. package/dist/adapters/codex/hooks.d.ts +1 -0
  3. package/dist/adapters/codex/hooks.js +3 -0
  4. package/dist/adapters/codex/runtime-entry.d.ts +3 -0
  5. package/dist/adapters/codex/runtime-entry.js +27 -4
  6. package/dist/adapters/cursor/cwd-resolution.d.ts +10 -0
  7. package/dist/adapters/cursor/cwd-resolution.js +58 -0
  8. package/dist/adapters/cursor/dispatcher-generation.d.ts +3 -0
  9. package/dist/adapters/cursor/dispatcher-generation.js +5 -0
  10. package/dist/adapters/cursor/hook-dispatch-entry.d.ts +7 -0
  11. package/dist/adapters/cursor/hook-dispatch-entry.js +126 -0
  12. package/dist/adapters/cursor/hook-router.d.ts +23 -0
  13. package/dist/adapters/cursor/hook-router.js +266 -0
  14. package/dist/adapters/cursor/hooks.d.ts +5 -0
  15. package/dist/adapters/cursor/hooks.js +115 -3
  16. package/dist/adapters/cursor/routing-layout.d.ts +5 -0
  17. package/dist/adapters/cursor/routing-layout.js +29 -0
  18. package/dist/adapters/cursor/runtime-entry.d.ts +18 -0
  19. package/dist/adapters/cursor/runtime-entry.js +135 -34
  20. package/dist/adapters/layouts/scope.d.ts +5 -0
  21. package/dist/adapters/layouts/scope.js +71 -0
  22. package/dist/adapters/shared/gate-runtime.js +27 -3
  23. package/dist/adapters/shared/repo-root.js +20 -1
  24. package/dist/bundle/claude-runtime.mjs +2660 -1951
  25. package/dist/bundle/codex-runtime.mjs +2680 -1953
  26. package/dist/bundle/cursor-dispatcher.mjs +443 -0
  27. package/dist/bundle/cursor-runtime.mjs +5638 -4697
  28. package/dist/cli.js +33 -3
  29. package/dist/commands/doctor.js +215 -16
  30. package/dist/commands/health-snapshot.d.ts +3 -0
  31. package/dist/commands/health-snapshot.js +61 -0
  32. package/dist/commands/recovery-checkpoints.d.ts +3 -3
  33. package/dist/commands/report.js +14 -0
  34. package/dist/commands/status.js +15 -0
  35. package/dist/commands/where.d.ts +4 -0
  36. package/dist/commands/where.js +52 -0
  37. package/dist/config-io.js +11 -2
  38. package/dist/core/approval-repo-lookup.d.ts +16 -0
  39. package/dist/core/approval-repo-lookup.js +48 -0
  40. package/dist/core/audit-io.d.ts +1 -1
  41. package/dist/core/audit-io.js +1 -1
  42. package/dist/core/audit-legacy-archive.js +43 -25
  43. package/dist/core/audit-query.d.ts +1 -0
  44. package/dist/core/audit-query.js +7 -0
  45. package/dist/core/audit-serialize.d.ts +3 -0
  46. package/dist/core/audit-serialize.js +39 -3
  47. package/dist/core/audit-summary.d.ts +9 -0
  48. package/dist/core/audit-summary.js +58 -1
  49. package/dist/core/audit-types.d.ts +4 -0
  50. package/dist/core/classify-tool.js +16 -8
  51. package/dist/core/effect-ir/argv-delegate.d.ts +9 -0
  52. package/dist/core/effect-ir/argv-delegate.js +42 -0
  53. package/dist/core/effect-ir/shell-lower/argv-delegate-gate.d.ts +3 -0
  54. package/dist/core/effect-ir/shell-lower/argv-delegate-gate.js +40 -0
  55. package/dist/core/effect-ir/shell-lower/augment.d.ts +4 -0
  56. package/dist/core/effect-ir/shell-lower/augment.js +97 -0
  57. package/dist/core/effect-ir/shell-lower/context.d.ts +10 -0
  58. package/dist/core/effect-ir/shell-lower/context.js +1 -0
  59. package/dist/core/effect-ir/shell-lower/decode-process.d.ts +9 -0
  60. package/dist/core/effect-ir/shell-lower/decode-process.js +201 -0
  61. package/dist/core/effect-ir/shell-lower/decoders/belay.d.ts +2 -0
  62. package/dist/core/effect-ir/shell-lower/decoders/belay.js +29 -0
  63. package/dist/core/effect-ir/shell-lower/decoders/builtins.d.ts +7 -0
  64. package/dist/core/effect-ir/shell-lower/decoders/builtins.js +194 -0
  65. package/dist/core/effect-ir/shell-lower/decoders/decode-package-exec.d.ts +4 -0
  66. package/dist/core/effect-ir/shell-lower/decoders/decode-package-exec.js +68 -0
  67. package/dist/core/effect-ir/shell-lower/decoders/docker.d.ts +3 -0
  68. package/dist/core/effect-ir/shell-lower/decoders/docker.js +43 -0
  69. package/dist/core/effect-ir/shell-lower/decoders/filesystem.d.ts +9 -0
  70. package/dist/core/effect-ir/shell-lower/decoders/filesystem.js +217 -0
  71. package/dist/core/effect-ir/shell-lower/decoders/prisma.d.ts +2 -0
  72. package/dist/core/effect-ir/shell-lower/decoders/prisma.js +54 -0
  73. package/dist/core/effect-ir/shell-lower/decoders/ruby.d.ts +11 -0
  74. package/dist/core/effect-ir/shell-lower/decoders/ruby.js +147 -0
  75. package/dist/core/effect-ir/shell-lower/decoders/toolchain.d.ts +7 -0
  76. package/dist/core/effect-ir/shell-lower/decoders/toolchain.js +155 -0
  77. package/dist/core/effect-ir/shell-lower/requirement.d.ts +12 -0
  78. package/dist/core/effect-ir/shell-lower/requirement.js +65 -0
  79. package/dist/core/effect-ir/shell-lower/segment.d.ts +11 -0
  80. package/dist/core/effect-ir/shell-lower/segment.js +82 -0
  81. package/dist/core/effect-ir/shell-lower/tokens.d.ts +26 -0
  82. package/dist/core/effect-ir/shell-lower/tokens.js +183 -0
  83. package/dist/core/effect-ir/shell-lower.d.ts +2 -7
  84. package/dist/core/effect-ir/shell-lower.js +122 -1511
  85. package/dist/core/glob.js +32 -16
  86. package/dist/core/integrity.d.ts +2 -2
  87. package/dist/core/integrity.js +52 -12
  88. package/dist/core/replay-scrub.d.ts +3 -0
  89. package/dist/core/replay-scrub.js +30 -3
  90. package/dist/core/shell-substitution.js +2 -2
  91. package/dist/core/shell-tokenizer.d.ts +28 -0
  92. package/dist/core/shell-tokenizer.js +111 -29
  93. package/dist/core/shell-unparseable.js +3 -1
  94. package/dist/core/verdict/docker-compose-run.d.ts +18 -0
  95. package/dist/core/verdict/docker-compose-run.js +136 -0
  96. package/dist/core/verdict/launcher-resolve.js +28 -28
  97. package/dist/core/verdict/parser.d.ts +3 -0
  98. package/dist/core/verdict/parser.js +23 -40
  99. package/dist/core/verdict/recursive-invocation.d.ts +20 -0
  100. package/dist/core/verdict/recursive-invocation.js +224 -0
  101. package/dist/defaults.d.ts +7 -3
  102. package/dist/defaults.js +35 -21
  103. package/dist/installer/bootstrap.d.ts +1 -0
  104. package/dist/installer/bootstrap.js +2 -2
  105. package/dist/installer/runtime-artifacts.js +43 -13
  106. package/dist/installer/scope-config.d.ts +2 -2
  107. package/dist/installer.d.ts +10 -1
  108. package/dist/installer.js +83 -7
  109. package/dist/node-resolution.d.ts +1 -0
  110. package/dist/node-resolution.js +61 -0
  111. package/dist/templates.d.ts +7 -4
  112. package/dist/templates.js +42 -4
  113. package/dist/types.d.ts +35 -1
  114. package/dist/version.d.ts +1 -1
  115. package/dist/version.js +1 -1
  116. package/package.json +5 -2
  117. package/skills/belay/SKILL.md +5 -0
  118. package/skills/belay/belay-report.md +4 -1
@@ -1,23 +1,53 @@
1
- import { chmod, mkdir, writeFile } from 'node:fs/promises';
1
+ import { randomUUID } from 'node:crypto';
2
+ import { chmod, mkdir, realpath, rename, rm, writeFile } from 'node:fs/promises';
2
3
  import path from 'node:path';
3
- import { buildRunnerScript, buildWindowsRunnerScript } from '../node-resolution.js';
4
- import { renderAuditHook, renderBeforeSubmitHook, renderRuntimeCore, renderShellGateHook, renderToolGateHook, } from '../templates.js';
4
+ import { buildRunnerScript, buildWindowsPowerShellRunnerScript, buildWindowsRunnerScript, } from '../node-resolution.js';
5
+ import { renderAuditHook, renderBeforeSubmitHook, renderCursorDispatcher, renderRuntimeCore, renderShellGateHook, renderToolGateHook, } from '../templates.js';
5
6
  async function writeFileMaybeExecutable(filePath, content, executable = false) {
6
7
  await mkdir(path.dirname(filePath), { recursive: true });
7
- await writeFile(filePath, content, 'utf8');
8
- if (executable) {
9
- await chmod(filePath, 0o755);
8
+ const temporaryPath = path.join(path.dirname(filePath), `.${path.basename(filePath)}.${process.pid}.${randomUUID()}.tmp`);
9
+ try {
10
+ await writeFile(temporaryPath, content, { encoding: 'utf8', flag: 'wx' });
11
+ if (executable) {
12
+ await chmod(temporaryPath, 0o755);
13
+ }
14
+ await rename(temporaryPath, filePath);
15
+ }
16
+ finally {
17
+ await rm(temporaryPath, { force: true });
10
18
  }
11
19
  }
12
20
  export async function writeRuntimeArtifacts(adapterName, paths) {
13
21
  const { hooksDir, runtimeDir } = paths;
22
+ const cursorOrigin = adapterName === 'cursor'
23
+ ? paths.scope === 'global'
24
+ ? { scope: 'global' }
25
+ : { scope: 'project', repoRoot: await realpath(paths.repoRoot) }
26
+ : undefined;
27
+ const artifacts = {
28
+ auditHook: renderAuditHook(adapterName, cursorOrigin),
29
+ beforeSubmitHook: renderBeforeSubmitHook(adapterName, cursorOrigin),
30
+ core: await renderRuntimeCore(adapterName),
31
+ dispatcher: adapterName === 'cursor' ? await renderCursorDispatcher() : undefined,
32
+ runner: buildRunnerScript(process.execPath),
33
+ runnerCmd: buildWindowsRunnerScript(process.execPath),
34
+ runnerPowerShell: adapterName === 'cursor' ? buildWindowsPowerShellRunnerScript(process.execPath) : undefined,
35
+ shellGateHook: renderShellGateHook(adapterName, cursorOrigin),
36
+ toolGateHook: renderToolGateHook(adapterName, cursorOrigin),
37
+ };
14
38
  await mkdir(hooksDir, { recursive: true });
15
39
  await mkdir(runtimeDir, { recursive: true });
16
- await writeFileMaybeExecutable(path.join(hooksDir, 'belay-before-submit.mjs'), renderBeforeSubmitHook());
17
- await writeFileMaybeExecutable(path.join(hooksDir, 'belay-shell-gate.mjs'), renderShellGateHook());
18
- await writeFileMaybeExecutable(path.join(hooksDir, 'belay-tool-gate.mjs'), renderToolGateHook());
19
- await writeFileMaybeExecutable(path.join(hooksDir, 'belay-audit.mjs'), renderAuditHook());
20
- await writeFileMaybeExecutable(path.join(runtimeDir, 'core.mjs'), await renderRuntimeCore(adapterName));
21
- await writeFileMaybeExecutable(path.join(hooksDir, 'belay-runner'), buildRunnerScript(process.execPath), true);
22
- await writeFileMaybeExecutable(path.join(hooksDir, 'belay-runner.cmd'), buildWindowsRunnerScript(process.execPath));
40
+ await writeFileMaybeExecutable(path.join(runtimeDir, 'core.mjs'), artifacts.core);
41
+ if (artifacts.dispatcher !== undefined) {
42
+ await writeFileMaybeExecutable(path.join(runtimeDir, 'dispatcher.mjs'), artifacts.dispatcher);
43
+ }
44
+ await writeFileMaybeExecutable(path.join(hooksDir, 'belay-runner'), artifacts.runner, true);
45
+ await writeFileMaybeExecutable(path.join(hooksDir, 'belay-runner.cmd'), artifacts.runnerCmd);
46
+ if (artifacts.runnerPowerShell !== undefined) {
47
+ await writeFileMaybeExecutable(path.join(hooksDir, 'belay-runner.ps1'), artifacts.runnerPowerShell);
48
+ }
49
+ await writeFileMaybeExecutable(path.join(hooksDir, 'belay-before-submit.mjs'), artifacts.beforeSubmitHook);
50
+ await writeFileMaybeExecutable(path.join(hooksDir, 'belay-shell-gate.mjs'), artifacts.shellGateHook);
51
+ await writeFileMaybeExecutable(path.join(hooksDir, 'belay-tool-gate.mjs'), artifacts.toolGateHook);
52
+ await writeFileMaybeExecutable(path.join(hooksDir, 'belay-audit.mjs'), artifacts.auditHook);
23
53
  }
@@ -1,8 +1,8 @@
1
1
  import { type ScopedPaths } from '../adapters/layouts/scope.js';
2
2
  import type { AdapterName } from '../adapters/layouts/types.js';
3
3
  import type { BelayConfigV4 } from '../core/config.js';
4
- import type { InitOptions, UpgradeOptions } from '../types.js';
4
+ import type { InitOptions, UninstallOptions, UpgradeOptions } from '../types.js';
5
5
  export type OperationScope = 'project' | 'global';
6
- export declare function resolveOperationScope(repoRoot: string, adapter: AdapterName, options?: InitOptions | UpgradeOptions): Promise<OperationScope>;
6
+ export declare function resolveOperationScope(repoRoot: string, adapter: AdapterName, options?: InitOptions | UpgradeOptions | UninstallOptions): Promise<OperationScope>;
7
7
  export declare function applyInstallScope(repoRoot: string, adapter: AdapterName, scope: OperationScope, config?: BelayConfigV4): Promise<BelayConfigV4>;
8
8
  export declare function pathsForOperation(adapter: AdapterName, scope: OperationScope, repoRoot: string): ScopedPaths;
@@ -1,5 +1,5 @@
1
1
  import type { AdapterName } from './adapters/layouts/types.js';
2
- import type { HooksFile, InitOptions, UpgradeOptions } from './types.js';
2
+ import type { HooksFile, InitOptions, UninstallOptions, UpgradeOptions } from './types.js';
3
3
  export type { InstallScope } from './adapters/layouts/scope.js';
4
4
  export declare function loadHooksFile(hooksPath: string): Promise<HooksFile>;
5
5
  export declare function mergeHooksFile(current: HooksFile, platform?: NodeJS.Platform, hooksDir?: string, repoRoot?: string): HooksFile;
@@ -10,6 +10,10 @@ export declare function initCursorProject(options?: InitOptions): Promise<{
10
10
  export declare function upgradeCursorProject(options?: UpgradeOptions): Promise<{
11
11
  repoRoot: string;
12
12
  }>;
13
+ export declare function uninstallCursorProject(options?: UninstallOptions): Promise<{
14
+ repoRoot: string;
15
+ scope: 'project' | 'global';
16
+ }>;
13
17
  export declare function initProject(options?: InitOptions): Promise<{
14
18
  repoRoot: string;
15
19
  withSkill: boolean;
@@ -20,3 +24,8 @@ export declare function upgradeProject(options?: UpgradeOptions): Promise<{
20
24
  repoRoot: string;
21
25
  adapter: AdapterName;
22
26
  }>;
27
+ export declare function uninstallProject(options?: UninstallOptions): Promise<{
28
+ repoRoot: string;
29
+ adapter: AdapterName;
30
+ scope: 'project' | 'global';
31
+ }>;
package/dist/installer.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { existsSync } from 'node:fs';
2
- import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
3
3
  import path from 'node:path';
4
- import { mergeCursorHooksFile } from './adapters/cursor/hooks.js';
4
+ import { hasManagedCursorHookEntries, mergeCursorHooksFile, stripCursorHooksFile, } from './adapters/cursor/hooks.js';
5
5
  import { cursorLayout } from './adapters/layouts/cursor.js';
6
6
  import { resolveScopedPaths } from './adapters/layouts/scope.js';
7
7
  import { getAdapter } from './adapters/registry.js';
@@ -13,7 +13,7 @@ import { isFreshConfigInput, mergeConfig, normalizeConfig, } from './core/config
13
13
  import { runtimeIntegrityFiles, writeIntegrityManifest } from './core/integrity.js';
14
14
  import { hasValidCloudConsent, isCloudJudgeConfig, migrateImplicitLocalJudgeIfNeeded, resolveInitJudgeConfig, } from './core/judge-config.js';
15
15
  import { resolveJudgeTransport } from './core/judge-runtime-detection.js';
16
- import { bootstrapStateFiles, writeSkillArtifacts } from './installer/bootstrap.js';
16
+ import { bootstrapStateFiles, CURSOR_COMMAND_ARTIFACTS, writeSkillArtifacts, } from './installer/bootstrap.js';
17
17
  import { writeRuntimeArtifacts } from './installer/runtime-artifacts.js';
18
18
  import { applyInstallScope, resolveOperationScope } from './installer/scope-config.js';
19
19
  import { applyConfigPreset } from './presets.js';
@@ -64,7 +64,6 @@ export async function initCursorProject(options = {}) {
64
64
  const mergedHooks = mergeCursorHooksFile(hooksFile, process.platform, paths.hooksDir, repoRoot);
65
65
  await ensureDir(paths.hooksDir);
66
66
  const config = await mergeAndWriteConfig(repoRoot, 'cursor');
67
- await applyInstallScope(repoRoot, 'cursor', scope, config);
68
67
  await writeRuntimeArtifacts('cursor', paths);
69
68
  await bootstrapStateFiles(repoRoot, config, paths);
70
69
  if (withSkill) {
@@ -72,8 +71,12 @@ export async function initCursorProject(options = {}) {
72
71
  }
73
72
  await mkdir(path.dirname(paths.hooksSettingsPath), { recursive: true });
74
73
  await writeFile(paths.hooksSettingsPath, `${JSON.stringify(mergedHooks, null, 2)}\n`, 'utf8');
74
+ const installedConfig = await applyInstallScope(repoRoot, 'cursor', scope, config);
75
+ if (scope === 'global') {
76
+ await cleanupStaleProjectCursorInstall(repoRoot);
77
+ }
75
78
  await writeIntegrityManifest(repoRoot, cursorLayout, runtimeIntegrityFiles(cursorLayout, paths));
76
- await archiveLegacyAuditLogIfNeeded(repoRoot, config);
79
+ await archiveLegacyAuditLogIfNeeded(repoRoot, installedConfig);
77
80
  return { repoRoot, withSkill };
78
81
  }
79
82
  export async function upgradeCursorProject(options = {}) {
@@ -81,19 +84,83 @@ export async function upgradeCursorProject(options = {}) {
81
84
  const scope = await resolveOperationScope(repoRoot, 'cursor', options);
82
85
  const paths = resolveScopedPaths(cursorLayout, scope, repoRoot);
83
86
  const config = await mergeAndWriteConfig(repoRoot, 'cursor');
84
- await applyInstallScope(repoRoot, 'cursor', scope, config);
85
87
  await writeRuntimeArtifacts('cursor', paths);
86
88
  const hooksFile = await loadHooksFile(paths.hooksSettingsPath);
87
89
  const merged = mergeCursorHooksFile(hooksFile, process.platform, paths.hooksDir, repoRoot);
88
90
  await mkdir(path.dirname(paths.hooksSettingsPath), { recursive: true });
89
91
  await writeFile(paths.hooksSettingsPath, `${JSON.stringify(merged, null, 2)}\n`, 'utf8');
92
+ if (scope === 'project') {
93
+ const globalPaths = resolveScopedPaths(cursorLayout, 'global', repoRoot);
94
+ if (existsSync(globalPaths.hooksSettingsPath)) {
95
+ const globalHooks = await loadHooksFile(globalPaths.hooksSettingsPath);
96
+ if (hasManagedCursorHookEntries(globalHooks, process.platform, globalPaths.hooksDir, repoRoot)) {
97
+ await writeRuntimeArtifacts('cursor', globalPaths);
98
+ const mergedGlobal = mergeCursorHooksFile(globalHooks, process.platform, globalPaths.hooksDir, repoRoot);
99
+ await writeFile(globalPaths.hooksSettingsPath, `${JSON.stringify(mergedGlobal, null, 2)}\n`, 'utf8');
100
+ }
101
+ }
102
+ }
90
103
  if (options.withSkill) {
91
104
  await writeSkillArtifacts('cursor', paths);
92
105
  }
106
+ const installedConfig = await applyInstallScope(repoRoot, 'cursor', scope, config);
107
+ if (scope === 'global') {
108
+ await cleanupStaleProjectCursorInstall(repoRoot);
109
+ }
93
110
  await writeIntegrityManifest(repoRoot, cursorLayout, runtimeIntegrityFiles(cursorLayout, paths));
94
- await archiveLegacyAuditLogIfNeeded(repoRoot, config);
111
+ await archiveLegacyAuditLogIfNeeded(repoRoot, installedConfig);
95
112
  return { repoRoot };
96
113
  }
114
+ const BELAY_HOOK_ARTIFACTS = [
115
+ 'belay-before-submit.mjs',
116
+ 'belay-shell-gate.mjs',
117
+ 'belay-tool-gate.mjs',
118
+ 'belay-audit.mjs',
119
+ 'belay-runner',
120
+ 'belay-runner.cmd',
121
+ 'belay-runner.ps1',
122
+ ];
123
+ async function removeBelayHookArtifacts(paths) {
124
+ for (const fileName of BELAY_HOOK_ARTIFACTS) {
125
+ await rm(path.join(paths.hooksDir, fileName), { force: true });
126
+ }
127
+ for (const fileName of ['core.mjs', 'dispatcher.mjs']) {
128
+ await rm(path.join(paths.runtimeDir, fileName), { force: true });
129
+ }
130
+ await rm(path.join(paths.skillsDir, 'SKILL.md'), { force: true });
131
+ if (paths.commandsDir) {
132
+ for (const fileName of CURSOR_COMMAND_ARTIFACTS) {
133
+ await rm(path.join(paths.commandsDir, fileName), { force: true });
134
+ }
135
+ }
136
+ }
137
+ async function cleanupStaleProjectCursorInstall(repoRoot) {
138
+ const projectPaths = resolveScopedPaths(cursorLayout, 'project', repoRoot);
139
+ if (!existsSync(projectPaths.hooksSettingsPath)) {
140
+ return;
141
+ }
142
+ const projectHooks = await loadHooksFile(projectPaths.hooksSettingsPath);
143
+ if (!hasManagedCursorHookEntries(projectHooks, process.platform, projectPaths.hooksDir, repoRoot)) {
144
+ return;
145
+ }
146
+ const stripped = stripCursorHooksFile(projectHooks, process.platform, projectPaths.hooksDir, repoRoot);
147
+ await writeFile(projectPaths.hooksSettingsPath, `${JSON.stringify(stripped, null, 2)}\n`, 'utf8');
148
+ await removeBelayHookArtifacts(projectPaths);
149
+ }
150
+ export async function uninstallCursorProject(options = {}) {
151
+ const repoRoot = path.resolve(options.targetDir ?? process.cwd());
152
+ const scope = await resolveOperationScope(repoRoot, 'cursor', options);
153
+ const paths = resolveScopedPaths(cursorLayout, scope, repoRoot);
154
+ const hooksSettingsExisted = existsSync(paths.hooksSettingsPath);
155
+ const hooksFile = await loadHooksFile(paths.hooksSettingsPath);
156
+ const stripped = stripCursorHooksFile(hooksFile, process.platform, paths.hooksDir, repoRoot);
157
+ if (hooksSettingsExisted) {
158
+ await mkdir(path.dirname(paths.hooksSettingsPath), { recursive: true });
159
+ await writeFile(paths.hooksSettingsPath, `${JSON.stringify(stripped, null, 2)}\n`, 'utf8');
160
+ }
161
+ await removeBelayHookArtifacts(paths);
162
+ return { repoRoot, scope };
163
+ }
97
164
  function resolveAdapterName(options, repoRoot) {
98
165
  if (options.adapter === 'claude') {
99
166
  return 'claude';
@@ -225,3 +292,12 @@ export async function upgradeProject(options = {}) {
225
292
  await refreshIntegrityManifest(repoRoot, adapterName);
226
293
  return { repoRoot, adapter: adapterName };
227
294
  }
295
+ export async function uninstallProject(options = {}) {
296
+ const repoRoot = path.resolve(options.targetDir ?? process.cwd());
297
+ const adapterName = resolveAdapterName(options, repoRoot);
298
+ if (adapterName !== 'cursor') {
299
+ throw new Error(`belay uninstall is only supported for Cursor adapter today. Use --adapter cursor or uninstall hooks manually for ${adapterName}.`);
300
+ }
301
+ const result = await uninstallCursorProject({ ...options, targetDir: repoRoot });
302
+ return { repoRoot: result.repoRoot, adapter: adapterName, scope: result.scope };
303
+ }
@@ -6,3 +6,4 @@ export interface NodeResolutionResult {
6
6
  export declare function resolveNodeBinary(): NodeResolutionResult;
7
7
  export declare function buildRunnerScript(defaultNodePath: string): string;
8
8
  export declare function buildWindowsRunnerScript(defaultNodePath: string): string;
9
+ export declare function buildWindowsPowerShellRunnerScript(defaultNodePath: string): string;
@@ -235,3 +235,64 @@ echo {}
235
235
  exit /b 0
236
236
  `;
237
237
  }
238
+ export function buildWindowsPowerShellRunnerScript(defaultNodePath) {
239
+ const encodedDefault = Buffer.from(defaultNodePath, 'utf8').toString('base64');
240
+ return `$ErrorActionPreference = 'Stop'
241
+
242
+ $DefaultNode = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${encodedDefault}'))
243
+ $HookName = if ($args.Count -gt 0) { [string] $args[0] } else { '' }
244
+ $HookArguments = if ($args.Count -gt 1) { @($args[1..($args.Count - 1)]) } else { @() }
245
+
246
+ function Resolve-Node {
247
+ if ($DefaultNode -and (Test-Path -LiteralPath $DefaultNode -PathType Leaf)) {
248
+ return $DefaultNode
249
+ }
250
+
251
+ $PathNode = Get-Command node.exe -CommandType Application -ErrorAction SilentlyContinue
252
+ if ($null -ne $PathNode) {
253
+ return $PathNode.Source
254
+ }
255
+
256
+ if ($env:USERPROFILE) {
257
+ $NvmRoot = Join-Path $env:USERPROFILE '.nvm'
258
+ if (Test-Path -LiteralPath $NvmRoot -PathType Container) {
259
+ foreach ($VersionDirectory in Get-ChildItem -LiteralPath $NvmRoot -Directory | Sort-Object Name -Descending) {
260
+ $Candidate = Join-Path $VersionDirectory.FullName 'node.exe'
261
+ if (Test-Path -LiteralPath $Candidate -PathType Leaf) {
262
+ return $Candidate
263
+ }
264
+ }
265
+ }
266
+ }
267
+
268
+ return $null
269
+ }
270
+
271
+ function Fail-Audit {
272
+ [Console]::Error.WriteLine('belay: unable to resolve Node for audit hook')
273
+ [Console]::Out.WriteLine('{}')
274
+ exit 0
275
+ }
276
+
277
+ if (-not $HookName) {
278
+ Fail-Audit
279
+ }
280
+
281
+ $NodeBin = Resolve-Node
282
+ if (-not $NodeBin) {
283
+ if ($HookName -eq 'belay-before-submit') {
284
+ [Console]::Out.WriteLine('{"continue":false,"user_message":"belay could not resolve Node. Install or expose Node, run belay doctor, then retry."}')
285
+ exit 0
286
+ }
287
+ if ($HookName -in @('belay-shell-gate', 'belay-tool-gate')) {
288
+ [Console]::Out.WriteLine('{"permission":"deny","user_message":"belay could not resolve Node. Install or expose Node, run belay doctor, then retry."}')
289
+ exit 0
290
+ }
291
+ Fail-Audit
292
+ }
293
+
294
+ $ScriptPath = Join-Path $PSScriptRoot "$HookName.mjs"
295
+ & $NodeBin $ScriptPath @HookArguments
296
+ exit $LASTEXITCODE
297
+ `;
298
+ }
@@ -1,7 +1,10 @@
1
+ import type { CursorHookOrigin } from './adapters/cursor/hook-router.js';
2
+ import type { AdapterName } from './adapters/layouts/types.js';
1
3
  import type { BelayConfigV3 } from './core/config.js';
2
4
  export declare function renderConfig(config: BelayConfigV3): string;
3
- export declare function renderBeforeSubmitHook(): string;
4
- export declare function renderShellGateHook(): string;
5
- export declare function renderToolGateHook(): string;
6
- export declare function renderAuditHook(): string;
5
+ export declare function renderBeforeSubmitHook(adapter: AdapterName, cursorOrigin?: CursorHookOrigin): string;
6
+ export declare function renderShellGateHook(adapter: AdapterName, cursorOrigin?: CursorHookOrigin): string;
7
+ export declare function renderToolGateHook(adapter: AdapterName, cursorOrigin?: CursorHookOrigin): string;
8
+ export declare function renderAuditHook(adapter: AdapterName, cursorOrigin?: CursorHookOrigin): string;
9
+ export declare function renderCursorDispatcher(): Promise<string>;
7
10
  export declare function renderRuntimeCore(adapter?: 'cursor' | 'claude' | 'codex'): Promise<string>;
package/dist/templates.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
+ import { CURSOR_DISPATCHER_GENERATION_HEADER } from './adapters/cursor/dispatcher-generation.js';
4
5
  import { hashValue } from './core/fingerprint.js';
5
6
  import { PACKAGE_VERSION } from './version.js';
6
7
  function inlineJson(value) {
@@ -9,26 +10,54 @@ function inlineJson(value) {
9
10
  export function renderConfig(config) {
10
11
  return `${inlineJson(config)}\n`;
11
12
  }
12
- export function renderBeforeSubmitHook() {
13
+ function requireCursorOrigin(origin) {
14
+ if (!origin) {
15
+ throw new Error('Cursor hook origin is required when rendering dispatcher shims.');
16
+ }
17
+ return origin;
18
+ }
19
+ function renderCursorDispatchHook(origin, kind, eventName) {
20
+ return `import { dispatchCursorHook } from '../belay/runtime/dispatcher.mjs'
21
+
22
+ await dispatchCursorHook({
23
+ origin: ${JSON.stringify(origin)},
24
+ kind: ${JSON.stringify(kind)},
25
+ eventName: ${eventName},
26
+ })
27
+ `;
28
+ }
29
+ export function renderBeforeSubmitHook(adapter, cursorOrigin) {
30
+ if (adapter === 'cursor') {
31
+ return renderCursorDispatchHook(requireCursorOrigin(cursorOrigin), 'before-submit', JSON.stringify('beforeSubmitPrompt'));
32
+ }
13
33
  return `import { runBeforeSubmitPromptHook } from '../belay/runtime/core.mjs'
14
34
 
15
35
  await runBeforeSubmitPromptHook()
16
36
  `;
17
37
  }
18
- export function renderShellGateHook() {
38
+ export function renderShellGateHook(adapter, cursorOrigin) {
39
+ if (adapter === 'cursor') {
40
+ return renderCursorDispatchHook(requireCursorOrigin(cursorOrigin), 'shell-gate', JSON.stringify('beforeShellExecution'));
41
+ }
19
42
  return `import { runShellGateHook } from '../belay/runtime/core.mjs'
20
43
 
21
44
  await runShellGateHook()
22
45
  `;
23
46
  }
24
- export function renderToolGateHook() {
47
+ export function renderToolGateHook(adapter, cursorOrigin) {
48
+ if (adapter === 'cursor') {
49
+ return renderCursorDispatchHook(requireCursorOrigin(cursorOrigin), 'tool-gate', "process.argv[2] ?? 'preToolUse'");
50
+ }
25
51
  return `import { runToolGateHook } from '../belay/runtime/core.mjs'
26
52
 
27
53
  const eventName = process.argv[2] ?? 'preToolUse'
28
54
  await runToolGateHook(eventName)
29
55
  `;
30
56
  }
31
- export function renderAuditHook() {
57
+ export function renderAuditHook(adapter, cursorOrigin) {
58
+ if (adapter === 'cursor') {
59
+ return renderCursorDispatchHook(requireCursorOrigin(cursorOrigin), 'audit', "process.argv[2] ?? 'postToolUse'");
60
+ }
32
61
  return `import { runAuditHook } from '../belay/runtime/core.mjs'
33
62
 
34
63
  const eventName = process.argv[2] ?? 'postToolUse'
@@ -44,6 +73,15 @@ async function readRuntimeBundle(adapter = 'cursor') {
44
73
  throw new Error('Runtime bundle missing. Run pnpm build before belay init or upgrade.');
45
74
  }
46
75
  }
76
+ export async function renderCursorDispatcher() {
77
+ const bundlePath = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'dist', 'bundle', 'cursor-dispatcher.mjs');
78
+ try {
79
+ return `${CURSOR_DISPATCHER_GENERATION_HEADER}${await readFile(bundlePath, 'utf8')}`;
80
+ }
81
+ catch {
82
+ throw new Error('Runtime bundle missing. Run pnpm build before belay init or upgrade.');
83
+ }
84
+ }
47
85
  export async function renderRuntimeCore(adapter = 'cursor') {
48
86
  const bundle = await readRuntimeBundle(adapter);
49
87
  const withoutStamp = bundle
package/dist/types.d.ts CHANGED
@@ -1,12 +1,13 @@
1
1
  export type { BelayConfig, BelayConfigV1, BelayConfigV2, BelayConfigV3, BelayControlPlaneConfig, BelayEgressConfig, BelayOverridesConfig, BelayPolicyConfig, BelayRedactionConfig, BelaySandboxConfig, BelayTransactionalConfig, UnknownLocalEffectPolicy, } from './core/config.js';
2
2
  export type { ApprovalRecord, ApprovalStateFile, Assessment, BelayMode, ClassifyResult, HookVerdict, } from './core/types.js';
3
3
  import type { InstallScope } from './adapters/layouts/scope.js';
4
- import type { RecentAskEntry } from './core/audit-summary.js';
4
+ import type { RecentAskEntry, RecentHostDenialEntry } from './core/audit-summary.js';
5
5
  import type { BelayEgressConfig, BelayOverridesConfig, BelayPolicyConfig, BelaySandboxConfig } from './core/config.js';
6
6
  import type { ApprovalRecord, ClassifyResult } from './core/types.js';
7
7
  export interface HookEntry {
8
8
  command: string;
9
9
  matcher?: string;
10
+ failClosed?: boolean;
10
11
  }
11
12
  export interface HooksFile {
12
13
  version: number;
@@ -42,6 +43,32 @@ export interface UpgradeOptions {
42
43
  /** Opt-in: migrate implicit factory-default ollama judge to host default provider. */
43
44
  migrateJudgeDefault?: boolean;
44
45
  }
46
+ export interface UninstallOptions {
47
+ targetDir?: string;
48
+ adapter?: AdapterName;
49
+ scope?: InstallScope;
50
+ }
51
+ export interface WhereOptions {
52
+ targetDir?: string;
53
+ adapter?: AdapterName;
54
+ scope?: InstallScope;
55
+ json?: boolean;
56
+ }
57
+ export interface WhereReport {
58
+ cwd: string;
59
+ repoRoot: string;
60
+ adapter: AdapterName;
61
+ installScope: 'project' | 'global';
62
+ configPresent: boolean;
63
+ cliExecutable?: string;
64
+ cliPackageRoot: string;
65
+ configPath: string;
66
+ hooksSettingsPath: string;
67
+ hooksDir: string;
68
+ runtimeDir: string;
69
+ skillsDir: string;
70
+ commandsDir?: string;
71
+ }
45
72
  export interface DoctorOptions {
46
73
  targetDir?: string;
47
74
  fix?: boolean;
@@ -88,6 +115,10 @@ export interface StatusOptions {
88
115
  export interface HealthSnapshotOptions {
89
116
  targetDir?: string;
90
117
  adapter?: AdapterName;
118
+ /** Override the host home directory when inspecting adapter-level execution settings. */
119
+ homeDir?: string;
120
+ /** Override Cursor config-path environment variables for deterministic host inspection. */
121
+ cursorConfigEnv?: Partial<Pick<NodeJS.ProcessEnv, 'CURSOR_CONFIG_DIR' | 'XDG_CONFIG_HOME'>>;
91
122
  }
92
123
  export interface HealthSnapshot {
93
124
  repoRoot: string;
@@ -172,6 +203,9 @@ export interface AuditVisibilityReport {
172
203
  allowCount: number;
173
204
  silentPassRate: number;
174
205
  recentAsks: RecentAskEntry[];
206
+ hostDeniedAfterAllowCount?: number;
207
+ recentHostDenials?: RecentHostDenialEntry[];
208
+ unrecognizedHostFailureCount?: number;
175
209
  warnings: string[];
176
210
  notes: string[];
177
211
  }
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const PACKAGE_VERSION = "0.9.2";
1
+ export declare const PACKAGE_VERSION = "0.9.4";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // Generated by scripts/sync-version.mjs — do not edit.
2
- export const PACKAGE_VERSION = '0.9.2';
2
+ export const PACKAGE_VERSION = '0.9.4';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guilz-dev/belay",
3
- "version": "0.9.2",
3
+ "version": "0.9.4",
4
4
  "description": "Belay-style approval and audit gating for agent runtimes.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -57,10 +57,13 @@
57
57
  "typecheck": "tsc --noEmit",
58
58
  "test": "pnpm build && vitest run",
59
59
  "test:structural": "pnpm build && vitest run src/__tests__/verdict/structural-suite.test.ts",
60
- "test:docker": "pnpm build && vitest run src/__tests__/capability/boundary-container-isolation.test.ts src/__tests__/capability/boundary-driver-container.test.ts",
60
+ "test:docker": "pnpm build && vitest run --config vitest.live.config.ts src/__tests__/capability/boundary-container-isolation.test.ts src/__tests__/capability/boundary-container-workspace-mount.test.ts src/__tests__/capability/boundary-driver-container.test.ts src/__tests__/contained-execution-docker.integration.test.ts",
61
+ "test:llm": "pnpm build && vitest run --config vitest.live.config.ts src/__tests__/verdict/llm/judge-accuracy.test.ts",
61
62
  "test:stable": "pnpm build && vitest run && vitest run && vitest run",
62
63
  "corpus": "pnpm build && node scripts/corpus.mjs",
63
64
  "probe:adversarial": "pnpm build && node scripts/adversarial-probe.mjs",
65
+ "probe:cursor-shell-rewrite": "node scripts/cursor-shell-rewrite-probe.mjs",
66
+ "probe:native-seatbelt-boundary": "node scripts/native-seatbelt-boundary-probe.mjs --live",
64
67
  "probe:coverage": "pnpm build && node scripts/coverage-probe.mjs",
65
68
  "probe:coverage:repeat": "pnpm build && node scripts/coverage-probe.mjs --repeat 3",
66
69
  "corpus:ratchet": "pnpm build && node scripts/corpus-ratchet.mjs"
@@ -54,6 +54,11 @@ To restore the legacy two-step UX (approve, then always retry manually), set
54
54
  For why it was blocked, use `/belay why <command>` or `belay explain --command "<command>"`.
55
55
  For the latest pending ask, use `/belay explain` or `belay explain`.
56
56
 
57
+ If `belay status` or `belay report` shows `Host denied after Belay allow`, the denial came from
58
+ the editor/agent host after Belay returned allow. There is no Belay approval ID to approve. Keep
59
+ host protection in Belay audit mode and approve only the exact host prompt when execution is
60
+ intended.
61
+
57
62
  **Do not use command allowlists** (`overrides.allow`) or legacy standing-allow records
58
63
  (shell, tool, or subagent) to fix blocks; none of them change runtime authorization.
59
64
  Improve EffectPlan semantics, approve once with `/belay-approve`, or use an exact
@@ -1,7 +1,10 @@
1
- Show audit visibility: ask/flag/allow counts, silent-pass rate, and recent asks.
1
+ Show audit visibility: ask/flag/allow counts, silent-pass rate, recent asks, and host
2
+ `permission_denied` failures correlated to a preceding Belay allow.
2
3
 
3
4
  ```bash
4
5
  belay report
5
6
  ```
6
7
 
7
8
  For full install health plus audit summary, use `belay status`.
9
+
10
+ `Host denied after Belay allow` is host-policy telemetry, not a pending Belay approval.