@agiflowai/hooks-adapter 0.0.14 → 0.0.15

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/dist/index.cjs CHANGED
@@ -42,6 +42,7 @@ node_crypto = __toESM(node_crypto);
42
42
  * DESIGN PATTERNS:
43
43
  * - Strongly-typed constant exports for compile-time safety
44
44
  * - Immutable by default (as const assertions)
45
+ * - Related constants grouped into const objects
45
46
  *
46
47
  * CODING STANDARDS:
47
48
  * - Primitive constants: UPPER_SNAKE_CASE or PascalCase for event names
@@ -52,15 +53,36 @@ node_crypto = __toESM(node_crypto);
52
53
  * - Magic strings without explanation
53
54
  */
54
55
  /**
55
- * Hook type identifiers for Claude Code hook events
56
+ * Grouped hook type identifiers for Claude Code hook events
56
57
  */
57
- const PRE_TOOL_USE = "PreToolUse";
58
- const POST_TOOL_USE = "PostToolUse";
58
+ const ClaudeCodeHookTypes = {
59
+ PRE_TOOL_USE: "PreToolUse",
60
+ POST_TOOL_USE: "PostToolUse",
61
+ STOP: "Stop",
62
+ USER_PROMPT_SUBMIT: "UserPromptSubmit",
63
+ TASK_COMPLETED: "TaskCompleted"
64
+ };
59
65
  /**
60
- * Hook type identifiers for Gemini CLI hook events
66
+ * Grouped hook type identifiers for Gemini CLI hook events
61
67
  */
62
- const BEFORE_TOOL_USE = "BeforeTool";
63
- const AFTER_TOOL_USE = "AfterTool";
68
+ const GeminiCliHookTypes = {
69
+ BEFORE_TOOL_USE: "BeforeTool",
70
+ AFTER_TOOL_USE: "AfterTool"
71
+ };
72
+ /** Hook event fired before a tool is executed in Claude Code */
73
+ const PRE_TOOL_USE = ClaudeCodeHookTypes.PRE_TOOL_USE;
74
+ /** Hook event fired after a tool has executed in Claude Code */
75
+ const POST_TOOL_USE = ClaudeCodeHookTypes.POST_TOOL_USE;
76
+ /** Hook event fired when a Claude Code session is about to stop */
77
+ const STOP = ClaudeCodeHookTypes.STOP;
78
+ /** Hook event fired when the user submits a new prompt in Claude Code */
79
+ const USER_PROMPT_SUBMIT = ClaudeCodeHookTypes.USER_PROMPT_SUBMIT;
80
+ /** Hook event fired when an agentic task completes in Claude Code */
81
+ const TASK_COMPLETED = ClaudeCodeHookTypes.TASK_COMPLETED;
82
+ /** Hook event fired before a tool is executed in Gemini CLI */
83
+ const BEFORE_TOOL_USE = GeminiCliHookTypes.BEFORE_TOOL_USE;
84
+ /** Hook event fired after a tool is executed in Gemini CLI */
85
+ const AFTER_TOOL_USE = GeminiCliHookTypes.AFTER_TOOL_USE;
64
86
 
65
87
  //#endregion
66
88
  //#region src/constants/decisions.ts
@@ -91,12 +113,13 @@ var BaseAdapter = class {
91
113
  const stdin = await this.readStdin();
92
114
  const response = await callback(this.parseInput(stdin));
93
115
  if (response.decision === "skip") {
94
- process.exit(0);
116
+ process.exit(response.exitCode ?? 0);
95
117
  return;
96
118
  }
119
+ if (response.userMessage) process.stderr.write(`${response.userMessage}\n`);
97
120
  const output = this.formatOutput(response);
98
121
  console.log(output);
99
- process.exit(0);
122
+ process.exit(response.exitCode ?? 0);
100
123
  } catch (error) {
101
124
  this.handleError(error);
102
125
  }
@@ -125,9 +148,10 @@ var BaseAdapter = class {
125
148
  process.exit(0);
126
149
  return;
127
150
  }
151
+ if (finalResponse.userMessage) process.stderr.write(`${finalResponse.userMessage}\n`);
128
152
  const output = this.formatOutput(finalResponse);
129
153
  console.log(output);
130
- process.exit(0);
154
+ process.exit(finalResponse.exitCode ?? 0);
131
155
  } catch (error) {
132
156
  this.handleError(error);
133
157
  }
