diff --git a/docs/tools/plugin.md b/docs/tools/plugin.md index 86a2b984316..9250501f2d9 100644 --- a/docs/tools/plugin.md +++ b/docs/tools/plugin.md @@ -330,22 +330,29 @@ Plugins export either: ## Plugin hooks -Plugins can ship hooks and register them at runtime. This lets a plugin bundle -event-driven automation without a separate hook pack install. +Plugins can register hooks at runtime. This lets a plugin bundle event-driven +automation without a separate hook pack install. ### Example -``` -import { registerPluginHooksFromDir } from "openclaw/plugin-sdk"; - +```ts export default function register(api) { - registerPluginHooksFromDir(api, "./hooks"); + api.registerHook( + "command:new", + async () => { + // Hook logic here. + }, + { + name: "my-plugin.command-new", + description: "Runs when /new is invoked", + }, + ); } ``` Notes: -- Hook directories follow the normal hook structure (`HOOK.md` + `handler.ts`). +- Register hooks explicitly via `api.registerHook(...)`. - Hook eligibility rules still apply (OS/bins/env/config requirements). - Plugin-managed hooks show up in `openclaw hooks list` with `plugin:`. - You cannot enable/disable plugin-managed hooks via `openclaw hooks`; enable/disable the plugin instead. diff --git a/src/hooks/plugin-hooks.ts b/src/hooks/plugin-hooks.ts deleted file mode 100644 index f7da685fb9b..00000000000 --- a/src/hooks/plugin-hooks.ts +++ /dev/null @@ -1,116 +0,0 @@ -import path from "node:path"; -import { pathToFileURL } from "node:url"; -import type { OpenClawPluginApi } from "../plugins/types.js"; -import { shouldIncludeHook } from "./config.js"; -import type { InternalHookHandler } from "./internal-hooks.js"; -import type { HookEntry } from "./types.js"; -import { loadHookEntriesFromDir } from "./workspace.js"; - -export type PluginHookLoadResult = { - hooks: HookEntry[]; - loaded: number; - skipped: number; - errors: string[]; -}; - -function resolveHookDir(api: OpenClawPluginApi, dir: string): string { - if (path.isAbsolute(dir)) { - return dir; - } - return path.resolve(path.dirname(api.source), dir); -} - -function normalizePluginHookEntry(api: OpenClawPluginApi, entry: HookEntry): HookEntry { - return { - ...entry, - hook: { - ...entry.hook, - source: "openclaw-plugin", - pluginId: api.id, - }, - metadata: { - ...entry.metadata, - hookKey: entry.metadata?.hookKey ?? `${api.id}:${entry.hook.name}`, - events: entry.metadata?.events ?? [], - }, - }; -} - -async function loadHookHandler( - entry: HookEntry, - api: OpenClawPluginApi, -): Promise { - try { - const url = pathToFileURL(entry.hook.handlerPath).href; - const cacheBustedUrl = `${url}?t=${Date.now()}`; - const mod = (await import(cacheBustedUrl)) as Record; - const exportName = entry.metadata?.export ?? "default"; - const handler = mod[exportName]; - if (typeof handler === "function") { - return handler as InternalHookHandler; - } - api.logger.warn?.(`[hooks] ${entry.hook.name} handler is not a function`); - return null; - } catch (err) { - api.logger.warn?.(`[hooks] Failed to load ${entry.hook.name}: ${String(err)}`); - return null; - } -} - -export async function registerPluginHooksFromDir( - api: OpenClawPluginApi, - dir: string, -): Promise { - const resolvedDir = resolveHookDir(api, dir); - const hooks = loadHookEntriesFromDir({ - dir: resolvedDir, - source: "openclaw-plugin", - pluginId: api.id, - }); - - const result: PluginHookLoadResult = { - hooks, - loaded: 0, - skipped: 0, - errors: [], - }; - - for (const entry of hooks) { - const normalizedEntry = normalizePluginHookEntry(api, entry); - const events = normalizedEntry.metadata?.events ?? []; - if (events.length === 0) { - api.logger.warn?.(`[hooks] ${entry.hook.name} has no events; skipping`); - api.registerHook(events, async () => undefined, { - entry: normalizedEntry, - register: false, - }); - result.skipped += 1; - continue; - } - - const handler = await loadHookHandler(entry, api); - if (!handler) { - result.errors.push(`[hooks] Failed to load ${entry.hook.name}`); - api.registerHook(events, async () => undefined, { - entry: normalizedEntry, - register: false, - }); - result.skipped += 1; - continue; - } - - const eligible = shouldIncludeHook({ entry: normalizedEntry, config: api.config }); - api.registerHook(events, handler, { - entry: normalizedEntry, - register: eligible, - }); - - if (eligible) { - result.loaded += 1; - } else { - result.skipped += 1; - } - } - - return result; -}