Files
clawdbot/src/hooks/loader.ts
2026-01-17 01:31:57 +00:00

153 lines
4.9 KiB
TypeScript

/**
* Dynamic loader for internal 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 internal 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} internal hook handlers`);
* ```
*/
export async function loadInternalHooks(
cfg: ClawdbotConfig,
workspaceDir: string,
): Promise<number> {
// Check if internal 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<string, unknown>;
// Get handler function (default or named export)
const exportName = entry.clawdbot?.export ?? 'default';
const handler = mod[exportName];
if (typeof handler !== 'function') {
console.error(
`Internal 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(
`Internal 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 internal hook: ${entry.hook.name} -> ${events.join(', ')}${exportName !== 'default' ? ` (export: ${exportName})` : ''}`,
);
loadedCount++;
} catch (err) {
console.error(
`Failed to load internal 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<string, unknown>;
// Get the handler function
const exportName = handlerConfig.export ?? 'default';
const handler = mod[exportName];
if (typeof handler !== 'function') {
console.error(
`Internal hook error: Handler '${exportName}' from ${modulePath} is not a function`,
);
continue;
}
// Register the handler
registerInternalHook(handlerConfig.event, handler as InternalHookHandler);
console.log(
`Registered internal hook (legacy): ${handlerConfig.event} -> ${modulePath}${exportName !== 'default' ? `#${exportName}` : ''}`,
);
loadedCount++;
} catch (err) {
console.error(
`Failed to load internal hook handler from ${handlerConfig.module}:`,
err instanceof Error ? err.message : String(err),
);
}
}
return loadedCount;
}