tasq/node_modules/@claude-flow/hooks/dist/bridge/official-hooks-bridge.js

280 lines
9.9 KiB
JavaScript

/**
* 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