feat(v0.3): H2 server — Claude Code hooks → live status side-channel
- 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.
This commit is contained in:
57
src/http/hook.ts
Normal file
57
src/http/hook.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* 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 };
|
||||
}
|
||||
Reference in New Issue
Block a user