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