/** * Official Claude Code Hooks Bridge * * Maps V3 internal hook events to official Claude Code hook events. * This bridge enables seamless integration between claude-flow's * internal hook system and the official Claude Code plugin API. * * @module v3/hooks/bridge/official-hooks-bridge */ import { HookEvent } from '../types.js'; /** * Mapping from V3 HookEvent to Official hook events */ export const V3_TO_OFFICIAL_HOOK_MAP = { // Direct mappings [HookEvent.PreToolUse]: 'PreToolUse', [HookEvent.PostToolUse]: 'PostToolUse', [HookEvent.SessionStart]: 'SessionStart', // File operations map to tool hooks with matchers [HookEvent.PreEdit]: 'PreToolUse', // matcher: Edit|Write|MultiEdit [HookEvent.PostEdit]: 'PostToolUse', // matcher: Edit|Write|MultiEdit [HookEvent.PreRead]: 'PreToolUse', // matcher: Read [HookEvent.PostRead]: 'PostToolUse', // matcher: Read // Command operations map to tool hooks [HookEvent.PreCommand]: 'PreToolUse', // matcher: Bash [HookEvent.PostCommand]: 'PostToolUse', // matcher: Bash // Task operations [HookEvent.PreTask]: 'UserPromptSubmit', [HookEvent.PostTask]: 'PostToolUse', // matcher: Task [HookEvent.TaskProgress]: null, // Internal only // Session operations [HookEvent.SessionEnd]: 'Stop', [HookEvent.SessionRestore]: 'SessionStart', // Agent operations [HookEvent.AgentSpawn]: 'PostToolUse', // matcher: Task [HookEvent.AgentTerminate]: 'SubagentStop', // Routing (internal) [HookEvent.PreRoute]: 'UserPromptSubmit', [HookEvent.PostRoute]: null, // Internal only // Learning (internal) [HookEvent.PatternLearned]: null, // Internal only [HookEvent.PatternConsolidated]: null, // Internal only }; /** * Tool matchers for V3 events that map to PreToolUse/PostToolUse */ export const V3_TOOL_MATCHERS = { [HookEvent.PreEdit]: '^(Write|Edit|MultiEdit)$', [HookEvent.PostEdit]: '^(Write|Edit|MultiEdit)$', [HookEvent.PreRead]: '^Read$', [HookEvent.PostRead]: '^Read$', [HookEvent.PreCommand]: '^Bash$', [HookEvent.PostCommand]: '^Bash$', [HookEvent.PreTask]: '^Task$', [HookEvent.PostTask]: '^Task$', [HookEvent.AgentSpawn]: '^Task$', }; /** * Bridge class for converting between V3 and official hooks */ export class OfficialHooksBridge { /** * Convert official hook input to V3 HookContext */ static toV3Context(input) { const event = this.officialToV3Event(input.hook_event_name, input.tool_name); const context = { event, timestamp: new Date(), metadata: { session_id: input.session_id, transcript_path: input.transcript_path, cwd: input.cwd, permission_mode: input.permission_mode, }, }; // Add tool information if (input.tool_name) { context.tool = { name: input.tool_name, parameters: input.tool_input ?? {}, }; } // Add file information for file operations if (input.tool_name && ['Write', 'Edit', 'MultiEdit', 'Read'].includes(input.tool_name)) { context.file = { path: input.tool_input?.file_path ?? '', operation: input.tool_name === 'Read' ? 'read' : 'modify', }; } // Add command information for Bash if (input.tool_name === 'Bash') { context.command = { raw: input.tool_input?.command ?? '', workingDirectory: input.cwd, exitCode: input.tool_exit_code, output: typeof input.tool_output === 'string' ? input.tool_output : undefined, }; } // Add task information for Task tool if (input.tool_name === 'Task') { context.task = { id: `task-${Date.now()}`, description: input.tool_input?.prompt ?? '', agent: input.tool_input?.subagent_type, }; } // Add session information context.session = { id: input.session_id, startedAt: new Date(), }; // Add prompt for UserPromptSubmit if (input.prompt) { context.routing = { task: input.prompt, }; } return context; } /** * Convert V3 HookResult to official hook output */ static toOfficialOutput(result, event) { const output = {}; // Map abort to decision if (result.abort) { output.decision = event === 'PermissionRequest' ? 'deny' : 'block'; output.continue = false; } else if (result.success) { output.decision = event === 'PermissionRequest' ? 'allow' : 'continue'; output.continue = true; } // Add reason if (result.error) { output.reason = result.error; } else if (result.message) { output.reason = result.message; } // Pass through updated input if present if (result.data?.updatedInput) { output.updatedInput = result.data.updatedInput; } return output; } /** * Convert official hook event to V3 HookEvent */ static officialToV3Event(officialEvent, toolName) { // Handle tool-specific mappings if (officialEvent === 'PreToolUse' && toolName) { if (['Write', 'Edit', 'MultiEdit'].includes(toolName)) { return HookEvent.PreEdit; } if (toolName === 'Read') { return HookEvent.PreRead; } if (toolName === 'Bash') { return HookEvent.PreCommand; } if (toolName === 'Task') { return HookEvent.PreTask; } return HookEvent.PreToolUse; } if (officialEvent === 'PostToolUse' && toolName) { if (['Write', 'Edit', 'MultiEdit'].includes(toolName)) { return HookEvent.PostEdit; } if (toolName === 'Read') { return HookEvent.PostRead; } if (toolName === 'Bash') { return HookEvent.PostCommand; } if (toolName === 'Task') { return HookEvent.PostTask; } return HookEvent.PostToolUse; } // Direct mappings const mapping = { PreToolUse: HookEvent.PreToolUse, PostToolUse: HookEvent.PostToolUse, UserPromptSubmit: HookEvent.PreTask, PermissionRequest: HookEvent.PreToolUse, Notification: HookEvent.PostTask, // Closest match Stop: HookEvent.SessionEnd, SubagentStop: HookEvent.AgentTerminate, PreCompact: HookEvent.SessionEnd, // Closest match SessionStart: HookEvent.SessionStart, }; return mapping[officialEvent] ?? HookEvent.PreToolUse; } /** * Get tool matcher for a V3 event */ static getToolMatcher(event) { return V3_TOOL_MATCHERS[event] ?? null; } /** * Check if V3 event maps to an official hook */ static hasOfficialMapping(event) { return V3_TO_OFFICIAL_HOOK_MAP[event] !== null; } /** * Create a CLI command for a V3 hook handler */ static createCLICommand(event, handler) { const baseCommand = 'npx claude-flow@alpha hooks'; switch (event) { case HookEvent.PreEdit: return `${baseCommand} pre-edit --file "$TOOL_INPUT_file_path"`; case HookEvent.PostEdit: return `${baseCommand} post-edit --file "$TOOL_INPUT_file_path" --success "$TOOL_SUCCESS" --train-patterns`; case HookEvent.PreCommand: return `${baseCommand} pre-command --command "$TOOL_INPUT_command"`; case HookEvent.PostCommand: return `${baseCommand} post-command --command "$TOOL_INPUT_command" --success "$TOOL_SUCCESS"`; case HookEvent.PreTask: return `${baseCommand} pre-task --description "$PROMPT"`; case HookEvent.PostTask: return `${baseCommand} post-task --task-id "$TOOL_RESULT_agent_id" --analyze-performance`; case HookEvent.SessionStart: return `${baseCommand} session-start --session-id "$SESSION_ID" --load-context`; case HookEvent.SessionEnd: return `${baseCommand} session-end --session-id "$SESSION_ID" --export-metrics`; default: return `${baseCommand} ${handler}`; } } } /** * Process stdin from official Claude Code hook system */ export async function processOfficialHookInput() { return new Promise((resolve) => { let data = ''; process.stdin.setEncoding('utf8'); process.stdin.on('readable', () => { let chunk; while ((chunk = process.stdin.read()) !== null) { data += chunk; } }); process.stdin.on('end', () => { try { resolve(JSON.parse(data)); } catch { resolve(null); } }); // Handle case where no stdin (testing) setTimeout(() => { if (!data) { resolve(null); } }, 100); }); } /** * Output result to official Claude Code hook system */ export function outputOfficialHookResult(output) { console.log(JSON.stringify(output)); } /** * Execute a V3 handler and bridge to official output */ export async function executeWithBridge(input, handler) { const context = OfficialHooksBridge.toV3Context(input); const result = await handler(context); return OfficialHooksBridge.toOfficialOutput(result, input.hook_event_name); } export default OfficialHooksBridge; //# sourceMappingURL=official-hooks-bridge.js.map