/** * Dynamic loader for hook handlers * * Loads hook handlers from external modules based on configuration * and from directory-based discovery (bundled, managed, workspace) */ import { pathToFileURL } from "node:url"; import path from "node:path"; import { registerInternalHook } from "./internal-hooks.js"; import type { ClawdbotConfig } from "../config/config.js"; import type { InternalHookHandler } from "./internal-hooks.js"; import { loadWorkspaceHookEntries } from "./workspace.js"; import { resolveHookConfig } from "./config.js"; import { shouldIncludeHook } from "./config.js"; /** * Load and register all hook handlers * * Loads hooks from both: * 1. Directory-based discovery (bundled, managed, workspace) * 2. Legacy config handlers (backwards compatibility) * * @param cfg - Clawdbot configuration * @param workspaceDir - Workspace directory for hook discovery * @returns Number of handlers successfully loaded * * @example * ```ts * const config = await loadConfig(); * const workspaceDir = resolveAgentWorkspaceDir(config, agentId); * const count = await loadInternalHooks(config, workspaceDir); * console.log(`Loaded ${count} hook handlers`); * ``` */ export async function loadInternalHooks( cfg: ClawdbotConfig, workspaceDir: string, ): Promise { // Check if hooks are enabled if (!cfg.hooks?.internal?.enabled) { return 0; } let loadedCount = 0; // 1. Load hooks from directories (new system) try { const hookEntries = loadWorkspaceHookEntries(workspaceDir, { config: cfg }); // Filter by eligibility const eligible = hookEntries.filter((entry) => shouldIncludeHook({ entry, config: cfg })); for (const entry of eligible) { const hookConfig = resolveHookConfig(cfg, entry.hook.name); // Skip if explicitly disabled in config if (hookConfig?.enabled === false) { continue; } try { // Import handler module with cache-busting const url = pathToFileURL(entry.hook.handlerPath).href; const cacheBustedUrl = `${url}?t=${Date.now()}`; const mod = (await import(cacheBustedUrl)) as Record; // Get handler function (default or named export) const exportName = entry.clawdbot?.export ?? "default"; const handler = mod[exportName]; if (typeof handler !== "function") { console.error( `Hook error: Handler '${exportName}' from ${entry.hook.name} is not a function`, ); continue; } // Register for all events listed in metadata const events = entry.clawdbot?.events ?? []; if (events.length === 0) { console.warn(`Hook warning: Hook '${entry.hook.name}' has no events defined in metadata`); continue; } for (const event of events) { registerInternalHook(event, handler as InternalHookHandler); } console.log( `Registered hook: ${entry.hook.name} -> ${events.join(", ")}${exportName !== "default" ? ` (export: ${exportName})` : ""}`, ); loadedCount++; } catch (err) { console.error( `Failed to load hook ${entry.hook.name}:`, err instanceof Error ? err.message : String(err), ); } } } catch (err) { console.error( "Failed to load directory-based hooks:", err instanceof Error ? err.message : String(err), ); } // 2. Load legacy config handlers (backwards compatibility) const handlers = cfg.hooks.internal.handlers ?? []; for (const handlerConfig of handlers) { try { // Resolve module path (absolute or relative to cwd) const modulePath = path.isAbsolute(handlerConfig.module) ? handlerConfig.module : path.join(process.cwd(), handlerConfig.module); // Import the module with cache-busting to ensure fresh reload const url = pathToFileURL(modulePath).href; const cacheBustedUrl = `${url}?t=${Date.now()}`; const mod = (await import(cacheBustedUrl)) as Record; // Get the handler function const exportName = handlerConfig.export ?? "default"; const handler = mod[exportName]; if (typeof handler !== "function") { console.error(`Hook error: Handler '${exportName}' from ${modulePath} is not a function`); continue; } // Register the handler registerInternalHook(handlerConfig.event, handler as InternalHookHandler); console.log( `Registered hook (legacy): ${handlerConfig.event} -> ${modulePath}${exportName !== "default" ? `#${exportName}` : ""}`, ); loadedCount++; } catch (err) { console.error( `Failed to load hook handler from ${handlerConfig.module}:`, err instanceof Error ? err.message : String(err), ); } } return loadedCount; }