/** * src/http/hook.ts (H2) — map a Claude Code hook POST to a session status update. * * Pure + never throws. The web-terminal sessionId arrives in the * `X-Webterm-Session` header (injected as $WEBTERM_SESSION on spawn); the hook * payload arrives as JSON in the body. We translate the event into a coarse * ClaudeStatus the UI can show. The server is still a byte-shuttle for the * terminal stream — this is an out-of-band side-channel. */ import type { ClaudeStatus } from '../types.js'; export interface HookEvent { sessionId: string; status: ClaudeStatus; detail?: string; } /** * @param sessionId value of the `X-Webterm-Session` header * @param body parsed JSON body of the hook POST (untrusted) * @returns a status update, or null if the payload is unusable */ export function parseHookEvent(sessionId: string | undefined, body: unknown): HookEvent | null { if (typeof sessionId !== 'string' || sessionId.length === 0) return null; if (body === null || typeof body !== 'object') return null; const b = body as Record; const event = typeof b['hook_event_name'] === 'string' ? b['hook_event_name'] : ''; const notif = typeof b['notification_type'] === 'string' ? b['notification_type'] : ''; const tool = typeof b['tool_name'] === 'string' ? b['tool_name'] : undefined; let status: ClaudeStatus; switch (event) { case 'PreToolUse': case 'PostToolUse': case 'UserPromptSubmit': status = 'working'; break; case 'PermissionRequest': status = 'waiting'; break; case 'Stop': case 'SessionEnd': status = 'idle'; break; case 'Notification': status = notif === 'permission_prompt' ? 'waiting' : 'idle'; break; default: return null; } return status === 'waiting' && tool !== undefined ? { sessionId, status, detail: tool } : { sessionId, status }; }