@@ -170,18 +194,18 @@ var BaseAdapter = class {
170
194
  //#endregion
171
195
  //#region src/adapters/ClaudeCodeAdapter.ts
172
196
  /**
173
- * ClaudeCodeAdapter - Unified adapter for Claude Code hook format (PreToolUse & PostToolUse)
197
+ * ClaudeCodeAdapter - Unified adapter for Claude Code hook format
174
198
  *
175
199
  * DESIGN PATTERNS:
176
200
  * - Adapter pattern: Converts Claude Code format to normalized format
177
201
  * - Parser pattern: Extracts file paths and operations from tool inputs
178
- * - State pattern: Stores hook event type to morph behavior between PreToolUse and PostToolUse
202
+ * - State pattern: Stores hook event type to morph behavior between hook events
179
203
  *
180
204
  * CODING STANDARDS:
181
205
  * - Parse Claude Code JSON stdin format exactly as specified
182
206
  * - Format output to match Claude Code hook response schema
183
207
  * - Handle missing/optional fields gracefully
184
- * - Support both PreToolUse and PostToolUse events in one adapter
208
+ * - Support PreToolUse, PostToolUse, Stop, UserPromptSubmit, TaskCompleted events
185
209
  *
186
210
  * AVOID:
187
211
  * - Assuming all fields are present
@@ -189,7 +213,15 @@ var BaseAdapter = class {
189
213
  * - Mutating input objects
190
214
  */
191
215
  /**
192
- * Unified adapter for Claude Code hook format (PreToolUse & PostToolUse)
216
+ * Blockable hook event names
217
+ */
218
+ const BLOCKABLE_EVENTS = new Set([
219
+ "Stop",
220
+ "UserPromptSubmit",
221
+ "TaskCompleted"
222
+ ]);
223
+ /**
224
+ * Unified adapter for Claude Code hook format
193
225
  */
194
226
  var ClaudeCodeAdapter = class extends BaseAdapter {
195
227
  hookEventName = "PreToolUse";
@@ -211,7 +243,7 @@ var ClaudeCodeAdapter = class extends BaseAdapter {
211
243
  }
212
244
  /**
213
245
  * Format normalized HookResponse into Claude Code output
214
- * Morphs output based on hook event type (PreToolUse vs PostToolUse)
246
+ * Morphs output based on hook event type
215
247
  *
216
248
  * @param response - Normalized hook response
217
249
  * @returns JSON string for Claude Code
@@ -226,6 +258,7 @@ var ClaudeCodeAdapter = class extends BaseAdapter {
226
258
  __agiflowai_aicode_utils.log.debug("ClaudeCodeAdapter: Skip decision, returning empty output");
227
259
  return emptyOutput;
228
260
  }
261
+ if (BLOCKABLE_EVENTS.has(this.hookEventName)) return this.formatBlockableOutput(response);
229
262
  if (this.hookEventName === "PostToolUse") return this.formatPostToolUseOutput(response);
230
263
  return this.formatPreToolUseOutput(response);
231
264
  }
@@ -258,6 +291,20 @@ var ClaudeCodeAdapter = class extends BaseAdapter {
258
291
  __agiflowai_aicode_utils.log.debug("ClaudeCodeAdapter: Formatted PostToolUse output", { output: formattedOutput });
259
292
  return formattedOutput;
260
293
  }
294
+ /**
295
+ * Format blockable output for Stop, UserPromptSubmit, and TaskCompleted hooks
296
+ * Maps 'deny' decision to 'block', otherwise returns empty object
297
+ */
298
+ formatBlockableOutput(response) {
299
+ const output = {};
300
+ if (response.decision === "deny") {
301
+ output.decision = "block";
302
+ output.reason = response.message;
303
+ }
304
+ const formattedOutput = JSON.stringify(output, null, 2);
305
+ __agiflowai_aicode_utils.log.debug(`ClaudeCodeAdapter: Formatted ${this.hookEventName} output`, { output: formattedOutput });
306
+ return formattedOutput;
307
+ }
261
308
  };
262
309
 
263
310
  //#endregion
@@ -348,6 +395,12 @@ function isNodeError(error) {
348
395
  return error instanceof Error && "code" in error;
349
396
  }
350
397
  /**
398
+ * Type guard for LogEntry — validates required fields at runtime
399
+ */
400
+ function isLogEntry(value) {
401
+ return typeof value === "object" && value !== null && "filePath" in value && typeof value.filePath === "string" && "operation" in value && typeof value.operation === "string";
402
+ }
403
+ /**
351
404
  * Service for tracking hook executions using an append-only log
352
405
  * Prevents duplicate hook actions (e.g., showing design patterns twice for same file)
353
406
  * Each session has its own log file for isolation
@@ -444,8 +497,10 @@ var ExecutionLogService = class ExecutionLogService {
444
497
  const lines = (await node_fs_promises.readFile(this.logFile, "utf-8")).trim().split("\n").filter(Boolean);
445
498
  const entries = [];
446
499
  for (const line of lines) try {
447
- entries.push(JSON.parse(line));
448
- } catch (parseError) {
500
+ const parsed = JSON.parse(line);
501
+ if (isLogEntry(parsed)) entries.push(parsed);
502
+ else console.warn("Skipping malformed log entry:", line.substring(0, 100));
503
+ } catch (_parseError) {
449
504
  console.warn("Skipping malformed log entry:", line.substring(0, 100));
450
505
  }
451
506
  this.cache = entries.slice(-ExecutionLogService.MAX_CACHE_SIZE);
@@ -508,10 +563,10 @@ var ExecutionLogService = class ExecutionLogService {
508
563
  */
509
564
  async getFileMetadata(filePath) {
510
565
  try {
511
- const content = await node_fs_promises.readFile(filePath, "utf-8");
566
+ const [content, stats] = await Promise.all([node_fs_promises.readFile(filePath, "utf-8"), node_fs_promises.stat(filePath)]);
512
567
  const checksum = node_crypto.createHash("md5").update(content).digest("hex");
513
568
  return {
514
- mtime: (await node_fs_promises.stat(filePath)).mtimeMs,
569
+ mtime: stats.mtimeMs,
515
570
  checksum
516
571
  };
517
572
  } catch (error) {
@@ -597,7 +652,7 @@ var ExecutionLogService = class ExecutionLogService {
597
652
  for (let i = entries.length - 1; i >= 0; i--) {
598
653
  const entry = entries[i];
599
654
  if (entry.operation === "scaffold") {
600
- if (entry.generatedFiles && entry.generatedFiles.includes(filePath)) return true;
655
+ if (entry.generatedFiles?.includes(filePath)) return true;
601
656
  }
602
657
  }
603
658
  return false;
@@ -608,6 +663,50 @@ var ExecutionLogService = class ExecutionLogService {
608
663
  }
609
664
  };
610
665
 
666
+ //#endregion
667
+ //#region src/utils/guards.ts
668
+ /**
669
+ * Set of valid decision values for efficient runtime lookup.
670
+ * Mirrors the Decision type: 'allow' | 'deny' | 'ask' | 'skip'
671
+ */
672
+ const VALID_DECISIONS = new Set([
673
+ "allow",
674
+ "deny",
675
+ "ask",
676
+ "skip"
677
+ ]);
678
+ /**
679
+ * Type guard for HookResponse objects.
680
+ * Validates that a value conforms to the HookResponse interface at runtime.
681
+ *
682
+ * @param value - Unknown value to check
683
+ * @returns True if value is a valid HookResponse
684
+ */
685
+ function isHookResponse(value) {
686
+ if (typeof value !== "object" || value === null) return false;
687
+ if (!("decision" in value) || typeof value.decision !== "string") return false;
688
+ if (!VALID_DECISIONS.has(value.decision)) return false;
689
+ if (!("message" in value) || typeof value.message !== "string") return false;
690
+ return true;
691
+ }
692
+ /**
693
+ * Type guard for HookContext objects.
694
+ * Validates that a value conforms to the HookContext interface at runtime.
695
+ * toolInput is shallowly validated because its internal structure varies by tool
696
+ * and is not constrained by the interface.
697
+ *
698
+ * @param value - Unknown value to check
699
+ * @returns True if value is a valid HookContext
700
+ */
701
+ function isHookContext(value) {
702
+ if (typeof value !== "object" || value === null) return false;
703
+ if (!("toolName" in value) || typeof value.toolName !== "string") return false;
704
+ if (!("toolInput" in value) || typeof value.toolInput !== "object" || value.toolInput === null) return false;
705
+ if (!("cwd" in value) || typeof value.cwd !== "string") return false;
706
+ if (!("sessionId" in value) || typeof value.sessionId !== "string") return false;
707
+ return true;
708
+ }
709
+
611
710
  //#endregion
612
711
  //#region src/utils/parseHookType.ts
613
712
  /**
@@ -639,12 +738,19 @@ exports.AFTER_TOOL_USE = AFTER_TOOL_USE;
639
738
  exports.BEFORE_TOOL_USE = BEFORE_TOOL_USE;
640
739
  exports.BaseAdapter = BaseAdapter;
641
740
  exports.ClaudeCodeAdapter = ClaudeCodeAdapter;
741
+ exports.ClaudeCodeHookTypes = ClaudeCodeHookTypes;
642
742
  exports.DECISION_ALLOW = DECISION_ALLOW;
643
743
  exports.DECISION_ASK = DECISION_ASK;
644
744
  exports.DECISION_DENY = DECISION_DENY;
645
745
  exports.DECISION_SKIP = DECISION_SKIP;
646
746
  exports.ExecutionLogService = ExecutionLogService;
647
747
  exports.GeminiCliAdapter = GeminiCliAdapter;
748
+ exports.GeminiCliHookTypes = GeminiCliHookTypes;
648
749
  exports.POST_TOOL_USE = POST_TOOL_USE;
649
750
  exports.PRE_TOOL_USE = PRE_TOOL_USE;
751
+ exports.STOP = STOP;
752
+ exports.TASK_COMPLETED = TASK_COMPLETED;
753
+ exports.USER_PROMPT_SUBMIT = USER_PROMPT_SUBMIT;
754
+ exports.isHookContext = isHookContext;
755
+ exports.isHookResponse = isHookResponse;
650
756
  exports.parseHookType = parseHookType;
package/dist/index.d.cts CHANGED
@@ -5,6 +5,7 @@
5
5
  * DESIGN PATTERNS:
6
6
  * - Strongly-typed constant exports for compile-time safety
7
7
  * - Immutable by default (as const assertions)
8
+ * - Related constants grouped into const objects
8
9
  *
9
10
  * CODING STANDARDS:
10
11
  * - Primitive constants: UPPER_SNAKE_CASE or PascalCase for event names
@@ -15,19 +16,55 @@
15
16
  * - Magic strings without explanation
16
17
  */
17
18
  /**
18
- * Hook type identifiers for Claude Code hook events
19
+ * Grouped hook type identifiers for Claude Code hook events
19
20
  */
20
- declare const PRE_TOOL_USE = "PreToolUse";
21
- declare const POST_TOOL_USE = "PostToolUse";
21
+ declare const ClaudeCodeHookTypes: {
22
+ /** Fires before a tool is executed, allowing interception or modification */
23
+ readonly PRE_TOOL_USE: "PreToolUse";
24
+ /** Fires after a tool has executed, allowing post-processing or blocking */
25
+ readonly POST_TOOL_USE: "PostToolUse";
26
+ /** Fires when a session is about to stop */
27
+ readonly STOP: "Stop";
28
+ /** Fires when the user submits a new prompt */
29
+ readonly USER_PROMPT_SUBMIT: "UserPromptSubmit";
30
+ /** Fires when an agentic task completes */
31
+ readonly TASK_COMPLETED: "TaskCompleted";
32
+ };
22
33
  /**
23
- * Hook type identifiers for Gemini CLI hook events
34
+ * Grouped hook type identifiers for Gemini CLI hook events
24
35
  */
25
- declare const BEFORE_TOOL_USE = "BeforeTool";
26
- declare const AFTER_TOOL_USE = "AfterTool";
36
+ declare const GeminiCliHookTypes: {
37
+ /** Fires before a tool is executed in Gemini CLI */
38
+ readonly BEFORE_TOOL_USE: "BeforeTool";
39
+ /** Fires after a tool is executed in Gemini CLI */
40
+ readonly AFTER_TOOL_USE: "AfterTool";
41
+ };
42
+ /** Hook event fired before a tool is executed in Claude Code */
43
+ declare const PRE_TOOL_USE: "PreToolUse";
44
+ /** Hook event fired after a tool has executed in Claude Code */
45
+ declare const POST_TOOL_USE: "PostToolUse";
46
+ /** Hook event fired when a Claude Code session is about to stop */
47
+ declare const STOP: "Stop";
48
+ /** Hook event fired when the user submits a new prompt in Claude Code */
49
+ declare const USER_PROMPT_SUBMIT: "UserPromptSubmit";
50
+ /** Hook event fired when an agentic task completes in Claude Code */
51
+ declare const TASK_COMPLETED: "TaskCompleted";
52
+ /** Hook event fired before a tool is executed in Gemini CLI */
53
+ declare const BEFORE_TOOL_USE: "BeforeTool";
54
+ /** Hook event fired after a tool is executed in Gemini CLI */
55
+ declare const AFTER_TOOL_USE: "AfterTool";
27
56
  /**
28
- * Union type of all supported hook types
57
+ * Union type of all supported Claude Code hook types
29
58
  */
30
- type HookType = typeof PRE_TOOL_USE | typeof POST_TOOL_USE | typeof BEFORE_TOOL_USE | typeof AFTER_TOOL_USE;
59
+ type ClaudeCodeHookType = (typeof ClaudeCodeHookTypes)[keyof typeof ClaudeCodeHookTypes];
60
+ /**
61
+ * Union type of all supported Gemini CLI hook types
62
+ */
63
+ type GeminiCliHookType = (typeof GeminiCliHookTypes)[keyof typeof GeminiCliHookTypes];
64
+ /**
65
+ * Union type of all supported hook types across all agents
66
+ */
67
+ type HookType = ClaudeCodeHookType | GeminiCliHookType;
31
68
  //#endregion
32
69
  //#region src/types/index.d.ts
33
70
  /**
@@ -85,6 +122,8 @@ interface HookResponse {
85
122
  userMessage?: string;
86
123
  /** Optional updated input parameters for the tool */
87
124
  updatedInput?: Record<string, unknown>;
125
+ /** Optional exit code for process termination (default: 0) */
126
+ exitCode?: number;
88
127
  }
89
128
  /**
90
129
  * Content item in tool result from Claude Code
@@ -102,17 +141,6 @@ interface ToolResult {
102
141
  /** Array of content items returned by the tool */
103
142
  readonly content?: readonly ToolResultContentItem[];
104
143
  }
105
- /**
106
- * Scaffold execution data from execution log
107
- */
108
- interface ScaffoldExecution {
109
- /** Unique scaffold execution ID */
110
- readonly scaffoldId: string;
111
- /** List of files generated by the scaffold */
112
- readonly generatedFiles: readonly string[];
113
- /** Name of the scaffold feature/method */
114
- readonly featureName?: string;
115
- }
116
144
  /**
117
145
  * Log entry structure from ExecutionLogService
118
146
  */
@@ -132,7 +160,7 @@ interface LogEntry {
132
160
  /** File path */
133
161
  readonly filePath: string;
134
162
  /** Decision made */
135
- readonly decision?: string;
163
+ readonly decision?: Decision;
136
164
  /** File pattern matched */
137
165
  readonly filePattern?: string;
138
166
  /** File modification timestamp */
@@ -143,15 +171,16 @@ interface LogEntry {
143
171
  readonly projectPath?: string;
144
172
  }
145
173
  /**
146
- * Pending scaffold log entry for temp file storage
174
+ * Scaffold execution data derived from LogEntry
147
175
  */
148
- interface PendingScaffoldLogEntry {
149
- /** Unique scaffold execution ID */
150
- readonly scaffoldId: string;
151
- /** List of files generated */
152
- readonly generatedFiles: readonly string[];
153
- /** Project path where scaffold was executed */
154
- readonly projectPath: string;
176
+ interface ScaffoldExecution extends Required<Pick<LogEntry, 'scaffoldId' | 'generatedFiles'>> {
177
+ /** Name of the scaffold feature/method */
178
+ readonly featureName?: string;
179
+ }
180
+ /**
181
+ * Pending scaffold log entry for temp file storage, derived from LogEntry
182
+ */
183
+ interface PendingScaffoldLogEntry extends Required<Pick<LogEntry, 'scaffoldId' | 'generatedFiles' | 'projectPath'>> {
155
184
  /** Scaffold feature name */
156
185
  readonly featureName?: string;
157
186
  }
@@ -214,42 +243,67 @@ declare abstract class BaseAdapter<TContext = any> {
214
243
  //#endregion
215
244
  //#region src/adapters/ClaudeCodeAdapter.d.ts
216
245
  /**
217
- * Claude Code hook input format (PreToolUse)
246
+ * Common fields shared by all Claude Code hook inputs
218
247
  */
219
- interface ClaudeCodePreToolUseInput {
220
- tool_name: string;
221
- tool_input: Record<string, any>;
248
+ interface ClaudeCodeCommonFields {
222
249
  cwd: string;
223
250
  session_id: string;
224
- hook_event_name: 'PreToolUse';
225
- tool_use_id: string;
226
251
  transcript_path: string;
227
252
  permission_mode: string;
253
+ }
254
+ /**
255
+ * Claude Code hook input format (PreToolUse)
256
+ */
257
+ interface ClaudeCodePreToolUseInput extends ClaudeCodeCommonFields {
258
+ hook_event_name: 'PreToolUse';
259
+ tool_name: string;
260
+ tool_input: Record<string, any>;
261
+ tool_use_id: string;
228
262
  llm_tool?: string;
229
263
  tool_config?: Record<string, unknown>;
230
264
  }
231
265
  /**
232
266
  * Claude Code hook input format (PostToolUse)
233
267
  */
234
- interface ClaudeCodePostToolUseInput {
268
+ interface ClaudeCodePostToolUseInput extends ClaudeCodeCommonFields {
269
+ hook_event_name: 'PostToolUse';
235
270
  tool_name: string;
236
271
  tool_input: Record<string, any>;
237
272
  tool_response: Record<string, any>;
238
- cwd: string;
239
- session_id: string;
240
- hook_event_name: 'PostToolUse';
241
273
  tool_use_id: string;
242
- transcript_path: string;
243
- permission_mode: string;
244
274
  llm_tool?: string;
245
275
  tool_config?: Record<string, unknown>;
246
276
  }
247
277
  /**
248
- * Union type for both hook input formats
278
+ * Claude Code hook input format (Stop)
249
279
  */
250
- type ClaudeCodeHookInput = ClaudeCodePreToolUseInput | ClaudeCodePostToolUseInput;
280
+ interface ClaudeCodeStopInput extends ClaudeCodeCommonFields {
281
+ hook_event_name: 'Stop';
282
+ stop_hook_active: boolean;
283
+ last_assistant_message: string;
284
+ }
285
+ /**
286
+ * Claude Code hook input format (UserPromptSubmit)
287
+ */
288
+ interface ClaudeCodeUserPromptSubmitInput extends ClaudeCodeCommonFields {
289
+ hook_event_name: 'UserPromptSubmit';
290
+ prompt: string;
291
+ }
292
+ /**
293
+ * Claude Code hook input format (TaskCompleted)
294
+ */
295
+ interface ClaudeCodeTaskCompletedInput extends ClaudeCodeCommonFields {
296
+ hook_event_name: 'TaskCompleted';
297
+ task_id: string;
298
+ task_subject: string;
299
+ task_description: string;
300
+ }
251
301
  /**
252
- * Unified adapter for Claude Code hook format (PreToolUse & PostToolUse)
302
+ * Union type for all hook input formats
303
+ */
304
+ type ClaudeCodeHookInput = ClaudeCodePreToolUseInput | ClaudeCodePostToolUseInput | ClaudeCodeStopInput | ClaudeCodeUserPromptSubmitInput | ClaudeCodeTaskCompletedInput;
305
+ /**
306
+ * Unified adapter for Claude Code hook format
253
307
  */
254
308
  declare class ClaudeCodeAdapter extends BaseAdapter<ClaudeCodeHookInput> {
255
309
  private hookEventName;
@@ -262,7 +316,7 @@ declare class ClaudeCodeAdapter extends BaseAdapter<ClaudeCodeHookInput> {
262
316
  parseInput(stdin: string): ClaudeCodeHookInput;
263
317
  /**
264
318
  * Format normalized HookResponse into Claude Code output
265
- * Morphs output based on hook event type (PreToolUse vs PostToolUse)
319
+ * Morphs output based on hook event type
266
320
  *
267
321
  * @param response - Normalized hook response
268
322
  * @returns JSON string for Claude Code
@@ -276,6 +330,11 @@ declare class ClaudeCodeAdapter extends BaseAdapter<ClaudeCodeHookInput> {
276
330
  * Format PostToolUse output
277
331
  */
278
332
  private formatPostToolUseOutput;
333
+ /**
334
+ * Format blockable output for Stop, UserPromptSubmit, and TaskCompleted hooks
335
+ * Maps 'deny' decision to 'block', otherwise returns empty object
336
+ */
337
+ formatBlockableOutput(response: HookResponse): string;
279
338
  }
280
339
  //#endregion
281
340
  //#region src/adapters/GeminiCliAdapter.d.ts
@@ -326,7 +385,7 @@ interface LogExecutionParams {
326
385
  sessionId: string;
327
386
  filePath: string;
328
387
  operation: string;
329
- decision: string;
388
+ decision: Decision;
330
389
  filePattern?: string;
331
390
  /** File modification timestamp (mtime) at time of execution */
332
391
  fileMtime?: number;
@@ -348,7 +407,7 @@ interface HasExecutedParams {
348
407
  /** File path to check */
349
408
  filePath: string;
350
409
  /** Decision to check for (e.g., 'deny' means we already showed patterns) */
351
- decision: string;
410
+ decision: Decision;
352
411
  /** Optional file pattern to match */
353
412
  filePattern?: string;
354
413
  /** Optional project path to distinguish same patterns in different projects */
@@ -439,7 +498,7 @@ declare class ExecutionLogService {
439
498
  * @param decision - Decision type to check for
440
499
  * @returns true if file has changed or no previous execution found, true on error (fail-open)
441
500
  */
442
- hasFileChanged(filePath: string, decision: string): Promise<boolean>;
501
+ hasFileChanged(filePath: string, decision: Decision): Promise<boolean>;
443
502
  /**
444
503
  * Check if file was recently reviewed (within debounce window)
445
504
  * Prevents noisy feedback during rapid successive edits
@@ -465,6 +524,26 @@ declare class ExecutionLogService {
465
524
  wasGeneratedByScaffold(filePath: string): Promise<boolean>;
466
525
  }
467
526
  //#endregion
527
+ //#region src/utils/guards.d.ts
528
+ /**
529
+ * Type guard for HookResponse objects.
530
+ * Validates that a value conforms to the HookResponse interface at runtime.
531
+ *
532
+ * @param value - Unknown value to check
533
+ * @returns True if value is a valid HookResponse
534
+ */
535
+ declare function isHookResponse(value: unknown): value is HookResponse;
536
+ /**
537
+ * Type guard for HookContext objects.
538
+ * Validates that a value conforms to the HookContext interface at runtime.
539
+ * toolInput is shallowly validated because its internal structure varies by tool
540
+ * and is not constrained by the interface.
541
+ *
542
+ * @param value - Unknown value to check
543
+ * @returns True if value is a valid HookContext
544
+ */
545
+ declare function isHookContext(value: unknown): value is HookContext;
546
+ //#endregion
468
547
  //#region src/utils/parseHookType.d.ts
469
548
  /**
470
549
  * parseHookType Utilities
@@ -513,4 +592,4 @@ interface ParsedHookType {
513
592
  */
514
593
  declare function parseHookType(hookType: string): ParsedHookType;
515
594
  //#endregion
516
- export { AFTER_TOOL_USE, BEFORE_TOOL_USE, BaseAdapter, ClaudeCodeAdapter, ClaudeCodeHookInput, ClaudeCodePostToolUseInput, ClaudeCodePreToolUseInput, DECISION_ALLOW, DECISION_ASK, DECISION_DENY, DECISION_SKIP, Decision, ExecutionLogService, GeminiCliAdapter, GeminiCliHookInput, HasExecutedParams, HookContext, HookResponse, HookType, LogEntry, LogExecutionParams, LogStats, POST_TOOL_USE, PRE_TOOL_USE, ParsedHookType, PendingScaffoldLogEntry, ScaffoldExecution, ToolResult, ToolResultContentItem, parseHookType };
595
+ export { AFTER_TOOL_USE, BEFORE_TOOL_USE, BaseAdapter, ClaudeCodeAdapter, ClaudeCodeHookInput, ClaudeCodeHookType, ClaudeCodeHookTypes, ClaudeCodePostToolUseInput, ClaudeCodePreToolUseInput, ClaudeCodeStopInput, ClaudeCodeTaskCompletedInput, ClaudeCodeUserPromptSubmitInput, DECISION_ALLOW, DECISION_ASK, DECISION_DENY, DECISION_SKIP, Decision, ExecutionLogService, GeminiCliAdapter, GeminiCliHookInput, GeminiCliHookType, GeminiCliHookTypes, HasExecutedParams, HookContext, HookResponse, HookType, LogEntry, LogExecutionParams, LogStats, POST_TOOL_USE, PRE_TOOL_USE, PendingScaffoldLogEntry, STOP, ScaffoldExecution, TASK_COMPLETED, ToolResult, ToolResultContentItem, USER_PROMPT_SUBMIT, isHookContext, isHookResponse, parseHookType };
package/dist/index.d.mts CHANGED
@@ -5,6 +5,7 @@
5
5
  * DESIGN PATTERNS:
6
6
  * - Strongly-typed constant exports for compile-time safety
7
7
  * - Immutable by default (as const assertions)
8
+ * - Related constants grouped into const objects
8
9
  *
9
10
  * CODING STANDARDS:
10
11
  * - Primitive constants: UPPER_SNAKE_CASE or PascalCase for event names
@@ -15,19 +16,55 @@
15
16
  * - Magic strings without explanation
16
17
  */
17
18
  /**
18
- * Hook type identifiers for Claude Code hook events
19
+ * Grouped hook type identifiers for Claude Code hook events
19
20
  */
20
- declare const PRE_TOOL_USE = "PreToolUse";
21
- declare const POST_TOOL_USE = "PostToolUse";
21
+ declare const ClaudeCodeHookTypes: {
22
+ /** Fires before a tool is executed, allowing interception or modification */
23
+ readonly PRE_TOOL_USE: "PreToolUse";
24
+ /** Fires after a tool has executed, allowing post-processing or blocking */
25
+ readonly POST_TOOL_USE: "PostToolUse";
26
+ /** Fires when a session is about to stop */
27
+ readonly STOP: "Stop";
28
+ /** Fires when the user submits a new prompt */
29
+ readonly USER_PROMPT_SUBMIT: "UserPromptSubmit";
30
+ /** Fires when an agentic task completes */
31
+ readonly TASK_COMPLETED: "TaskCompleted";
32
+ };
22
33
  /**
23
- * Hook type identifiers for Gemini CLI hook events
34
+ * Grouped hook type identifiers for Gemini CLI hook events
24
35
  */
25
- declare const BEFORE_TOOL_USE = "BeforeTool";
26
- declare const AFTER_TOOL_USE = "AfterTool";
36
+ declare const GeminiCliHookTypes: {
37
+ /** Fires before a tool is executed in Gemini CLI */
38
+ readonly BEFORE_TOOL_USE: "BeforeTool";
39
+ /** Fires after a tool is executed in Gemini CLI */
40
+ readonly AFTER_TOOL_USE: "AfterTool";
41
+ };
42
+ /** Hook event fired before a tool is executed in Claude Code */
43
+ declare const PRE_TOOL_USE: "PreToolUse";
44
+ /** Hook event fired after a tool has executed in Claude Code */
45
+ declare const POST_TOOL_USE: "PostToolUse";
46
+ /** Hook event fired when a Claude Code session is about to stop */
47
+ declare const STOP: "Stop";
48
+ /** Hook event fired when the user submits a new prompt in Claude Code */
49
+ declare const USER_PROMPT_SUBMIT: "UserPromptSubmit";
50
+ /** Hook event fired when an agentic task completes in Claude Code */
51
+ declare const TASK_COMPLETED: "TaskCompleted";
52
+ /** Hook event fired before a tool is executed in Gemini CLI */
53
+ declare const BEFORE_TOOL_USE: "BeforeTool";
54
+ /** Hook event fired after a tool is executed in Gemini CLI */
55
+ declare const AFTER_TOOL_USE: "AfterTool";
27
56
  /**
28
- * Union type of all supported hook types
57
+ * Union type of all supported Claude Code hook types
29
58
  */
30
- type HookType = typeof PRE_TOOL_USE | typeof POST_TOOL_USE | typeof BEFORE_TOOL_USE | typeof AFTER_TOOL_USE;
59
+ type ClaudeCodeHookType = (typeof ClaudeCodeHookTypes)[keyof typeof ClaudeCodeHookTypes];
60
+ /**
61
+ * Union type of all supported Gemini CLI hook types
62
+ */
63
+ type GeminiCliHookType = (typeof GeminiCliHookTypes)[keyof typeof GeminiCliHookTypes];
64
+ /**
65
+ * Union type of all supported hook types across all agents
66
+ */
67
+ type HookType = ClaudeCodeHookType | GeminiCliHookType;
31
68
  //#endregion
32
69
  //#region src/types/index.d.ts
33
70
  /**
@@ -85,6 +122,8 @@ interface HookResponse {
85
122
  userMessage?: string;
86
123
  /** Optional updated input parameters for the tool */
87
124
  updatedInput?: Record<string, unknown>;
125
+ /** Optional exit code for process termination (default: 0) */
126
+ exitCode?: number;
88
127
  }
89
128
  /**
90
129
  * Content item in tool result from Claude Code
@@ -102,17 +141,6 @@ interface ToolResult {
102
141
  /** Array of content items returned by the tool */
103
142
  readonly content?: readonly ToolResultContentItem[];
104
143
  }
105
- /**
106
- * Scaffold execution data from execution log
107
- */
108
- interface ScaffoldExecution {
109
- /** Unique scaffold execution ID */
110
- readonly scaffoldId: string;
111
- /** List of files generated by the scaffold */
112
- readonly generatedFiles: readonly string[];
113
- /** Name of the scaffold feature/method */
114
- readonly featureName?: string;
115
- }
116
144
  /**
117
145
  * Log entry structure from ExecutionLogService
118
146
  */
@@ -132,7 +160,7 @@ interface LogEntry {
132
160
  /** File path */
133
161
  readonly filePath: string;
134
162
  /** Decision made */
135
- readonly decision?: string;
163
+ readonly decision?: Decision;
136
164
  /** File pattern matched */
137
165
  readonly filePattern?: string;
138
166
  /** File modification timestamp */
@@ -143,15 +171,16 @@ interface LogEntry {
143
171
  readonly projectPath?: string;
144
172
  }
145
173
  /**
146
- * Pending scaffold log entry for temp file storage
174
+ * Scaffold execution data derived from LogEntry
147
175
  */
148
- interface PendingScaffoldLogEntry {
149
- /** Unique scaffold execution ID */
150
- readonly scaffoldId: string;
151
- /** List of files generated */
152
- readonly generatedFiles: readonly string[];
153
- /** Project path where scaffold was executed */
154
- readonly projectPath: string;
176
+ interface ScaffoldExecution extends Required<Pick<LogEntry, 'scaffoldId' | 'generatedFiles'>> {
177
+ /** Name of the scaffold feature/method */
178
+ readonly featureName?: string;
179
+ }
180
+ /**
181
+ * Pending scaffold log entry for temp file storage, derived from LogEntry
182
+ */
183
+ interface PendingScaffoldLogEntry extends Required<Pick<LogEntry, 'scaffoldId' | 'generatedFiles' | 'projectPath'>> {
155
184
  /** Scaffold feature name */
156
185
  readonly featureName?: string;
157
186
  }
@@ -214,42 +243,67 @@ declare abstract class BaseAdapter<TContext = any> {
214
243
  //#endregion
215
244
  //#region src/adapters/ClaudeCodeAdapter.d.ts
216
245
  /**
217
- * Claude Code hook input format (PreToolUse)
246
+ * Common fields shared by all Claude Code hook inputs
218
247
  */
219
- interface ClaudeCodePreToolUseInput {
220
- tool_name: string;
221
- tool_input: Record<string, any>;
248
+ interface ClaudeCodeCommonFields {
222
249
  cwd: string;
223
250
  session_id: string;
224
- hook_event_name: 'PreToolUse';
225
- tool_use_id: string;
226
251
  transcript_path: string;
227
252
  permission_mode: string;
253
+ }
254
+ /**
255
+ * Claude Code hook input format (PreToolUse)
256
+ */
257
+ interface ClaudeCodePreToolUseInput extends ClaudeCodeCommonFields {
258
+ hook_event_name: 'PreToolUse';
259
+ tool_name: string;
260
+ tool_input: Record<string, any>;
261
+ tool_use_id: string;
228
262
  llm_tool?: string;
229
263
  tool_config?: Record<string, unknown>;
230
264
  }
231
265
  /**
232
266
  * Claude Code hook input format (PostToolUse)
233
267
  */
234
- interface ClaudeCodePostToolUseInput {
268
+ interface ClaudeCodePostToolUseInput extends ClaudeCodeCommonFields {
269
+ hook_event_name: 'PostToolUse';
235
270
  tool_name: string;
236
271
  tool_input: Record<string, any>;
237
272
  tool_response: Record<string, any>;
238
- cwd: string;
239
- session_id: string;
240
- hook_event_name: 'PostToolUse';
241
273
  tool_use_id: string;
242
- transcript_path: string;
243
- permission_mode: string;
244
274
  llm_tool?: string;
245
275
  tool_config?: Record<string, unknown>;
246
276
  }
247
277
  /**
248
- * Union type for both hook input formats
278
+ * Claude Code hook input format (Stop)
249
279
  */
250
- type ClaudeCodeHookInput = ClaudeCodePreToolUseInput | ClaudeCodePostToolUseInput;
280
+ interface ClaudeCodeStopInput extends ClaudeCodeCommonFields {
281
+ hook_event_name: 'Stop';
282
+ stop_hook_active: boolean;
283
+ last_assistant_message: string;
284
+ }
285
+ /**
286
+ * Claude Code hook input format (UserPromptSubmit)
287
+ */
288
+ interface ClaudeCodeUserPromptSubmitInput extends ClaudeCodeCommonFields {
289
+ hook_event_name: 'UserPromptSubmit';
290
+ prompt: string;
291
+ }
292
+ /**
293
+ * Claude Code hook input format (TaskCompleted)
294
+ */
295
+ interface ClaudeCodeTaskCompletedInput extends ClaudeCodeCommonFields {
296
+ hook_event_name: 'TaskCompleted';
297
+ task_id: string;
298
+ task_subject: string;
299
+ task_description: string;
300
+ }
251
301
  /**
252
- * Unified adapter for Claude Code hook format (PreToolUse & PostToolUse)
302
+ * Union type for all hook input formats
303
+ */
304
+ type ClaudeCodeHookInput = ClaudeCodePreToolUseInput | ClaudeCodePostToolUseInput | ClaudeCodeStopInput | ClaudeCodeUserPromptSubmitInput | ClaudeCodeTaskCompletedInput;
305
+ /**
306
+ * Unified adapter for Claude Code hook format
253
307
  */
254
308
  declare class ClaudeCodeAdapter extends BaseAdapter<ClaudeCodeHookInput> {
255
309
  private hookEventName;
@@ -262,7 +316,7 @@ declare class ClaudeCodeAdapter extends BaseAdapter<ClaudeCodeHookInput> {
262
316
  parseInput(stdin: string): ClaudeCodeHookInput;
263
317
  /**
264
318
  * Format normalized HookResponse into Claude Code output
265
- * Morphs output based on hook event type (PreToolUse vs PostToolUse)
319
+ * Morphs output based on hook event type
266
320
  *
267
321
  * @param response - Normalized hook response
268
322
  * @returns JSON string for Claude Code
@@ -276,6 +330,11 @@ declare class ClaudeCodeAdapter extends BaseAdapter<ClaudeCodeHookInput> {
276
330
  * Format PostToolUse output
277
331
  */
278
332
  private formatPostToolUseOutput;
333
+ /**
334
+ * Format blockable output for Stop, UserPromptSubmit, and TaskCompleted hooks
335
+ * Maps 'deny' decision to 'block', otherwise returns empty object
336
+ */
337
+ formatBlockableOutput(response: HookResponse): string;
279
338
  }
280
339
  //#endregion
281
340
  //#region src/adapters/GeminiCliAdapter.d.ts
@@ -326,7 +385,7 @@ interface LogExecutionParams {
326
385
  sessionId: string;
327
386
  filePath: string;
328
387
  operation: string;
329
- decision: string;
388
+ decision: Decision;
330
389
  filePattern?: string;
331
390
  /** File modification timestamp (mtime) at time of execution */
332
391
  fileMtime?: number;
@@ -348,7 +407,7 @@ interface HasExecutedParams {
348
407
  /** File path to check */
349
408
  filePath: string;
350
409
  /** Decision to check for (e.g., 'deny' means we already showed patterns) */
351
- decision: string;
410
+ decision: Decision;
352
411
  /** Optional file pattern to match */
353
412
  filePattern?: string;
354
413
  /** Optional project path to distinguish same patterns in different projects */
@@ -439,7 +498,7 @@ declare class ExecutionLogService {
439
498
  * @param decision - Decision type to check for
440
499
  * @returns true if file has changed or no previous execution found, true on error (fail-open)
441
500
  */
442
- hasFileChanged(filePath: string, decision: string): Promise<boolean>;
501
+ hasFileChanged(filePath: string, decision: Decision): Promise<boolean>;
443
502
  /**
444
503
  * Check if file was recently reviewed (within debounce window)
445
504
  * Prevents noisy feedback during rapid successive edits
@@ -465,6 +524,26 @@ declare class ExecutionLogService {
465
524
  wasGeneratedByScaffold(filePath: string): Promise<boolean>;
466
525
  }
467
526
  //#endregion
527
+ //#region src/utils/guards.d.ts
528
+ /**
529
+ * Type guard for HookResponse objects.
530
+ * Validates that a value conforms to the HookResponse interface at runtime.
531
+ *
532
+ * @param value - Unknown value to check
533
+ * @returns True if value is a valid HookResponse
534
+ */
535
+ declare function isHookResponse(value: unknown): value is HookResponse;
536
+ /**
537
+ * Type guard for HookContext objects.
538
+ * Validates that a value conforms to the HookContext interface at runtime.
539
+ * toolInput is shallowly validated because its internal structure varies by tool
540
+ * and is not constrained by the interface.
541
+ *
542
+ * @param value - Unknown value to check
543
+ * @returns True if value is a valid HookContext
544
+ */
545
+ declare function isHookContext(value: unknown): value is HookContext;
546
+ //#endregion
468
547
  //#region src/utils/parseHookType.d.ts
469
548
  /**
470
549
  * parseHookType Utilities
@@ -513,4 +592,4 @@ interface ParsedHookType {
513
592
  */
514
593
  declare function parseHookType(hookType: string): ParsedHookType;
515
594
  //#endregion
516
- export { AFTER_TOOL_USE, BEFORE_TOOL_USE, BaseAdapter, ClaudeCodeAdapter, ClaudeCodeHookInput, ClaudeCodePostToolUseInput, ClaudeCodePreToolUseInput, DECISION_ALLOW, DECISION_ASK, DECISION_DENY, DECISION_SKIP, Decision, ExecutionLogService, GeminiCliAdapter, GeminiCliHookInput, HasExecutedParams, HookContext, HookResponse, HookType, LogEntry, LogExecutionParams, LogStats, POST_TOOL_USE, PRE_TOOL_USE, ParsedHookType, PendingScaffoldLogEntry, ScaffoldExecution, ToolResult, ToolResultContentItem, parseHookType };
595
+ export { AFTER_TOOL_USE, BEFORE_TOOL_USE, BaseAdapter, ClaudeCodeAdapter, ClaudeCodeHookInput, ClaudeCodeHookType, ClaudeCodeHookTypes, ClaudeCodePostToolUseInput, ClaudeCodePreToolUseInput, ClaudeCodeStopInput, ClaudeCodeTaskCompletedInput, ClaudeCodeUserPromptSubmitInput, DECISION_ALLOW, DECISION_ASK, DECISION_DENY, DECISION_SKIP, Decision, ExecutionLogService, GeminiCliAdapter, GeminiCliHookInput, GeminiCliHookType, GeminiCliHookTypes, HasExecutedParams, HookContext, HookResponse, HookType, LogEntry, LogExecutionParams, LogStats, POST_TOOL_USE, PRE_TOOL_USE, PendingScaffoldLogEntry, STOP, ScaffoldExecution, TASK_COMPLETED, ToolResult, ToolResultContentItem, USER_PROMPT_SUBMIT, isHookContext, isHookResponse, parseHookType };
package/dist/index.mjs CHANGED
@@ -11,6 +11,7 @@ import * as crypto from "node:crypto";
11
11
  * DESIGN PATTERNS:
12
12
  * - Strongly-typed constant exports for compile-time safety
13
13
  * - Immutable by default (as const assertions)
14
+ * - Related constants grouped into const objects
14
15
  *
15
16
  * CODING STANDARDS:
16
17
  * - Primitive constants: UPPER_SNAKE_CASE or PascalCase for event names
@@ -21,15 +22,36 @@ import * as crypto from "node:crypto";
21
22
  * - Magic strings without explanation
22
23
  */
23
24
  /**
24
- * Hook type identifiers for Claude Code hook events
25
+ * Grouped hook type identifiers for Claude Code hook events
25
26
  */
26
- const PRE_TOOL_USE = "PreToolUse";
27
- const POST_TOOL_USE = "PostToolUse";
27
+ const ClaudeCodeHookTypes = {
28
+ PRE_TOOL_USE: "PreToolUse",
29
+ POST_TOOL_USE: "PostToolUse",
30
+ STOP: "Stop",
31
+ USER_PROMPT_SUBMIT: "UserPromptSubmit",
32
+ TASK_COMPLETED: "TaskCompleted"
33
+ };
28
34
  /**
29
- * Hook type identifiers for Gemini CLI hook events
35
+ * Grouped hook type identifiers for Gemini CLI hook events
30
36
  */
31
- const BEFORE_TOOL_USE = "BeforeTool";
32
- const AFTER_TOOL_USE = "AfterTool";
37
+ const GeminiCliHookTypes = {
38
+ BEFORE_TOOL_USE: "BeforeTool",
39
+ AFTER_TOOL_USE: "AfterTool"
40
+ };
41
+ /** Hook event fired before a tool is executed in Claude Code */
42
+ const PRE_TOOL_USE = ClaudeCodeHookTypes.PRE_TOOL_USE;
43
+ /** Hook event fired after a tool has executed in Claude Code */
44
+ const POST_TOOL_USE = ClaudeCodeHookTypes.POST_TOOL_USE;
45
+ /** Hook event fired when a Claude Code session is about to stop */
46
+ const STOP = ClaudeCodeHookTypes.STOP;
47
+ /** Hook event fired when the user submits a new prompt in Claude Code */
48
+ const USER_PROMPT_SUBMIT = ClaudeCodeHookTypes.USER_PROMPT_SUBMIT;
49
+ /** Hook event fired when an agentic task completes in Claude Code */
50
+ const TASK_COMPLETED = ClaudeCodeHookTypes.TASK_COMPLETED;
51
+ /** Hook event fired before a tool is executed in Gemini CLI */
52
+ const BEFORE_TOOL_USE = GeminiCliHookTypes.BEFORE_TOOL_USE;
53
+ /** Hook event fired after a tool is executed in Gemini CLI */
54
+ const AFTER_TOOL_USE = GeminiCliHookTypes.AFTER_TOOL_USE;
33
55
 
34
56
  //#endregion
35
57
  //#region src/constants/decisions.ts
@@ -60,12 +82,13 @@ var BaseAdapter = class {
60
82
  const stdin = await this.readStdin();
61
83
  const response = await callback(this.parseInput(stdin));
62
84
  if (response.decision === "skip") {
63
- process.exit(0);
85
+ process.exit(response.exitCode ?? 0);
64
86
  return;
65
87
  }
88
+ if (response.userMessage) process.stderr.write(`${response.userMessage}\n`);
66
89
  const output = this.formatOutput(response);
67
90
  console.log(output);
68
- process.exit(0);
91
+ process.exit(response.exitCode ?? 0);
69
92
  } catch (error) {
70
93
  this.handleError(error);
71
94
  }
@@ -94,9 +117,10 @@ var BaseAdapter = class {
94
117
  process.exit(0);
95
118
  return;
96
119
  }
120
+ if (finalResponse.userMessage) process.stderr.write(`${finalResponse.userMessage}\n`);
97
121
  const output = this.formatOutput(finalResponse);
98
122
  console.log(output);
99
- process.exit(0);
123
+ process.exit(finalResponse.exitCode ?? 0);
100
124
  } catch (error) {
101
125
  this.handleError(error);
102
126
  }
@@ -139,18 +163,18 @@ var BaseAdapter = class {
139
163
  //#endregion
140
164
  //#region src/adapters/ClaudeCodeAdapter.ts
141
165
  /**
142
- * ClaudeCodeAdapter - Unified adapter for Claude Code hook format (PreToolUse & PostToolUse)
166
+ * ClaudeCodeAdapter - Unified adapter for Claude Code hook format
143
167
  *
144
168
  * DESIGN PATTERNS:
145
169
  * - Adapter pattern: Converts Claude Code format to normalized format
146
170
  * - Parser pattern: Extracts file paths and operations from tool inputs
147
- * - State pattern: Stores hook event type to morph behavior between PreToolUse and PostToolUse
171
+ * - State pattern: Stores hook event type to morph behavior between hook events
148
172
  *
149
173
  * CODING STANDARDS:
150
174
  * - Parse Claude Code JSON stdin format exactly as specified
151
175
  * - Format output to match Claude Code hook response schema
152
176
  * - Handle missing/optional fields gracefully
153
- * - Support both PreToolUse and PostToolUse events in one adapter
177
+ * - Support PreToolUse, PostToolUse, Stop, UserPromptSubmit, TaskCompleted events
154
178
  *
155
179
  * AVOID:
156
180
  * - Assuming all fields are present
@@ -158,7 +182,15 @@ var BaseAdapter = class {
158
182
  * - Mutating input objects
159
183
  */
160
184
  /**
161
- * Unified adapter for Claude Code hook format (PreToolUse & PostToolUse)
185
+ * Blockable hook event names
186
+ */
187
+ const BLOCKABLE_EVENTS = new Set([
188
+ "Stop",
189
+ "UserPromptSubmit",
190
+ "TaskCompleted"
191
+ ]);
192
+ /**
193
+ * Unified adapter for Claude Code hook format
162
194
  */
163
195
  var ClaudeCodeAdapter = class extends BaseAdapter {
164
196
  hookEventName = "PreToolUse";
@@ -180,7 +212,7 @@ var ClaudeCodeAdapter = class extends BaseAdapter {
180
212
  }
181
213
  /**
182
214
  * Format normalized HookResponse into Claude Code output
183
- * Morphs output based on hook event type (PreToolUse vs PostToolUse)
215
+ * Morphs output based on hook event type
184
216
  *
185
217
  * @param response - Normalized hook response
186
218
  * @returns JSON string for Claude Code
@@ -195,6 +227,7 @@ var ClaudeCodeAdapter = class extends BaseAdapter {
195
227
  log.debug("ClaudeCodeAdapter: Skip decision, returning empty output");
196
228
  return emptyOutput;
197
229
  }
230
+ if (BLOCKABLE_EVENTS.has(this.hookEventName)) return this.formatBlockableOutput(response);
198
231
  if (this.hookEventName === "PostToolUse") return this.formatPostToolUseOutput(response);
199
232
  return this.formatPreToolUseOutput(response);
200
233
  }
@@ -227,6 +260,20 @@ var ClaudeCodeAdapter = class extends BaseAdapter {
227
260
  log.debug("ClaudeCodeAdapter: Formatted PostToolUse output", { output: formattedOutput });
228
261
  return formattedOutput;
229
262
  }
263
+ /**
264
+ * Format blockable output for Stop, UserPromptSubmit, and TaskCompleted hooks
265
+ * Maps 'deny' decision to 'block', otherwise returns empty object
266
+ */
267
+ formatBlockableOutput(response) {
268
+ const output = {};
269
+ if (response.decision === "deny") {
270
+ output.decision = "block";
271
+ output.reason = response.message;
272
+ }
273
+ const formattedOutput = JSON.stringify(output, null, 2);
274
+ log.debug(`ClaudeCodeAdapter: Formatted ${this.hookEventName} output`, { output: formattedOutput });
275
+ return formattedOutput;
276
+ }
230
277
  };
231
278
 
232
279
  //#endregion
@@ -317,6 +364,12 @@ function isNodeError(error) {
317
364
  return error instanceof Error && "code" in error;
318
365
  }
319
366
  /**
367
+ * Type guard for LogEntry — validates required fields at runtime
368
+ */
369
+ function isLogEntry(value) {
370
+ return typeof value === "object" && value !== null && "filePath" in value && typeof value.filePath === "string" && "operation" in value && typeof value.operation === "string";
371
+ }
372
+ /**
320
373
  * Service for tracking hook executions using an append-only log
321
374
  * Prevents duplicate hook actions (e.g., showing design patterns twice for same file)
322
375
  * Each session has its own log file for isolation
@@ -413,8 +466,10 @@ var ExecutionLogService = class ExecutionLogService {
413
466
  const lines = (await fs.readFile(this.logFile, "utf-8")).trim().split("\n").filter(Boolean);
414
467
  const entries = [];
415
468
  for (const line of lines) try {
416
- entries.push(JSON.parse(line));
417
- } catch (parseError) {
469
+ const parsed = JSON.parse(line);
470
+ if (isLogEntry(parsed)) entries.push(parsed);
471
+ else console.warn("Skipping malformed log entry:", line.substring(0, 100));
472
+ } catch (_parseError) {
418
473
  console.warn("Skipping malformed log entry:", line.substring(0, 100));
419
474
  }
420
475
  this.cache = entries.slice(-ExecutionLogService.MAX_CACHE_SIZE);
@@ -477,10 +532,10 @@ var ExecutionLogService = class ExecutionLogService {
477
532
  */
478
533
  async getFileMetadata(filePath) {
479
534
  try {
480
- const content = await fs.readFile(filePath, "utf-8");
535
+ const [content, stats] = await Promise.all([fs.readFile(filePath, "utf-8"), fs.stat(filePath)]);
481
536
  const checksum = crypto.createHash("md5").update(content).digest("hex");
482
537
  return {
483
- mtime: (await fs.stat(filePath)).mtimeMs,
538
+ mtime: stats.mtimeMs,
484
539
  checksum
485
540
  };
486
541
  } catch (error) {
@@ -566,7 +621,7 @@ var ExecutionLogService = class ExecutionLogService {
566
621
  for (let i = entries.length - 1; i >= 0; i--) {
567
622
  const entry = entries[i];
568
623
  if (entry.operation === "scaffold") {
569
- if (entry.generatedFiles && entry.generatedFiles.includes(filePath)) return true;
624
+ if (entry.generatedFiles?.includes(filePath)) return true;
570
625
  }
571
626
  }
572
627
  return false;
@@ -577,6 +632,50 @@ var ExecutionLogService = class ExecutionLogService {
577
632
  }
578
633
  };
579
634
 
635
+ //#endregion
636
+ //#region src/utils/guards.ts
637
+ /**
638
+ * Set of valid decision values for efficient runtime lookup.
639
+ * Mirrors the Decision type: 'allow' | 'deny' | 'ask' | 'skip'
640
+ */
641
+ const VALID_DECISIONS = new Set([
642
+ "allow",
643
+ "deny",
644
+ "ask",
645
+ "skip"
646
+ ]);
647
+ /**
648
+ * Type guard for HookResponse objects.
649
+ * Validates that a value conforms to the HookResponse interface at runtime.
650
+ *
651
+ * @param value - Unknown value to check
652
+ * @returns True if value is a valid HookResponse
653
+ */
654
+ function isHookResponse(value) {
655
+ if (typeof value !== "object" || value === null) return false;
656
+ if (!("decision" in value) || typeof value.decision !== "string") return false;
657
+ if (!VALID_DECISIONS.has(value.decision)) return false;
658
+ if (!("message" in value) || typeof value.message !== "string") return false;
659
+ return true;
660
+ }
661
+ /**
662
+ * Type guard for HookContext objects.
663
+ * Validates that a value conforms to the HookContext interface at runtime.
664
+ * toolInput is shallowly validated because its internal structure varies by tool
665
+ * and is not constrained by the interface.
666
+ *
667
+ * @param value - Unknown value to check
668
+ * @returns True if value is a valid HookContext
669
+ */
670
+ function isHookContext(value) {
671
+ if (typeof value !== "object" || value === null) return false;
672
+ if (!("toolName" in value) || typeof value.toolName !== "string") return false;
673
+ if (!("toolInput" in value) || typeof value.toolInput !== "object" || value.toolInput === null) return false;
674
+ if (!("cwd" in value) || typeof value.cwd !== "string") return false;
675
+ if (!("sessionId" in value) || typeof value.sessionId !== "string") return false;
676
+ return true;
677
+ }
678
+
580
679
  //#endregion
581
680
  //#region src/utils/parseHookType.ts
582
681
  /**
@@ -604,4 +703,4 @@ function parseHookType(hookType) {
604
703
  }
605
704
 
606
705
  //#endregion
607
- export { AFTER_TOOL_USE, BEFORE_TOOL_USE, BaseAdapter, ClaudeCodeAdapter, DECISION_ALLOW, DECISION_ASK, DECISION_DENY, DECISION_SKIP, ExecutionLogService, GeminiCliAdapter, POST_TOOL_USE, PRE_TOOL_USE, parseHookType };
706
+ export { AFTER_TOOL_USE, BEFORE_TOOL_USE, BaseAdapter, ClaudeCodeAdapter, ClaudeCodeHookTypes, DECISION_ALLOW, DECISION_ASK, DECISION_DENY, DECISION_SKIP, ExecutionLogService, GeminiCliAdapter, GeminiCliHookTypes, POST_TOOL_USE, PRE_TOOL_USE, STOP, TASK_COMPLETED, USER_PROMPT_SUBMIT, isHookContext, isHookResponse, parseHookType };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@agiflowai/hooks-adapter",
3
3
  "description": "Hook adapters for normalizing AI agent hook formats (Claude Code, Gemini, etc.)",
4
- "version": "0.0.14",
4
+ "version": "0.0.15",
5
5
  "license": "AGPL-3.0",
6
6
  "author": "AgiflowIO",
7
7
  "repository": {
@@ -25,8 +25,8 @@
25
25
  "README.md"
26
26
  ],
27
27
  "dependencies": {
28
- "@agiflowai/aicode-utils": "1.0.13",
29
- "@agiflowai/coding-agent-bridge": "1.0.16"
28
+ "@agiflowai/aicode-utils": "1.0.14",
29
+ "@agiflowai/coding-agent-bridge": "1.0.17"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/node": "^22.0.0",