@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,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;