feat: support plugin-managed hooks

This commit is contained in:
Peter Steinberger
2026-01-18 05:56:59 +00:00
parent 88b37e80fc
commit e2c10a2b7a
16 changed files with 436 additions and 26 deletions

View File

@@ -5,12 +5,14 @@ import type {
GatewayRequestHandler,
GatewayRequestHandlers,
} from "../gateway/server-methods/types.js";
import { registerInternalHook } from "../hooks/internal-hooks.js";
import { resolveUserPath } from "../utils.js";
import type {
ClawdbotPluginApi,
ClawdbotPluginChannelRegistration,
ClawdbotPluginCliRegistrar,
ClawdbotPluginHttpHandler,
ClawdbotPluginHookOptions,
ProviderPlugin,
ClawdbotPluginService,
ClawdbotPluginToolContext,
@@ -22,6 +24,8 @@ import type {
PluginKind,
} from "./types.js";
import type { PluginRuntime } from "./runtime/types.js";
import type { HookEntry } from "../hooks/types.js";
import path from "node:path";
export type PluginToolRegistration = {
pluginId: string;
@@ -57,6 +61,13 @@ export type PluginProviderRegistration = {
source: string;
};
export type PluginHookRegistration = {
pluginId: string;
entry: HookEntry;
events: string[];
source: string;
};
export type PluginServiceRegistration = {
pluginId: string;
service: ClawdbotPluginService;
@@ -76,6 +87,7 @@ export type PluginRecord = {
status: "loaded" | "disabled" | "error";
error?: string;
toolNames: string[];
hookNames: string[];
channelIds: string[];
providerIds: string[];
gatewayMethods: string[];
@@ -90,6 +102,7 @@ export type PluginRecord = {
export type PluginRegistry = {
plugins: PluginRecord[];
tools: PluginToolRegistration[];
hooks: PluginHookRegistration[];
channels: PluginChannelRegistration[];
providers: PluginProviderRegistration[];
gatewayHandlers: GatewayRequestHandlers;
@@ -109,6 +122,7 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
const registry: PluginRegistry = {
plugins: [],
tools: [],
hooks: [],
channels: [],
providers: [],
gatewayHandlers: {},
@@ -150,6 +164,76 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
});
};
const registerHook = (
record: PluginRecord,
events: string | string[],
handler: Parameters<typeof registerInternalHook>[1],
opts: ClawdbotPluginHookOptions | undefined,
config: ClawdbotPluginApi["config"],
) => {
const eventList = Array.isArray(events) ? events : [events];
const normalizedEvents = eventList.map((event) => event.trim()).filter(Boolean);
const entry = opts?.entry ?? null;
const name = entry?.hook.name ?? opts?.name?.trim();
if (!name) {
pushDiagnostic({
level: "warn",
pluginId: record.id,
source: record.source,
message: "hook registration missing name",
});
return;
}
const description = entry?.hook.description ?? opts?.description ?? "";
const hookEntry: HookEntry = entry
? {
...entry,
hook: {
...entry.hook,
name,
description,
source: "clawdbot-plugin",
pluginId: record.id,
},
clawdbot: {
...entry.clawdbot,
events: normalizedEvents,
},
}
: {
hook: {
name,
description,
source: "clawdbot-plugin",
pluginId: record.id,
filePath: record.source,
baseDir: path.dirname(record.source),
handlerPath: record.source,
},
frontmatter: {},
clawdbot: { events: normalizedEvents },
invocation: { enabled: true },
};
record.hookNames.push(name);
registry.hooks.push({
pluginId: record.id,
entry: hookEntry,
events: normalizedEvents,
source: record.source,
});
const hookSystemEnabled = config?.hooks?.internal?.enabled === true;
if (!hookSystemEnabled || opts?.register === false) {
return;
}
for (const event of normalizedEvents) {
registerInternalHook(event, handler);
}
};
const registerGatewayMethod = (
record: PluginRecord,
method: string,
@@ -287,6 +371,8 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
runtime: registryParams.runtime,
logger: normalizeLogger(registryParams.logger),
registerTool: (tool, opts) => registerTool(record, tool, opts),
registerHook: (events, handler, opts) =>
registerHook(record, events, handler, opts, params.config),
registerHttpHandler: (handler) => registerHttpHandler(record, handler),
registerChannel: (registration) => registerChannel(record, registration),
registerProvider: (provider) => registerProvider(record, provider),