@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
package/dist/scaffold.js
ADDED
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scaffold module for generating new Claude Code hook projects.
|
|
3
|
+
*
|
|
4
|
+
* Generates a complete TypeScript project structure with:
|
|
5
|
+
* - package.json with dependencies and scripts
|
|
6
|
+
* - tsconfig.json with ESM/Node20 configuration
|
|
7
|
+
* - biome.json for linting/formatting
|
|
8
|
+
* - Hook template files for each requested hook type
|
|
9
|
+
* - Vitest test files for each hook
|
|
10
|
+
* - vitest.config.ts for test configuration
|
|
11
|
+
* @module
|
|
12
|
+
* @example
|
|
13
|
+
* ```bash
|
|
14
|
+
* npx @goodfoot/agent-hooks --agent claude-code --scaffold ./my-hooks --hooks Stop,SubagentStop -o dist/hooks.json
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
import * as fs from "node:fs";
|
|
18
|
+
import * as path from "node:path";
|
|
19
|
+
import { HOOK_FACTORY_TO_EVENT } from "./constants.js";
|
|
20
|
+
// ============================================================================
|
|
21
|
+
// Constants
|
|
22
|
+
// ============================================================================
|
|
23
|
+
/**
|
|
24
|
+
* Valid hook event names (derived from HOOK_FACTORY_TO_EVENT values).
|
|
25
|
+
*/
|
|
26
|
+
const VALID_HOOK_EVENT_NAMES = new Set(Object.values(HOOK_FACTORY_TO_EVENT));
|
|
27
|
+
/**
|
|
28
|
+
* Case-insensitive lookup map for hook event names.
|
|
29
|
+
* Built once at module load to avoid recreation on each validation call.
|
|
30
|
+
*/
|
|
31
|
+
const CASE_INSENSITIVE_EVENT_LOOKUP = new Map(Array.from(VALID_HOOK_EVENT_NAMES).map((name) => [name.toLowerCase(), name]));
|
|
32
|
+
/**
|
|
33
|
+
* Mapping from hook event name to factory function name.
|
|
34
|
+
*/
|
|
35
|
+
const EVENT_TO_HOOK_FACTORY = Object.fromEntries(Object.entries(HOOK_FACTORY_TO_EVENT).map(([factory, event]) => [event, factory]));
|
|
36
|
+
/**
|
|
37
|
+
* Mapping from hook event name to output function name.
|
|
38
|
+
*/
|
|
39
|
+
const EVENT_TO_OUTPUT_FUNCTION = {
|
|
40
|
+
PreToolUse: "preToolUseOutput",
|
|
41
|
+
PostToolUse: "postToolUseOutput",
|
|
42
|
+
PostToolUseFailure: "postToolUseFailureOutput",
|
|
43
|
+
PostToolBatch: "postToolBatchOutput",
|
|
44
|
+
Notification: "notificationOutput",
|
|
45
|
+
UserPromptExpansion: "userPromptExpansionOutput",
|
|
46
|
+
UserPromptSubmit: "userPromptSubmitOutput",
|
|
47
|
+
SessionStart: "sessionStartOutput",
|
|
48
|
+
SessionEnd: "sessionEndOutput",
|
|
49
|
+
Stop: "stopOutput",
|
|
50
|
+
StopFailure: "stopFailureOutput",
|
|
51
|
+
SubagentStart: "subagentStartOutput",
|
|
52
|
+
SubagentStop: "subagentStopOutput",
|
|
53
|
+
PreCompact: "preCompactOutput",
|
|
54
|
+
PostCompact: "postCompactOutput",
|
|
55
|
+
PermissionRequest: "permissionRequestOutput",
|
|
56
|
+
PermissionDenied: "permissionDeniedOutput",
|
|
57
|
+
Setup: "setupOutput",
|
|
58
|
+
TeammateIdle: "teammateIdleOutput",
|
|
59
|
+
TaskCreated: "taskCreatedOutput",
|
|
60
|
+
TaskCompleted: "taskCompletedOutput",
|
|
61
|
+
Elicitation: "elicitationOutput",
|
|
62
|
+
ElicitationResult: "elicitationResultOutput",
|
|
63
|
+
ConfigChange: "configChangeOutput",
|
|
64
|
+
InstructionsLoaded: "instructionsLoadedOutput",
|
|
65
|
+
WorktreeCreate: "worktreeCreateOutput",
|
|
66
|
+
WorktreeRemove: "worktreeRemoveOutput",
|
|
67
|
+
CwdChanged: "cwdChangedOutput",
|
|
68
|
+
FileChanged: "fileChangedOutput",
|
|
69
|
+
MessageDisplay: "messageDisplayOutput",
|
|
70
|
+
};
|
|
71
|
+
// ============================================================================
|
|
72
|
+
// Validation
|
|
73
|
+
// ============================================================================
|
|
74
|
+
/**
|
|
75
|
+
* Validates hook names against valid hook event names.
|
|
76
|
+
*
|
|
77
|
+
* Accepts PascalCase names case-insensitively (e.g., 'stop' -> 'Stop').
|
|
78
|
+
* @param hookNames - Array of hook names to validate
|
|
79
|
+
* @returns Object with normalized hook names or error message
|
|
80
|
+
*/
|
|
81
|
+
function validateHookNames(hookNames) {
|
|
82
|
+
const normalized = [];
|
|
83
|
+
const invalid = [];
|
|
84
|
+
for (const hookName of hookNames) {
|
|
85
|
+
const normalizedName = CASE_INSENSITIVE_EVENT_LOOKUP.get(hookName.toLowerCase());
|
|
86
|
+
if (normalizedName !== undefined) {
|
|
87
|
+
normalized.push(normalizedName);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
invalid.push(hookName);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (invalid.length > 0) {
|
|
94
|
+
const validNames = Array.from(VALID_HOOK_EVENT_NAMES).sort().join(", ");
|
|
95
|
+
return {
|
|
96
|
+
valid: false,
|
|
97
|
+
error: `Invalid hook name(s): ${invalid.join(", ")}\nValid hook names: ${validNames}`,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return { valid: true, normalized };
|
|
101
|
+
}
|
|
102
|
+
// ============================================================================
|
|
103
|
+
// File Generation
|
|
104
|
+
// ============================================================================
|
|
105
|
+
/**
|
|
106
|
+
* Converts a PascalCase hook event name to kebab-case filename.
|
|
107
|
+
* @param eventName - Hook event name (e.g., 'PreToolUse')
|
|
108
|
+
* @returns Kebab-case filename (e.g., 'pre-tool-use')
|
|
109
|
+
*/
|
|
110
|
+
function toKebabCase(eventName) {
|
|
111
|
+
return eventName
|
|
112
|
+
.replace(/([a-z])([A-Z])/g, "$1-$2")
|
|
113
|
+
.replace(/([A-Z])([A-Z][a-z])/g, "$1-$2")
|
|
114
|
+
.toLowerCase();
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Generates package.json content for the scaffolded project.
|
|
118
|
+
* @param projectName - Name derived from directory basename
|
|
119
|
+
* @param outputPath - Relative path for hooks.json output
|
|
120
|
+
* @returns JSON string for package.json
|
|
121
|
+
*/
|
|
122
|
+
function generatePackageJson(projectName, outputPath) {
|
|
123
|
+
const packageJson = {
|
|
124
|
+
name: projectName,
|
|
125
|
+
version: "1.0.0",
|
|
126
|
+
type: "module",
|
|
127
|
+
scripts: {
|
|
128
|
+
build: `agent-hooks --agent claude-code -i "src/**/*.ts" -o "${outputPath}"`,
|
|
129
|
+
test: "vitest run",
|
|
130
|
+
lint: "biome check .",
|
|
131
|
+
typecheck: "tsc --noEmit",
|
|
132
|
+
},
|
|
133
|
+
dependencies: {
|
|
134
|
+
"@goodfoot/agent-hooks": "^1.0.0",
|
|
135
|
+
},
|
|
136
|
+
devDependencies: {
|
|
137
|
+
"@biomejs/biome": "2.4.16",
|
|
138
|
+
"@types/node": "^22.0.0",
|
|
139
|
+
typescript: "^5.9.3",
|
|
140
|
+
vitest: "^4.0.16",
|
|
141
|
+
},
|
|
142
|
+
engines: {
|
|
143
|
+
node: ">=20.11.0",
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
return `${JSON.stringify(packageJson, null, 2)}\n`;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Generates tsconfig.json content for the scaffolded project.
|
|
150
|
+
* @returns JSON string for tsconfig.json
|
|
151
|
+
*/
|
|
152
|
+
function generateTsConfig() {
|
|
153
|
+
const tsconfig = {
|
|
154
|
+
compilerOptions: {
|
|
155
|
+
target: "ES2022",
|
|
156
|
+
module: "NodeNext",
|
|
157
|
+
moduleResolution: "NodeNext",
|
|
158
|
+
strict: true,
|
|
159
|
+
esModuleInterop: true,
|
|
160
|
+
skipLibCheck: true,
|
|
161
|
+
declaration: true,
|
|
162
|
+
declarationMap: true,
|
|
163
|
+
outDir: "./dist",
|
|
164
|
+
},
|
|
165
|
+
include: ["src/**/*.ts", "test/**/*.ts"],
|
|
166
|
+
exclude: ["node_modules", "dist"],
|
|
167
|
+
};
|
|
168
|
+
return `${JSON.stringify(tsconfig, null, 2)}\n`;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Generates biome.json content for the scaffolded project.
|
|
172
|
+
*
|
|
173
|
+
* Uses Biome v2 configuration format with:
|
|
174
|
+
* - assist.actions.source.organizeImports for import organization
|
|
175
|
+
* - files.includes for specifying which files to process
|
|
176
|
+
* @returns JSON string for biome.json
|
|
177
|
+
*/
|
|
178
|
+
function generateBiomeConfig() {
|
|
179
|
+
return `{
|
|
180
|
+
"$schema": "https://biomejs.dev/schemas/2.4.16/schema.json",
|
|
181
|
+
"formatter": {
|
|
182
|
+
"enabled": true,
|
|
183
|
+
"indentStyle": "space",
|
|
184
|
+
"indentWidth": 2,
|
|
185
|
+
"lineWidth": 120
|
|
186
|
+
},
|
|
187
|
+
"linter": {
|
|
188
|
+
"enabled": true,
|
|
189
|
+
"rules": {
|
|
190
|
+
"recommended": true
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
"assist": {
|
|
194
|
+
"actions": {
|
|
195
|
+
"source": {
|
|
196
|
+
"organizeImports": "on"
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
"files": {
|
|
201
|
+
"includes": ["src/**/*.ts", "test/**/*.ts", "*.ts"]
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
`;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Generates vitest.config.ts content for the scaffolded project.
|
|
208
|
+
*
|
|
209
|
+
* Uses double quotes and trailing commas to match biome's formatting preferences.
|
|
210
|
+
* @returns TypeScript content for vitest.config.ts
|
|
211
|
+
*/
|
|
212
|
+
function generateVitestConfig() {
|
|
213
|
+
return `import { defineConfig } from "vitest/config";
|
|
214
|
+
|
|
215
|
+
export default defineConfig({
|
|
216
|
+
test: {
|
|
217
|
+
include: ["test/**/*.test.ts"],
|
|
218
|
+
globals: false,
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
`;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Generates CLAUDE.md content for the scaffolded project.
|
|
225
|
+
* @returns Markdown content for CLAUDE.md
|
|
226
|
+
*/
|
|
227
|
+
function generateClaudeMd() {
|
|
228
|
+
return "Load the `agent-hooks:claude-code` skill immediately if it is available.\n";
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Generates .gitignore content for the scaffolded project.
|
|
232
|
+
* @returns Content for .gitignore
|
|
233
|
+
*/
|
|
234
|
+
function generateGitignore() {
|
|
235
|
+
return `# Dependencies
|
|
236
|
+
node_modules/
|
|
237
|
+
|
|
238
|
+
# Build outputs
|
|
239
|
+
dist/
|
|
240
|
+
bin/
|
|
241
|
+
*.mjs
|
|
242
|
+
|
|
243
|
+
# Generated files
|
|
244
|
+
hooks.json
|
|
245
|
+
|
|
246
|
+
# IDE
|
|
247
|
+
.idea/
|
|
248
|
+
.vscode/
|
|
249
|
+
*.swp
|
|
250
|
+
*.swo
|
|
251
|
+
|
|
252
|
+
# OS
|
|
253
|
+
.DS_Store
|
|
254
|
+
Thumbs.db
|
|
255
|
+
|
|
256
|
+
# Logs
|
|
257
|
+
*.log
|
|
258
|
+
`;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Generates README.md content for the scaffolded project.
|
|
262
|
+
* @param projectName - Name of the project
|
|
263
|
+
* @param hooks - Array of hook event names included in this project
|
|
264
|
+
* @returns Markdown content for README.md
|
|
265
|
+
*/
|
|
266
|
+
function generateReadme(projectName, hooks) {
|
|
267
|
+
const hookList = hooks.map((h) => `\`${h}\``).join(", ");
|
|
268
|
+
return `# ${projectName}
|
|
269
|
+
|
|
270
|
+
This project contains [Claude Code hooks](https://docs.anthropic.com/en/docs/claude-code/hooks) built with the \`@goodfoot/agent-hooks\` library. Hooks let you extend Claude Code's behavior by running custom code at specific points during a session—before or after tool execution, when Claude starts or stops, and more. This project includes hooks for: ${hookList}.
|
|
271
|
+
|
|
272
|
+
To get started, run \`npm install\` to install dependencies, then \`npm run build\` to compile your hooks into \`hooks.json\`. Copy the generated \`hooks.json\` to your Claude Code settings directory (or reference it in your \`.claude/settings.json\`), and your hooks will run automatically. Edit the files in \`src/\` to customize behavior, and use \`npm test\` to verify your changes work correctly.
|
|
273
|
+
`;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Generates a hook template file for a specific hook type.
|
|
277
|
+
*
|
|
278
|
+
* Uses double quotes for all strings to match biome's formatting preferences.
|
|
279
|
+
* @param eventName - Hook event name (e.g., 'Stop')
|
|
280
|
+
* @returns TypeScript content for the hook file
|
|
281
|
+
*/
|
|
282
|
+
function generateHookTemplate(eventName) {
|
|
283
|
+
const factoryName = EVENT_TO_HOOK_FACTORY[eventName];
|
|
284
|
+
const outputName = EVENT_TO_OUTPUT_FUNCTION[eventName];
|
|
285
|
+
// Generate appropriate return statement based on hook type
|
|
286
|
+
// All hooks include systemMessage to demonstrate the pattern
|
|
287
|
+
let returnStatement;
|
|
288
|
+
switch (eventName) {
|
|
289
|
+
case "Stop":
|
|
290
|
+
case "SubagentStop":
|
|
291
|
+
returnStatement = `return ${outputName}({
|
|
292
|
+
decision: "approve",
|
|
293
|
+
systemMessage: "${eventName} hook executed successfully.",
|
|
294
|
+
});`;
|
|
295
|
+
break;
|
|
296
|
+
case "PreToolUse":
|
|
297
|
+
returnStatement = `return ${outputName}({
|
|
298
|
+
systemMessage: "Tool execution allowed by ${eventName} hook.",
|
|
299
|
+
hookSpecificOutput: { permissionDecision: "allow" },
|
|
300
|
+
});`;
|
|
301
|
+
break;
|
|
302
|
+
case "PostToolUse":
|
|
303
|
+
case "PostToolUseFailure":
|
|
304
|
+
case "PostToolBatch":
|
|
305
|
+
returnStatement = `return ${outputName}({
|
|
306
|
+
systemMessage: "${eventName} hook processed the result.",
|
|
307
|
+
hookSpecificOutput: { additionalContext: "Hook completed successfully." },
|
|
308
|
+
});`;
|
|
309
|
+
break;
|
|
310
|
+
case "SessionStart":
|
|
311
|
+
returnStatement = `return ${outputName}({
|
|
312
|
+
systemMessage: "Session initialized by ${eventName} hook.",
|
|
313
|
+
hookSpecificOutput: { additionalContext: "Environment ready." },
|
|
314
|
+
});`;
|
|
315
|
+
break;
|
|
316
|
+
case "UserPromptExpansion":
|
|
317
|
+
returnStatement = `return ${outputName}({
|
|
318
|
+
systemMessage: "Prompt expansion observed.",
|
|
319
|
+
hookSpecificOutput: { additionalContext: \`Command: \${input.command_name}\` },
|
|
320
|
+
});`;
|
|
321
|
+
break;
|
|
322
|
+
case "UserPromptSubmit":
|
|
323
|
+
returnStatement = `return ${outputName}({
|
|
324
|
+
systemMessage: "User prompt received.",
|
|
325
|
+
hookSpecificOutput: { additionalContext: "Prompt processed by hook." },
|
|
326
|
+
});`;
|
|
327
|
+
break;
|
|
328
|
+
case "PreCompact":
|
|
329
|
+
returnStatement = `return ${outputName}({
|
|
330
|
+
systemMessage: "Remember: Context is being compacted.",
|
|
331
|
+
});`;
|
|
332
|
+
break;
|
|
333
|
+
case "PermissionRequest":
|
|
334
|
+
returnStatement = `return ${outputName}({
|
|
335
|
+
systemMessage: "Permission request processed.",
|
|
336
|
+
});`;
|
|
337
|
+
break;
|
|
338
|
+
case "Elicitation":
|
|
339
|
+
returnStatement = `return ${outputName}({
|
|
340
|
+
hookSpecificOutput: { action: "accept" },
|
|
341
|
+
});`;
|
|
342
|
+
break;
|
|
343
|
+
case "ElicitationResult":
|
|
344
|
+
returnStatement = `return ${outputName}({});`;
|
|
345
|
+
break;
|
|
346
|
+
case "WorktreeCreate":
|
|
347
|
+
// WorktreeCreate is a command hook whose protocol is the bare worktree path on
|
|
348
|
+
// stdout. `worktreePath` is required and written to stdout as plain text.
|
|
349
|
+
returnStatement = `return ${outputName}({
|
|
350
|
+
worktreePath: \`\${input.cwd}/.worktrees/\${input.name}\`,
|
|
351
|
+
});`;
|
|
352
|
+
break;
|
|
353
|
+
case "ConfigChange":
|
|
354
|
+
case "InstructionsLoaded":
|
|
355
|
+
case "WorktreeRemove":
|
|
356
|
+
returnStatement = `return ${outputName}({});`;
|
|
357
|
+
break;
|
|
358
|
+
case "MessageDisplay":
|
|
359
|
+
returnStatement = `return ${outputName}({});`;
|
|
360
|
+
break;
|
|
361
|
+
default:
|
|
362
|
+
// SessionEnd, Notification, SubagentStart use simple output with systemMessage
|
|
363
|
+
returnStatement = `return ${outputName}({
|
|
364
|
+
systemMessage: "${eventName} hook executed.",
|
|
365
|
+
});`;
|
|
366
|
+
}
|
|
367
|
+
// SessionStart hooks get extended context with persistEnvVar
|
|
368
|
+
const contextDestructure = eventName === "SessionStart" ? "{ logger, persistEnvVar }" : "{ logger }";
|
|
369
|
+
return `/**
|
|
370
|
+
* ${eventName} hook implementation.
|
|
371
|
+
*
|
|
372
|
+
* @see https://code.claude.com/docs/en/hooks#${eventName.toLowerCase()}
|
|
373
|
+
*/
|
|
374
|
+
|
|
375
|
+
import { ${factoryName}, ${outputName} } from "@goodfoot/agent-hooks/claude-code";
|
|
376
|
+
|
|
377
|
+
export default ${factoryName}({}, (input, ${contextDestructure}) => {
|
|
378
|
+
logger.info("${eventName} hook triggered", { input });
|
|
379
|
+
${returnStatement}
|
|
380
|
+
});
|
|
381
|
+
`;
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Generates a test file for a specific hook type.
|
|
385
|
+
*
|
|
386
|
+
* Uses double quotes, alphabetical import order (describe, expect, it),
|
|
387
|
+
* and trailing commas to match biome's formatting preferences.
|
|
388
|
+
* Uses the real Logger class (silent by default) instead of mocks.
|
|
389
|
+
* @param eventName - Hook event name (e.g., 'Stop')
|
|
390
|
+
* @param hookFilename - Kebab-case filename of the hook (e.g., 'stop')
|
|
391
|
+
* @returns TypeScript content for the test file
|
|
392
|
+
*/
|
|
393
|
+
function generateTestFile(eventName, hookFilename) {
|
|
394
|
+
// SessionStart hooks need extended context with persistEnvVar
|
|
395
|
+
const contextCode = eventName === "SessionStart"
|
|
396
|
+
? `{
|
|
397
|
+
logger,
|
|
398
|
+
persistEnvVar: () => {},
|
|
399
|
+
persistEnvVars: () => {},
|
|
400
|
+
}`
|
|
401
|
+
: "{ logger }";
|
|
402
|
+
return `/**
|
|
403
|
+
* Tests for the ${eventName} hook.
|
|
404
|
+
*/
|
|
405
|
+
|
|
406
|
+
import { Logger } from "@goodfoot/agent-hooks/claude-code";
|
|
407
|
+
import { describe, expect, it } from "vitest";
|
|
408
|
+
import hook from "../src/${hookFilename}.js";
|
|
409
|
+
|
|
410
|
+
// Logger is silent by default (no stdout/stderr output) — no mocking needed
|
|
411
|
+
const logger = new Logger();
|
|
412
|
+
|
|
413
|
+
describe("${eventName} Hook", () => {
|
|
414
|
+
it("exports a valid hook function", () => {
|
|
415
|
+
expect(hook).toBeDefined();
|
|
416
|
+
expect(typeof hook).toBe("function");
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it("has correct eventName metadata", () => {
|
|
420
|
+
expect(hook.eventName).toBe("${eventName}");
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it("returns a valid output shape", async () => {
|
|
424
|
+
const mockInput = {} as Parameters<typeof hook>[0];
|
|
425
|
+
const context = ${contextCode};
|
|
426
|
+
|
|
427
|
+
const result = await hook(mockInput, context);
|
|
428
|
+
|
|
429
|
+
// Verify output has expected structure
|
|
430
|
+
expect(result).toBeDefined();
|
|
431
|
+
expect(result).toHaveProperty("_type", "${eventName}");
|
|
432
|
+
expect(result).toHaveProperty("stdout");
|
|
433
|
+
expect(typeof result.stdout).toBe("object");
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
`;
|
|
437
|
+
}
|
|
438
|
+
// ============================================================================
|
|
439
|
+
// Main Scaffold Function
|
|
440
|
+
// ============================================================================
|
|
441
|
+
/**
|
|
442
|
+
* Scaffolds a new Claude Code hook project.
|
|
443
|
+
*
|
|
444
|
+
* Creates the complete project structure including:
|
|
445
|
+
* - package.json, tsconfig.json, biome.json, vitest.config.ts
|
|
446
|
+
* - src/ directory with hook implementations
|
|
447
|
+
* - test/ directory with vitest tests
|
|
448
|
+
* @param options - Scaffold configuration options
|
|
449
|
+
* @throws Exits with code 1 if directory exists or hook names are invalid
|
|
450
|
+
* @example
|
|
451
|
+
* ```typescript
|
|
452
|
+
* scaffoldProject({
|
|
453
|
+
* directory: './my-hooks',
|
|
454
|
+
* hooks: ['Stop', 'SubagentStop'],
|
|
455
|
+
* outputPath: 'dist/hooks.json'
|
|
456
|
+
* });
|
|
457
|
+
* ```
|
|
458
|
+
*/
|
|
459
|
+
export function scaffoldProject(options) {
|
|
460
|
+
const { directory, hooks, outputPath } = options;
|
|
461
|
+
// Resolve to absolute path
|
|
462
|
+
const absoluteDir = path.resolve(process.cwd(), directory);
|
|
463
|
+
// Check if directory already exists
|
|
464
|
+
if (fs.existsSync(absoluteDir)) {
|
|
465
|
+
process.stderr.write(`Error: Directory already exists: ${absoluteDir}\n`);
|
|
466
|
+
process.exit(1);
|
|
467
|
+
}
|
|
468
|
+
// Validate hook names
|
|
469
|
+
const validation = validateHookNames(hooks);
|
|
470
|
+
if (!validation.valid) {
|
|
471
|
+
process.stderr.write(`Error: ${validation.error}\n`);
|
|
472
|
+
process.exit(1);
|
|
473
|
+
}
|
|
474
|
+
const normalizedHooks = validation.normalized;
|
|
475
|
+
// Create directory structure
|
|
476
|
+
const srcDir = path.join(absoluteDir, "src");
|
|
477
|
+
const testDir = path.join(absoluteDir, "test");
|
|
478
|
+
fs.mkdirSync(absoluteDir, { recursive: true });
|
|
479
|
+
fs.mkdirSync(srcDir, { recursive: true });
|
|
480
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
481
|
+
// Generate project name from directory basename
|
|
482
|
+
const projectName = path.basename(absoluteDir);
|
|
483
|
+
// Generate configuration files
|
|
484
|
+
fs.writeFileSync(path.join(absoluteDir, "package.json"), generatePackageJson(projectName, outputPath));
|
|
485
|
+
fs.writeFileSync(path.join(absoluteDir, "tsconfig.json"), generateTsConfig());
|
|
486
|
+
fs.writeFileSync(path.join(absoluteDir, "biome.json"), generateBiomeConfig());
|
|
487
|
+
fs.writeFileSync(path.join(absoluteDir, "vitest.config.ts"), generateVitestConfig());
|
|
488
|
+
fs.writeFileSync(path.join(absoluteDir, "CLAUDE.md"), generateClaudeMd());
|
|
489
|
+
fs.writeFileSync(path.join(absoluteDir, ".gitignore"), generateGitignore());
|
|
490
|
+
fs.writeFileSync(path.join(absoluteDir, "README.md"), generateReadme(projectName, normalizedHooks));
|
|
491
|
+
// Generate hook files and tests
|
|
492
|
+
for (const eventName of normalizedHooks) {
|
|
493
|
+
const kebabName = toKebabCase(eventName);
|
|
494
|
+
// Generate hook file
|
|
495
|
+
const hookContent = generateHookTemplate(eventName);
|
|
496
|
+
fs.writeFileSync(path.join(srcDir, `${kebabName}.ts`), hookContent);
|
|
497
|
+
// Generate test file
|
|
498
|
+
const testContent = generateTestFile(eventName, kebabName);
|
|
499
|
+
fs.writeFileSync(path.join(testDir, `${kebabName}.test.ts`), testContent);
|
|
500
|
+
}
|
|
501
|
+
// Output success message
|
|
502
|
+
process.stdout.write(`Created hook project at ${absoluteDir}\n`);
|
|
503
|
+
process.stdout.write("\nNext steps:\n");
|
|
504
|
+
process.stdout.write(` cd ${directory}\n`);
|
|
505
|
+
process.stdout.write(" npm install\n");
|
|
506
|
+
process.stdout.write(" npm run build\n");
|
|
507
|
+
process.stdout.write("\nGenerated hooks:\n");
|
|
508
|
+
for (const eventName of normalizedHooks) {
|
|
509
|
+
const kebabName = toKebabCase(eventName);
|
|
510
|
+
process.stdout.write(` - src/${kebabName}.ts\n`);
|
|
511
|
+
}
|
|
512
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@goodfoot/agent-hooks",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Unified agent hooks library: shared core runtime with per-agent entry points for Claude Code, Codex, and Antigravity",
|
|
5
|
+
"homepage": "https://github.com/goodfoot-io/marketplace/tree/main/packages/agent-hooks",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/goodfoot-io/marketplace.git",
|
|
9
|
+
"directory": "packages/agent-hooks"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"sideEffects": false,
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=20.11.0"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"types"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"typecheck": "tsc --noEmit",
|
|
22
|
+
"build": "tsc --build tsconfig.build.json",
|
|
23
|
+
"lint": "biome check --write --unsafe --max-diagnostics 500 ./src ./tests ./e2e ./scripts && node ./scripts/check-stale-imports.mjs",
|
|
24
|
+
"test": "node ./scripts/check-e2e-parity.mjs && node ./scripts/check-stale-imports.mjs && node ./scripts/check-agent-duplication.mjs && vitest run",
|
|
25
|
+
"check:e2e-parity": "node ./scripts/check-e2e-parity.mjs",
|
|
26
|
+
"release": "bash ../../scripts/release-package.sh agent-hooks",
|
|
27
|
+
"release:dry-run": "bash ../../scripts/release-package.sh agent-hooks --dry-run"
|
|
28
|
+
},
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"import": "./dist/index.js",
|
|
32
|
+
"types": "./types/index.d.ts"
|
|
33
|
+
},
|
|
34
|
+
"./claude-code": {
|
|
35
|
+
"import": "./dist/agents/claude-code/index.js",
|
|
36
|
+
"types": "./types/agents/claude-code/index.d.ts"
|
|
37
|
+
},
|
|
38
|
+
"./codex": {
|
|
39
|
+
"import": "./dist/agents/codex/index.js",
|
|
40
|
+
"types": "./types/agents/codex/index.d.ts"
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"bin": "./dist/cli.js",
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"bin": {
|
|
46
|
+
"agent-hooks": "./dist/cli.js"
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"esbuild": "^0.24.0",
|
|
51
|
+
"glob": "^11.0.0",
|
|
52
|
+
"typescript": "^5.9.3"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@anthropic-ai/claude-agent-sdk": "^0.3.162",
|
|
56
|
+
"@biomejs/biome": "2.4.16",
|
|
57
|
+
"@types/node": "^24",
|
|
58
|
+
"tsx": "^4.20.6",
|
|
59
|
+
"vitest": "4.0.13"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Antigravity hook event names and the (currently empty) advisory allow-list.
|
|
3
|
+
*
|
|
4
|
+
* Unlike Claude Code's and Codex's allow-lists, which transcribe a named set
|
|
5
|
+
* of enrichment hooks out of a shipped README, Antigravity has no in-repo
|
|
6
|
+
* doc to transcribe from this release (Step 0 descope — see the plan's Step
|
|
7
|
+
* 5 blockquote). The allow-list therefore defaults to **every event
|
|
8
|
+
* excluded**, not a curated subset, until a future card's `CONTRACT.md`
|
|
9
|
+
* names a specific enrichment hook safe to opt into `unexpectedError:
|
|
10
|
+
* "continue"`.
|
|
11
|
+
*
|
|
12
|
+
* That default is itself gated on a second precondition: because Antigravity
|
|
13
|
+
* has no exit-code channel at all (every event is payload-only — see
|
|
14
|
+
* `transport.ts` and `outputs.ts`'s {@link AntigravityBlockError}), a
|
|
15
|
+
* fail-open swallow on this agent has no narrower fallback than Claude
|
|
16
|
+
* Code's or Codex's — there is no stderr+exit-2 loud path to fall back to.
|
|
17
|
+
* `AntigravityBlockError` landing first (this same step) is what makes it
|
|
18
|
+
* safe to ever widen this list at all: an intentional block computed
|
|
19
|
+
* mid-throw still survives `unexpectedError: "continue"` via `drive()`'s
|
|
20
|
+
* pre-policy classification, exactly as it does for the other two agents.
|
|
21
|
+
* @module
|
|
22
|
+
*/
|
|
23
|
+
import type { UnexpectedErrorPolicy } from "../../core/types.js";
|
|
24
|
+
import type { HookEventName } from "./types.js";
|
|
25
|
+
/** All Antigravity hook event names as a readonly array. */
|
|
26
|
+
export declare const HOOK_EVENT_NAMES: readonly ["PreToolUse", "PostToolUse", "PreInvocation", "PostInvocation", "Stop"];
|
|
27
|
+
/** The two events whose config accepts a `matcher` (plan step 5.3: "matcher on the two ToolUse events"). */
|
|
28
|
+
export declare const EVENTS_WITH_MATCHER: readonly HookEventName[];
|
|
29
|
+
/**
|
|
30
|
+
* Events never allowed the fail-open `unexpectedError: "continue"` policy.
|
|
31
|
+
*
|
|
32
|
+
* Every shipped event, by default — see module docs. This is a stated
|
|
33
|
+
* default (plan Decisions: "advisory list default empty until CONTRACT.md
|
|
34
|
+
* justifies an entry"), not a silent gap.
|
|
35
|
+
*/
|
|
36
|
+
export declare const EXCLUDED_FROM_ADVISORY: readonly ["PreToolUse", "PostToolUse", "PreInvocation", "PostInvocation", "Stop"];
|
|
37
|
+
/** The exclusion set as a literal union. Currently every event. */
|
|
38
|
+
export type ExcludedEventName = (typeof EXCLUDED_FROM_ADVISORY)[number];
|
|
39
|
+
/** Type-level membership check: `true` for every event name today. */
|
|
40
|
+
export type IsExcludedEvent<TEventName extends HookEventName> = TEventName extends ExcludedEventName ? true : false;
|
|
41
|
+
/** The advisory allow-list as a literal union: every event except {@link ExcludedEventName} — currently `never`. */
|
|
42
|
+
export type AdvisoryEventName = Exclude<HookEventName, ExcludedEventName>;
|
|
43
|
+
/** The advisory allow-list. Empty until a future `CONTRACT.md` names an entry. */
|
|
44
|
+
export declare const ADVISORY_EVENTS: readonly AdvisoryEventName[];
|
|
45
|
+
/**
|
|
46
|
+
* The policy type an event may carry, narrowed by allow-list membership.
|
|
47
|
+
* Every event collapses to `"error"` today, since {@link ADVISORY_EVENTS} is
|
|
48
|
+
* empty.
|
|
49
|
+
*/
|
|
50
|
+
export type AllowedUnexpectedErrorPolicy<TEventName extends HookEventName> = IsExcludedEvent<TEventName> extends true ? Exclude<UnexpectedErrorPolicy, "continue"> : UnexpectedErrorPolicy;
|