- types: ClaudeStatus + ServerMessage 'status'; Session.claudeStatus; SessionManager.handleHookEvent - session: inject WEBTERM_SESSION + WEBTERM_HOOK_URL into the spawned shell env so hooks know which tab they belong to - http/hook.ts: pure event→status mapper (PreToolUse→working, PermissionRequest/ Notification:permission_prompt→waiting, Stop/idle_prompt→idle); 6 unit tests - server: POST /hook (loopback-only, express.json 64kb) → manager pushes 'status' - fix: closeIdleConnections() on shutdown so an idle hook keep-alive socket doesn't hang close()/SIGTERM - integration ⑦ (real PTY): attach → POST /hook → ws receives status. 205 tests green.
58 lines
1.9 KiB
TypeScript
58 lines
1.9 KiB
TypeScript
/**
|
|
* 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<string, unknown>;
|
|
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 };
|
|
}
|