@goodfoot/agent-hooks 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +137 -0
  2. package/dist/agents/antigravity/events.js +42 -0
  3. package/dist/agents/antigravity/hooks.js +59 -0
  4. package/dist/agents/antigravity/index.js +28 -0
  5. package/dist/agents/antigravity/outputs.js +81 -0
  6. package/dist/agents/antigravity/transport.js +84 -0
  7. package/dist/agents/antigravity/types.js +11 -0
  8. package/dist/agents/claude-code/env.js +27 -0
  9. package/dist/agents/claude-code/events.js +87 -0
  10. package/dist/agents/claude-code/hooks.js +1000 -0
  11. package/dist/agents/claude-code/index.js +57 -0
  12. package/dist/agents/claude-code/outputs.js +579 -0
  13. package/dist/agents/claude-code/tool-helpers.js +840 -0
  14. package/dist/agents/claude-code/transport.js +254 -0
  15. package/dist/agents/claude-code/types.js +11 -0
  16. package/dist/agents/codex/cli-support.js +318 -0
  17. package/dist/agents/codex/constants.js +29 -0
  18. package/dist/agents/codex/events.js +68 -0
  19. package/dist/agents/codex/hooks.js +93 -0
  20. package/dist/agents/codex/index.js +21 -0
  21. package/dist/agents/codex/outputs.js +189 -0
  22. package/dist/agents/codex/scaffold.js +273 -0
  23. package/dist/agents/codex/transport.js +139 -0
  24. package/dist/agents/codex/types.js +1 -0
  25. package/dist/cli.js +1357 -0
  26. package/dist/constants.js +39 -0
  27. package/dist/core/define-hook.js +61 -0
  28. package/dist/core/env.js +186 -0
  29. package/dist/core/logger.js +508 -0
  30. package/dist/core/stdin.js +38 -0
  31. package/dist/core/transport.js +231 -0
  32. package/dist/core/types.js +10 -0
  33. package/dist/index.js +17 -0
  34. package/dist/scaffold.js +512 -0
  35. package/package.json +61 -0
  36. package/types/agents/antigravity/events.d.ts +50 -0
  37. package/types/agents/antigravity/hooks.d.ts +76 -0
  38. package/types/agents/antigravity/index.d.ts +27 -0
  39. package/types/agents/antigravity/outputs.d.ts +107 -0
  40. package/types/agents/antigravity/transport.d.ts +47 -0
  41. package/types/agents/antigravity/types.d.ts +58 -0
  42. package/types/agents/claude-code/env.d.ts +27 -0
  43. package/types/agents/claude-code/events.d.ts +69 -0
  44. package/types/agents/claude-code/hooks.d.ts +1182 -0
  45. package/types/agents/claude-code/index.d.ts +23 -0
  46. package/types/agents/claude-code/outputs.d.ts +1160 -0
  47. package/types/agents/claude-code/tool-helpers.d.ts +807 -0
  48. package/types/agents/claude-code/transport.d.ts +119 -0
  49. package/types/agents/claude-code/types.d.ts +557 -0
  50. package/types/agents/codex/cli-support.d.ts +105 -0
  51. package/types/agents/codex/constants.d.ts +10 -0
  52. package/types/agents/codex/events.d.ts +63 -0
  53. package/types/agents/codex/hooks.d.ts +135 -0
  54. package/types/agents/codex/index.d.ts +19 -0
  55. package/types/agents/codex/outputs.d.ts +190 -0
  56. package/types/agents/codex/scaffold.d.ts +14 -0
  57. package/types/agents/codex/transport.d.ts +74 -0
  58. package/types/agents/codex/types.d.ts +131 -0
  59. package/types/cli.d.ts +315 -0
  60. package/types/constants.d.ts +9 -0
  61. package/types/core/define-hook.d.ts +39 -0
  62. package/types/core/env.d.ts +150 -0
  63. package/types/core/logger.d.ts +476 -0
  64. package/types/core/stdin.d.ts +21 -0
  65. package/types/core/transport.d.ts +131 -0
  66. package/types/core/types.d.ts +137 -0
  67. package/types/index.d.ts +18 -0
  68. package/types/scaffold.d.ts +46 -0
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Typed factory functions for Codex hooks, built on the shared core
3
+ * {@link ../core/define-hook.ts|defineHook}.
4
+ *
5
+ * Public surface mirrors the source codex-hooks package 1.3.0: same factory names,
6
+ * config shapes (`MatcherHookConfig`/`NoMatcherHookConfig`, including
7
+ * `statusMessage`), result unions, and `hookEventName` metadata property.
8
+ * Two deliberate additions carry the consolidation contract:
9
+ *
10
+ * - every factory injects the advisory allow-list policy gate, so
11
+ * `unexpectedError: "continue"` fails closed at factory-call time for any
12
+ * event not on {@link ADVISORY_EVENTS};
13
+ * - non-advisory events' config types narrow the policy field via
14
+ * {@link MatcherHookConfigFor}/{@link NoMatcherHookConfigFor}, rejecting
15
+ * `"continue"` at compile time as well.
16
+ * @module
17
+ */
18
+ import { defineHook } from "../../core/define-hook.js";
19
+ import { ADVISORY_EVENTS } from "./events.js";
20
+ // ============================================================================
21
+ // Generic Factory
22
+ // ============================================================================
23
+ /**
24
+ * The runtime policy gate injected into every Codex factory: rejects
25
+ * `unexpectedError: "continue"` at factory-call time for any event not on
26
+ * the advisory allow-list ({@link ADVISORY_EVENTS}). Mirrors the Claude Code
27
+ * surface's gate exactly — one closure serves all ten events because the
28
+ * gate receives the event name.
29
+ */
30
+ const advisoryPolicyGate = (eventName, policy) => policy !== "continue" || ADVISORY_EVENTS.includes(eventName);
31
+ /**
32
+ * Creates a Codex hook function for a specific event on the shared core.
33
+ * Binds the event name, injects the advisory allow-list policy gate, and
34
+ * carries the Codex-specific `statusMessage` through to the returned
35
+ * metadata.
36
+ * @internal
37
+ */
38
+ function createHookFunction(hookEventName, config, handler) {
39
+ const coreConfig = {
40
+ matcher: "matcher" in config ? config.matcher : undefined,
41
+ timeout: config.timeout,
42
+ unexpectedError: config.unexpectedError,
43
+ onUnexpectedError: config.onUnexpectedError,
44
+ };
45
+ const hookFn = defineHook(hookEventName, coreConfig, handler, advisoryPolicyGate);
46
+ const codexFn = hookFn;
47
+ codexFn.hookEventName = hookEventName;
48
+ codexFn.statusMessage = config.statusMessage;
49
+ return codexFn;
50
+ }
51
+ // ============================================================================
52
+ // Factories (signatures copied from the source package; configs narrowed)
53
+ // ============================================================================
54
+ /** Creates a PreToolUse hook handler. Excluded from the advisory list. */
55
+ export function preToolUseHook(config, handler) {
56
+ return createHookFunction("PreToolUse", config, handler);
57
+ }
58
+ /** Creates a PostToolUse hook handler. Blocking variant excluded from the advisory list. */
59
+ export function postToolUseHook(config, handler) {
60
+ return createHookFunction("PostToolUse", config, handler);
61
+ }
62
+ /** Creates a PermissionRequest hook handler. Excluded from the advisory list. */
63
+ export function permissionRequestHook(config, handler) {
64
+ return createHookFunction("PermissionRequest", config, handler);
65
+ }
66
+ /** Creates a UserPromptSubmit hook handler. Advisory enrichment hook. */
67
+ export function userPromptSubmitHook(config, handler) {
68
+ return createHookFunction("UserPromptSubmit", config, handler);
69
+ }
70
+ /** Creates a SessionStart hook handler. Advisory enrichment hook. */
71
+ export function sessionStartHook(config, handler) {
72
+ return createHookFunction("SessionStart", config, handler);
73
+ }
74
+ /** Creates a SubagentStart hook handler. Advisory enrichment hook. */
75
+ export function subagentStartHook(config, handler) {
76
+ return createHookFunction("SubagentStart", config, handler);
77
+ }
78
+ /** Creates a Stop hook handler. Blocking variant excluded from the advisory list. */
79
+ export function stopHook(config, handler) {
80
+ return createHookFunction("Stop", config, handler);
81
+ }
82
+ /** Creates a SubagentStop hook handler. Blocking variant excluded from the advisory list. */
83
+ export function subagentStopHook(config, handler) {
84
+ return createHookFunction("SubagentStop", config, handler);
85
+ }
86
+ /** Creates a PreCompact hook handler. Excluded by default pending doc clarification. */
87
+ export function preCompactHook(config, handler) {
88
+ return createHookFunction("PreCompact", config, handler);
89
+ }
90
+ /** Creates a PostCompact hook handler. Excluded by default pending doc clarification. */
91
+ export function postCompactHook(config, handler) {
92
+ return createHookFunction("PostCompact", config, handler);
93
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * `@goodfoot/agent-hooks/codex` — the Codex agent surface.
3
+ *
4
+ * Re-exports all 10 hook factories, their output builders (reserved fields
5
+ * emitted only when passed), the `BlockError` block signal, input/output
6
+ * types, constants, the advisory allow-list events module, Logger, and the
7
+ * runtime `execute` entry point. No default export.
8
+ * @module
9
+ */
10
+ // Logger exports
11
+ export { LOG_LEVELS, Logger, logger } from "../../core/logger.js";
12
+ // Constants
13
+ export { DEFAULT_ESBUILD_LOADERS, DEFAULT_TIMEOUT_MS, EVENTS_WITH_MATCHER, EVENTS_WITH_TEXT_OUTPUT, HOOK_FACTORY_TO_EVENT, PACKAGE_NAME, } from "./constants.js";
14
+ // Advisory allow-list events
15
+ export { ADVISORY_EVENTS, EXCLUDED_FROM_ADVISORY, HOOK_EVENT_NAMES } from "./events.js";
16
+ // Hook factory functions - all 10 event types
17
+ export { permissionRequestHook, postCompactHook, postToolUseHook, preCompactHook, preToolUseHook, sessionStartHook, stopHook, subagentStartHook, subagentStopHook, userPromptSubmitHook, } from "./hooks.js";
18
+ // Output types and builders (BlockError subclasses core HookBlockError)
19
+ export { BlockError, EXIT_CODES, permissionRequestOutput, postCompactOutput, postToolUseOutput, preCompactOutput, preToolUseLegacyBlockOutput, preToolUseOutput, rawOutput, sessionStartOutput, stopOutput, subagentStartOutput, subagentStopOutput, userPromptSubmitOutput, } from "./outputs.js";
20
+ // Runtime exports - execute function and wire conversion
21
+ export { convertToHookOutput, createCodexTransport, execute } from "./transport.js";
@@ -0,0 +1,189 @@
1
+ import { HookBlockError } from "../../core/transport.js";
2
+ export const EXIT_CODES = {
3
+ SUCCESS: 0,
4
+ ERROR: 1,
5
+ BLOCK: 2,
6
+ };
7
+ /**
8
+ * The Codex block signal, carried into the new package as a subclass of the
9
+ * shared core {@link ../core/transport.ts|HookBlockError} so `drive()`
10
+ * classifies it **before** consulting `unexpectedError` policy — carrying an
11
+ * ordering the source runtime already implemented (`runtime.ts` tested
12
+ * `instanceof BlockError` ahead of the policy branch). Public surface is
13
+ * preserved: `name`, `reason`, and `instanceof BlockError` semantics are
14
+ * unchanged, and `instanceof HookBlockError` now also holds.
15
+ */
16
+ export class BlockError extends HookBlockError {
17
+ reason;
18
+ constructor(reason) {
19
+ super(reason);
20
+ this.name = "BlockError";
21
+ this.reason = reason;
22
+ }
23
+ }
24
+ function omitUndefined(value) {
25
+ return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
26
+ }
27
+ function buildOutput(type, stdout, stderr) {
28
+ return {
29
+ _type: type,
30
+ stdout: omitUndefined(stdout),
31
+ ...(stderr !== undefined ? { stderr } : {}),
32
+ };
33
+ }
34
+ export function rawOutput(stdout, stderr) {
35
+ return buildOutput("Raw", stdout, stderr);
36
+ }
37
+ export function preToolUseOutput(options = {}) {
38
+ const hasSpecific = options.additionalContext !== undefined ||
39
+ options.permissionDecision !== undefined ||
40
+ options.permissionDecisionReason !== undefined ||
41
+ options.updatedInput !== undefined;
42
+ const hookSpecificOutput = hasSpecific
43
+ ? omitUndefined({
44
+ hookEventName: "PreToolUse",
45
+ additionalContext: options.additionalContext,
46
+ permissionDecision: options.permissionDecision,
47
+ permissionDecisionReason: options.permissionDecisionReason,
48
+ updatedInput: options.updatedInput,
49
+ })
50
+ : undefined;
51
+ return buildOutput("PreToolUse", {
52
+ continue: options.continue,
53
+ stopReason: options.stopReason,
54
+ suppressOutput: options.suppressOutput,
55
+ systemMessage: options.systemMessage,
56
+ decision: options.decision,
57
+ reason: options.reason,
58
+ hookSpecificOutput,
59
+ });
60
+ }
61
+ export function preToolUseLegacyBlockOutput(options) {
62
+ return buildOutput("PreToolUse", {
63
+ systemMessage: options.systemMessage,
64
+ decision: options.decision,
65
+ reason: options.reason,
66
+ });
67
+ }
68
+ export function postToolUseOutput(options = {}) {
69
+ const hasSpecific = options.additionalContext !== undefined || options.updatedMCPToolOutput !== undefined;
70
+ const hookSpecificOutput = hasSpecific
71
+ ? omitUndefined({
72
+ hookEventName: "PostToolUse",
73
+ additionalContext: options.additionalContext,
74
+ updatedMCPToolOutput: options.updatedMCPToolOutput,
75
+ })
76
+ : undefined;
77
+ return buildOutput("PostToolUse", {
78
+ continue: options.continue,
79
+ stopReason: options.stopReason,
80
+ suppressOutput: options.suppressOutput,
81
+ systemMessage: options.systemMessage,
82
+ decision: options.decision,
83
+ reason: options.reason,
84
+ hookSpecificOutput,
85
+ });
86
+ }
87
+ export function permissionRequestOutput(options) {
88
+ const decision = omitUndefined({
89
+ behavior: options.behavior,
90
+ message: options.message,
91
+ interrupt: options.interrupt,
92
+ updatedInput: options.updatedInput,
93
+ updatedPermissions: options.updatedPermissions,
94
+ });
95
+ const hookSpecificOutput = {
96
+ hookEventName: "PermissionRequest",
97
+ decision,
98
+ };
99
+ return buildOutput("PermissionRequest", {
100
+ continue: options.continue,
101
+ stopReason: options.stopReason,
102
+ suppressOutput: options.suppressOutput,
103
+ systemMessage: options.systemMessage,
104
+ hookSpecificOutput,
105
+ });
106
+ }
107
+ export function userPromptSubmitOutput(options = {}) {
108
+ const hookSpecificOutput = options.additionalContext !== undefined
109
+ ? {
110
+ hookEventName: "UserPromptSubmit",
111
+ additionalContext: options.additionalContext,
112
+ }
113
+ : undefined;
114
+ return buildOutput("UserPromptSubmit", {
115
+ continue: options.continue,
116
+ stopReason: options.stopReason,
117
+ suppressOutput: options.suppressOutput,
118
+ systemMessage: options.systemMessage,
119
+ decision: options.decision,
120
+ reason: options.reason,
121
+ hookSpecificOutput,
122
+ });
123
+ }
124
+ export function sessionStartOutput(options = {}) {
125
+ const hookSpecificOutput = options.additionalContext !== undefined
126
+ ? {
127
+ hookEventName: "SessionStart",
128
+ additionalContext: options.additionalContext,
129
+ }
130
+ : undefined;
131
+ return buildOutput("SessionStart", {
132
+ continue: options.continue,
133
+ stopReason: options.stopReason,
134
+ suppressOutput: options.suppressOutput,
135
+ systemMessage: options.systemMessage,
136
+ hookSpecificOutput,
137
+ });
138
+ }
139
+ export function subagentStartOutput(options = {}) {
140
+ const hookSpecificOutput = options.additionalContext !== undefined
141
+ ? {
142
+ hookEventName: "SubagentStart",
143
+ additionalContext: options.additionalContext,
144
+ }
145
+ : undefined;
146
+ return buildOutput("SubagentStart", {
147
+ continue: options.continue,
148
+ stopReason: options.stopReason,
149
+ suppressOutput: options.suppressOutput,
150
+ systemMessage: options.systemMessage,
151
+ hookSpecificOutput,
152
+ });
153
+ }
154
+ export function stopOutput(options = {}) {
155
+ return buildOutput("Stop", {
156
+ continue: options.continue,
157
+ stopReason: options.stopReason,
158
+ suppressOutput: options.suppressOutput,
159
+ systemMessage: options.systemMessage,
160
+ decision: options.decision,
161
+ reason: options.reason,
162
+ });
163
+ }
164
+ export function subagentStopOutput(options = {}) {
165
+ return buildOutput("SubagentStop", {
166
+ continue: options.continue,
167
+ stopReason: options.stopReason,
168
+ suppressOutput: options.suppressOutput,
169
+ systemMessage: options.systemMessage,
170
+ decision: options.decision,
171
+ reason: options.reason,
172
+ });
173
+ }
174
+ export function preCompactOutput(options = {}) {
175
+ return buildOutput("PreCompact", {
176
+ continue: options.continue,
177
+ stopReason: options.stopReason,
178
+ suppressOutput: options.suppressOutput,
179
+ systemMessage: options.systemMessage,
180
+ });
181
+ }
182
+ export function postCompactOutput(options = {}) {
183
+ return buildOutput("PostCompact", {
184
+ continue: options.continue,
185
+ stopReason: options.stopReason,
186
+ suppressOutput: options.suppressOutput,
187
+ systemMessage: options.systemMessage,
188
+ });
189
+ }
@@ -0,0 +1,273 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { HOOK_FACTORY_TO_EVENT, PACKAGE_NAME } from "./constants.js";
4
+ const VALID_HOOK_EVENT_NAMES = new Set(Object.values(HOOK_FACTORY_TO_EVENT));
5
+ const EVENT_LOOKUP = new Map(Array.from(VALID_HOOK_EVENT_NAMES).map((value) => [value.toLowerCase(), value]));
6
+ const EVENT_TO_FACTORY = Object.fromEntries(Object.entries(HOOK_FACTORY_TO_EVENT).map(([factory, eventName]) => [eventName, factory]));
7
+ const EVENT_TO_OUTPUT = {
8
+ PreToolUse: "preToolUseOutput",
9
+ PostToolUse: "postToolUseOutput",
10
+ PermissionRequest: "permissionRequestOutput",
11
+ UserPromptSubmit: "userPromptSubmitOutput",
12
+ SessionStart: "sessionStartOutput",
13
+ SubagentStart: "subagentStartOutput",
14
+ Stop: "stopOutput",
15
+ SubagentStop: "subagentStopOutput",
16
+ PreCompact: "preCompactOutput",
17
+ PostCompact: "postCompactOutput",
18
+ };
19
+ export function validateHookNames(hookNames) {
20
+ const normalized = [];
21
+ const invalid = [];
22
+ for (const hookName of hookNames) {
23
+ const eventName = EVENT_LOOKUP.get(hookName.toLowerCase());
24
+ if (eventName === undefined) {
25
+ invalid.push(hookName);
26
+ }
27
+ else {
28
+ normalized.push(eventName);
29
+ }
30
+ }
31
+ if (invalid.length > 0) {
32
+ return {
33
+ valid: false,
34
+ error: `Invalid hook names: ${invalid.join(", ")}. Valid names: ${Array.from(VALID_HOOK_EVENT_NAMES).join(", ")}`,
35
+ };
36
+ }
37
+ return { valid: true, normalized };
38
+ }
39
+ function toKebabCase(value) {
40
+ return value
41
+ .replace(/([a-z])([A-Z])/g, "$1-$2")
42
+ .replace(/([A-Z])([A-Z][a-z])/g, "$1-$2")
43
+ .toLowerCase();
44
+ }
45
+ function generatePackageJson(projectName, outputPath) {
46
+ return `${JSON.stringify({
47
+ name: projectName,
48
+ version: "1.0.0",
49
+ type: "module",
50
+ scripts: {
51
+ build: `agent-hooks --agent codex -i "src/**/*.ts" -o "${outputPath}"`,
52
+ test: "vitest run",
53
+ lint: "biome check .",
54
+ typecheck: "tsc --noEmit",
55
+ },
56
+ dependencies: {
57
+ [PACKAGE_NAME]: "^1.0.0",
58
+ },
59
+ devDependencies: {
60
+ "@biomejs/biome": "2.4.9",
61
+ "@types/node": "^24.0.0",
62
+ typescript: "^5.9.3",
63
+ vitest: "^4.0.13",
64
+ },
65
+ engines: {
66
+ node: ">=20.11.0",
67
+ },
68
+ }, null, 2)}\n`;
69
+ }
70
+ function generateTsConfig() {
71
+ return `${JSON.stringify({
72
+ compilerOptions: {
73
+ target: "ES2022",
74
+ module: "NodeNext",
75
+ moduleResolution: "NodeNext",
76
+ strict: true,
77
+ esModuleInterop: true,
78
+ skipLibCheck: true,
79
+ },
80
+ include: ["src/**/*.ts", "test/**/*.ts"],
81
+ exclude: ["dist", "node_modules"],
82
+ }, null, 2)}\n`;
83
+ }
84
+ function generateBiomeConfig() {
85
+ return `{
86
+ "$schema": "https://biomejs.dev/schemas/2.4.9/schema.json",
87
+ "formatter": {
88
+ "enabled": true,
89
+ "indentStyle": "space",
90
+ "indentWidth": 2,
91
+ "lineWidth": 120
92
+ },
93
+ "linter": {
94
+ "enabled": true,
95
+ "rules": {
96
+ "recommended": true
97
+ }
98
+ }
99
+ }
100
+ `;
101
+ }
102
+ function generateVitestConfig() {
103
+ return `import { defineConfig } from "vitest/config";
104
+
105
+ export default defineConfig({
106
+ test: {
107
+ include: ["test/**/*.test.ts"],
108
+ },
109
+ });
110
+ `;
111
+ }
112
+ function generateReadme(projectName, hooks) {
113
+ const hookList = hooks.map((hookName) => `\`${hookName}\``).join(", ");
114
+ return `# ${projectName}
115
+
116
+ This project contains Codex hooks built with \`${PACKAGE_NAME}\`. The scaffold is intentionally narrow: it targets only the five synchronous command hook events Codex currently supports. This project includes: ${hookList}.
117
+
118
+ Run \`npm install\`, then \`npm run build\` to compile the hooks and generate \`hooks.json\`. Use \`npm test\` to run the starter tests.
119
+ `;
120
+ }
121
+ function generateGitignore() {
122
+ return `dist/
123
+ node_modules/
124
+ *.log
125
+ `;
126
+ }
127
+ function generateHookSource(eventName) {
128
+ const factory = EVENT_TO_FACTORY[eventName];
129
+ const output = EVENT_TO_OUTPUT[eventName];
130
+ const importItems = [factory, output].join(", ");
131
+ switch (eventName) {
132
+ case "PreToolUse":
133
+ return `import { ${importItems} } from "${PACKAGE_NAME}";
134
+
135
+ export default ${factory}({ matcher: "Bash" }, (input) => {
136
+ if (input.tool_input.command.includes("rm -rf")) {
137
+ return ${output}({
138
+ systemMessage: "Blocked dangerous shell command.",
139
+ permissionDecision: "deny",
140
+ permissionDecisionReason: "Refusing destructive Bash command.",
141
+ });
142
+ }
143
+ });
144
+ `;
145
+ case "PostToolUse":
146
+ return `import { ${importItems} } from "${PACKAGE_NAME}";
147
+
148
+ export default ${factory}({ matcher: "Bash" }, (input) => {
149
+ return ${output}({
150
+ additionalContext: \`Observed Bash command: \${input.tool_input.command}\`,
151
+ });
152
+ });
153
+ `;
154
+ case "SessionStart":
155
+ return `import { ${importItems} } from "${PACKAGE_NAME}";
156
+
157
+ export default ${factory}({ matcher: "startup" }, () => {
158
+ return ${output}({
159
+ additionalContext: "Project-specific Codex hook context loaded.",
160
+ });
161
+ });
162
+ `;
163
+ case "UserPromptSubmit":
164
+ return `import { ${importItems} } from "${PACKAGE_NAME}";
165
+
166
+ export default ${factory}({}, (input) => {
167
+ if (input.prompt.trim().length === 0) {
168
+ return ${output}({
169
+ decision: "block",
170
+ reason: "Prompt must not be empty.",
171
+ });
172
+ }
173
+ });
174
+ `;
175
+ case "Stop":
176
+ return `import { ${importItems} } from "${PACKAGE_NAME}";
177
+
178
+ export default ${factory}({}, (input) => {
179
+ if (input.stop_hook_active && input.last_assistant_message === null) {
180
+ return ${output}({
181
+ continue: true,
182
+ systemMessage: "Stop hook observed an empty assistant message.",
183
+ });
184
+ }
185
+ });
186
+ `;
187
+ case "PermissionRequest":
188
+ return `import { ${importItems} } from "${PACKAGE_NAME}";
189
+
190
+ export default ${factory}({ matcher: "Bash" }, (input) => {
191
+ return ${output}({
192
+ behavior: "allow",
193
+ message: \`Allowed \${input.tool_name} via PermissionRequest hook.\`,
194
+ });
195
+ });
196
+ `;
197
+ case "SubagentStart":
198
+ return `import { ${importItems} } from "${PACKAGE_NAME}";
199
+
200
+ export default ${factory}({ matcher: ".*" }, (input) => {
201
+ return ${output}({
202
+ additionalContext: \`Subagent \${input.agent_type} (\${input.agent_id}) starting.\`,
203
+ });
204
+ });
205
+ `;
206
+ case "SubagentStop":
207
+ return `import { ${importItems} } from "${PACKAGE_NAME}";
208
+
209
+ export default ${factory}({ matcher: ".*" }, (input) => {
210
+ if (input.last_assistant_message === null) {
211
+ return ${output}({
212
+ decision: "block",
213
+ reason: \`Subagent \${input.agent_type} produced no message.\`,
214
+ });
215
+ }
216
+ });
217
+ `;
218
+ case "PreCompact":
219
+ return `import { ${importItems} } from "${PACKAGE_NAME}";
220
+
221
+ export default ${factory}({ matcher: "manual" }, () => {
222
+ return ${output}({
223
+ systemMessage: "Compaction starting.",
224
+ });
225
+ });
226
+ `;
227
+ case "PostCompact":
228
+ return `import { ${importItems} } from "${PACKAGE_NAME}";
229
+
230
+ export default ${factory}({ matcher: "manual" }, () => {
231
+ return ${output}({
232
+ systemMessage: "Compaction complete.",
233
+ });
234
+ });
235
+ `;
236
+ }
237
+ }
238
+ function generateHookTest(eventName) {
239
+ const filename = toKebabCase(eventName);
240
+ return `import hook from "../src/${filename}.js";
241
+ import { describe, expect, it } from "vitest";
242
+
243
+ describe("${eventName} hook", () => {
244
+ it("exports a ${eventName} hook", () => {
245
+ expect(hook.hookEventName).toBe("${eventName}");
246
+ });
247
+ });
248
+ `;
249
+ }
250
+ export function scaffoldProject(options) {
251
+ const validation = validateHookNames(options.hooks);
252
+ if (!validation.valid) {
253
+ throw new Error(validation.error);
254
+ }
255
+ const { directory, outputPath } = options;
256
+ if (fs.existsSync(directory)) {
257
+ throw new Error(`Directory already exists: ${directory}`);
258
+ }
259
+ fs.mkdirSync(path.join(directory, "src"), { recursive: true });
260
+ fs.mkdirSync(path.join(directory, "test"), { recursive: true });
261
+ const projectName = path.basename(directory);
262
+ fs.writeFileSync(path.join(directory, "package.json"), generatePackageJson(projectName, outputPath));
263
+ fs.writeFileSync(path.join(directory, "tsconfig.json"), generateTsConfig());
264
+ fs.writeFileSync(path.join(directory, "biome.json"), generateBiomeConfig());
265
+ fs.writeFileSync(path.join(directory, "vitest.config.ts"), generateVitestConfig());
266
+ fs.writeFileSync(path.join(directory, ".gitignore"), generateGitignore());
267
+ fs.writeFileSync(path.join(directory, "README.md"), generateReadme(projectName, validation.normalized));
268
+ for (const hookName of validation.normalized) {
269
+ const baseName = toKebabCase(hookName);
270
+ fs.writeFileSync(path.join(directory, "src", `${baseName}.ts`), generateHookSource(hookName));
271
+ fs.writeFileSync(path.join(directory, "test", `${baseName}.test.ts`), generateHookTest(hookName));
272
+ }
273
+ }