source dump of claude code
at main 192 lines 4.5 kB view raw
1/** 2 * Hook event system for broadcasting hook execution events. 3 * 4 * This module provides a generic event system that is separate from the 5 * main message stream. Handlers can register to receive events and decide 6 * what to do with them (e.g., convert to SDK messages, log, etc.). 7 */ 8 9import { HOOK_EVENTS } from 'src/entrypoints/sdk/coreTypes.js' 10 11import { logForDebugging } from '../debug.js' 12 13/** 14 * Hook events that are always emitted regardless of the includeHookEvents 15 * option. These are low-noise lifecycle events that were in the original 16 * allowlist and are backwards-compatible. 17 */ 18const ALWAYS_EMITTED_HOOK_EVENTS = ['SessionStart', 'Setup'] as const 19 20const MAX_PENDING_EVENTS = 100 21 22export type HookStartedEvent = { 23 type: 'started' 24 hookId: string 25 hookName: string 26 hookEvent: string 27} 28 29export type HookProgressEvent = { 30 type: 'progress' 31 hookId: string 32 hookName: string 33 hookEvent: string 34 stdout: string 35 stderr: string 36 output: string 37} 38 39export type HookResponseEvent = { 40 type: 'response' 41 hookId: string 42 hookName: string 43 hookEvent: string 44 output: string 45 stdout: string 46 stderr: string 47 exitCode?: number 48 outcome: 'success' | 'error' | 'cancelled' 49} 50 51export type HookExecutionEvent = 52 | HookStartedEvent 53 | HookProgressEvent 54 | HookResponseEvent 55export type HookEventHandler = (event: HookExecutionEvent) => void 56 57const pendingEvents: HookExecutionEvent[] = [] 58let eventHandler: HookEventHandler | null = null 59let allHookEventsEnabled = false 60 61export function registerHookEventHandler( 62 handler: HookEventHandler | null, 63): void { 64 eventHandler = handler 65 if (handler && pendingEvents.length > 0) { 66 for (const event of pendingEvents.splice(0)) { 67 handler(event) 68 } 69 } 70} 71 72function emit(event: HookExecutionEvent): void { 73 if (eventHandler) { 74 eventHandler(event) 75 } else { 76 pendingEvents.push(event) 77 if (pendingEvents.length > MAX_PENDING_EVENTS) { 78 pendingEvents.shift() 79 } 80 } 81} 82 83function shouldEmit(hookEvent: string): boolean { 84 if ((ALWAYS_EMITTED_HOOK_EVENTS as readonly string[]).includes(hookEvent)) { 85 return true 86 } 87 return ( 88 allHookEventsEnabled && 89 (HOOK_EVENTS as readonly string[]).includes(hookEvent) 90 ) 91} 92 93export function emitHookStarted( 94 hookId: string, 95 hookName: string, 96 hookEvent: string, 97): void { 98 if (!shouldEmit(hookEvent)) return 99 100 emit({ 101 type: 'started', 102 hookId, 103 hookName, 104 hookEvent, 105 }) 106} 107 108export function emitHookProgress(data: { 109 hookId: string 110 hookName: string 111 hookEvent: string 112 stdout: string 113 stderr: string 114 output: string 115}): void { 116 if (!shouldEmit(data.hookEvent)) return 117 118 emit({ 119 type: 'progress', 120 ...data, 121 }) 122} 123 124export function startHookProgressInterval(params: { 125 hookId: string 126 hookName: string 127 hookEvent: string 128 getOutput: () => Promise<{ stdout: string; stderr: string; output: string }> 129 intervalMs?: number 130}): () => void { 131 if (!shouldEmit(params.hookEvent)) return () => {} 132 133 let lastEmittedOutput = '' 134 const interval = setInterval(() => { 135 void params.getOutput().then(({ stdout, stderr, output }) => { 136 if (output === lastEmittedOutput) return 137 lastEmittedOutput = output 138 emitHookProgress({ 139 hookId: params.hookId, 140 hookName: params.hookName, 141 hookEvent: params.hookEvent, 142 stdout, 143 stderr, 144 output, 145 }) 146 }) 147 }, params.intervalMs ?? 1000) 148 interval.unref() 149 150 return () => clearInterval(interval) 151} 152 153export function emitHookResponse(data: { 154 hookId: string 155 hookName: string 156 hookEvent: string 157 output: string 158 stdout: string 159 stderr: string 160 exitCode?: number 161 outcome: 'success' | 'error' | 'cancelled' 162}): void { 163 // Always log full hook output to debug log for verbose mode debugging 164 const outputToLog = data.stdout || data.stderr || data.output 165 if (outputToLog) { 166 logForDebugging( 167 `Hook ${data.hookName} (${data.hookEvent}) ${data.outcome}:\n${outputToLog}`, 168 ) 169 } 170 171 if (!shouldEmit(data.hookEvent)) return 172 173 emit({ 174 type: 'response', 175 ...data, 176 }) 177} 178 179/** 180 * Enable emission of all hook event types (beyond SessionStart and Setup). 181 * Called when the SDK `includeHookEvents` option is set or when running 182 * in CLAUDE_CODE_REMOTE mode. 183 */ 184export function setAllHookEventsEnabled(enabled: boolean): void { 185 allHookEventsEnabled = enabled 186} 187 188export function clearHookEventState(): void { 189 eventHandler = null 190 pendingEvents.length = 0 191 allHookEventsEnabled = false 192}