UNPKG

erosolar-cli

Version:

Unified AI agent framework for the command line - Multi-provider support with schema-driven tools, code intelligence, and transparent reasoning

267 lines 7.99 kB
/** * Hooks System * * Event-triggered scripts that automate workflows, based on Claude Code's hooks architecture. * Hooks can execute shell commands or query an LLM for decisions. */ import { exec } from 'node:child_process'; import { promisify } from 'node:util'; import { existsSync, readFileSync } from 'node:fs'; import { join } from 'node:path'; import { homedir } from 'node:os'; const execAsync = promisify(exec); /** * Load hooks from settings files */ export function loadHooks(workingDir) { const hooks = {}; // Load from user settings (~/.claude/settings.json or ~/.erosolar/settings.json) const userSettingsPaths = [ join(homedir(), '.claude', 'settings.json'), join(homedir(), '.erosolar', 'settings.json'), ]; // Load from project settings (.claude/settings.json or .erosolar/settings.json) const projectSettingsPaths = [ join(workingDir, '.claude', 'settings.json'), join(workingDir, '.erosolar', 'settings.json'), ]; // Load user settings first, then override with project settings for (const settingsPath of [...userSettingsPaths, ...projectSettingsPaths]) { if (existsSync(settingsPath)) { try { const content = readFileSync(settingsPath, 'utf-8'); const settings = JSON.parse(content); if (settings.hooks) { for (const [event, eventHooks] of Object.entries(settings.hooks)) { if (!hooks[event]) { hooks[event] = []; } hooks[event].push(...eventHooks); } } } catch { // Ignore invalid settings files } } } return hooks; } /** * Check if a hook matches the given context */ function matchesHook(hook, context) { if (!hook.matcher) { return true; } try { const regex = new RegExp(hook.matcher); // Match against tool name if available if (context.toolName) { return regex.test(context.toolName); } // Match against event name return regex.test(context.event); } catch { return false; } } /** * Execute a command hook */ async function executeCommandHook(hook, context) { if (!hook.command) { return { success: false, error: 'No command specified' }; } const timeout = hook.timeout ?? 30000; // Prepare environment variables for the hook const env = { ...process.env, HOOK_EVENT: context.event, HOOK_TOOL_NAME: context.toolName ?? '', HOOK_TOOL_ARGS: context.toolArgs ? JSON.stringify(context.toolArgs) : '', HOOK_TOOL_RESULT: context.toolResult ?? '', HOOK_USER_INPUT: context.userInput ?? '', HOOK_SESSION_ID: context.sessionId ?? '', HOOK_WORKING_DIR: context.workingDir, }; try { const { stdout, stderr } = await execAsync(hook.command, { cwd: context.workingDir, timeout, env, }); // Try to parse JSON output for structured results const output = stdout.trim(); try { const parsed = JSON.parse(output); return { success: true, output, decision: parsed.decision, reason: parsed.reason, blocked: parsed.blocked, }; } catch { // Return raw output if not JSON return { success: true, output, }; } } catch (error) { const message = error instanceof Error ? error.message : String(error); // Exit code 2 means blocking if (message.includes('exit code 2')) { return { success: false, blocked: true, error: message, }; } return { success: false, error: message, }; } } /** * Execute a prompt hook (would query LLM in full implementation) */ async function executePromptHook(hook, context) { if (!hook.prompt) { return { success: false, error: 'No prompt specified' }; } // In a full implementation, this would: // 1. Send the prompt to a fast LLM (e.g., Haiku) // 2. Parse the response for decision // 3. Return the result // For now, return a placeholder that allows continuation return { success: true, decision: 'continue', reason: 'Prompt hooks require LLM integration', }; } /** * Execute a single hook */ async function executeHook(hook, context) { switch (hook.type) { case 'command': return executeCommandHook(hook, context); case 'prompt': return executePromptHook(hook, context); default: return { success: false, error: `Unknown hook type: ${hook.type}` }; } } /** * HooksManager class for managing and executing hooks */ export class HooksManager { hooks; workingDir; constructor(workingDir) { this.workingDir = workingDir; this.hooks = loadHooks(workingDir); } /** * Reload hooks from settings files */ reload() { this.hooks = loadHooks(this.workingDir); } /** * Check if hooks are configured for an event */ hasHooks(event) { return Boolean(this.hooks[event]?.length); } /** * Get all hooks for an event */ getHooks(event) { return this.hooks[event] ?? []; } /** * Execute all matching hooks for an event */ async executeHooks(context) { const eventHooks = this.hooks[context.event] ?? []; const results = []; for (const hook of eventHooks) { if (matchesHook(hook, context)) { const result = await executeHook(hook, context); results.push(result); // Stop on blocking result if (result.blocked) { break; } } } return results; } /** * Execute pre-tool hooks and check if tool should proceed */ async executePreToolHooks(toolName, args) { const context = { event: 'PreToolUse', toolName, toolArgs: args, workingDir: this.workingDir, }; const results = await this.executeHooks(context); // Check if any hook blocked the operation const blocked = results.some((r) => r.blocked || r.decision === 'deny'); return { allowed: !blocked, results }; } /** * Execute post-tool hooks */ async executePostToolHooks(toolName, args, result) { const context = { event: 'PostToolUse', toolName, toolArgs: args, toolResult: result, workingDir: this.workingDir, }; return this.executeHooks(context); } /** * Execute user prompt hooks */ async executeUserPromptHooks(userInput) { const context = { event: 'UserPromptSubmit', userInput, workingDir: this.workingDir, }; const results = await this.executeHooks(context); const blocked = results.some((r) => r.blocked || r.decision === 'deny'); return { allowed: !blocked, results }; } /** * Execute session lifecycle hooks */ async executeSessionHook(event, sessionId) { const context = { event, sessionId, workingDir: this.workingDir, }; return this.executeHooks(context); } } /** * Create a hooks manager for the given working directory */ export function createHooksManager(workingDir) { return new HooksManager(workingDir); } //# sourceMappingURL=hooks.js.map