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