@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.
- package/README.md +137 -0
- package/dist/agents/antigravity/events.js +42 -0
- package/dist/agents/antigravity/hooks.js +59 -0
- package/dist/agents/antigravity/index.js +28 -0
- package/dist/agents/antigravity/outputs.js +81 -0
- package/dist/agents/antigravity/transport.js +84 -0
- package/dist/agents/antigravity/types.js +11 -0
- package/dist/agents/claude-code/env.js +27 -0
- package/dist/agents/claude-code/events.js +87 -0
- package/dist/agents/claude-code/hooks.js +1000 -0
- package/dist/agents/claude-code/index.js +57 -0
- package/dist/agents/claude-code/outputs.js +579 -0
- package/dist/agents/claude-code/tool-helpers.js +840 -0
- package/dist/agents/claude-code/transport.js +254 -0
- package/dist/agents/claude-code/types.js +11 -0
- package/dist/agents/codex/cli-support.js +318 -0
- package/dist/agents/codex/constants.js +29 -0
- package/dist/agents/codex/events.js +68 -0
- package/dist/agents/codex/hooks.js +93 -0
- package/dist/agents/codex/index.js +21 -0
- package/dist/agents/codex/outputs.js +189 -0
- package/dist/agents/codex/scaffold.js +273 -0
- package/dist/agents/codex/transport.js +139 -0
- package/dist/agents/codex/types.js +1 -0
- package/dist/cli.js +1357 -0
- package/dist/constants.js +39 -0
- package/dist/core/define-hook.js +61 -0
- package/dist/core/env.js +186 -0
- package/dist/core/logger.js +508 -0
- package/dist/core/stdin.js +38 -0
- package/dist/core/transport.js +231 -0
- package/dist/core/types.js +10 -0
- package/dist/index.js +17 -0
- package/dist/scaffold.js +512 -0
- package/package.json +61 -0
- package/types/agents/antigravity/events.d.ts +50 -0
- package/types/agents/antigravity/hooks.d.ts +76 -0
- package/types/agents/antigravity/index.d.ts +27 -0
- package/types/agents/antigravity/outputs.d.ts +107 -0
- package/types/agents/antigravity/transport.d.ts +47 -0
- package/types/agents/antigravity/types.d.ts +58 -0
- package/types/agents/claude-code/env.d.ts +27 -0
- package/types/agents/claude-code/events.d.ts +69 -0
- package/types/agents/claude-code/hooks.d.ts +1182 -0
- package/types/agents/claude-code/index.d.ts +23 -0
- package/types/agents/claude-code/outputs.d.ts +1160 -0
- package/types/agents/claude-code/tool-helpers.d.ts +807 -0
- package/types/agents/claude-code/transport.d.ts +119 -0
- package/types/agents/claude-code/types.d.ts +557 -0
- package/types/agents/codex/cli-support.d.ts +105 -0
- package/types/agents/codex/constants.d.ts +10 -0
- package/types/agents/codex/events.d.ts +63 -0
- package/types/agents/codex/hooks.d.ts +135 -0
- package/types/agents/codex/index.d.ts +19 -0
- package/types/agents/codex/outputs.d.ts +190 -0
- package/types/agents/codex/scaffold.d.ts +14 -0
- package/types/agents/codex/transport.d.ts +74 -0
- package/types/agents/codex/types.d.ts +131 -0
- package/types/cli.d.ts +315 -0
- package/types/constants.d.ts +9 -0
- package/types/core/define-hook.d.ts +39 -0
- package/types/core/env.d.ts +150 -0
- package/types/core/logger.d.ts +476 -0
- package/types/core/stdin.d.ts +21 -0
- package/types/core/transport.d.ts +131 -0
- package/types/core/types.d.ts +137 -0
- package/types/index.d.ts +18 -0
- 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
|
+
}
|