feat: add internal hooks system
This commit is contained in:
152
src/hooks/loader.ts
Normal file
152
src/hooks/loader.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
Reference in New Issue
Block a user