feat: support plugin-managed hooks
This commit is contained in:
parent
88b37e80fc
commit
e2c10a2b7a
@ -11,6 +11,7 @@ Manage agent hooks (event-driven automations for commands like `/new`, `/reset`,
|
|||||||
|
|
||||||
Related:
|
Related:
|
||||||
- Hooks: [Hooks](/hooks)
|
- Hooks: [Hooks](/hooks)
|
||||||
|
- Plugin hooks: [Plugins](/plugin#plugin-hooks)
|
||||||
|
|
||||||
## List All Hooks
|
## List All Hooks
|
||||||
|
|
||||||
@ -118,6 +119,9 @@ clawdbot hooks enable <name>
|
|||||||
|
|
||||||
Enable a specific hook by adding it to your config (`~/.clawdbot/config.json`).
|
Enable a specific hook by adding it to your config (`~/.clawdbot/config.json`).
|
||||||
|
|
||||||
|
**Note:** Hooks managed by plugins show `plugin:<id>` in `clawdbot hooks list` and
|
||||||
|
can’t be enabled/disabled here. Enable/disable the plugin instead.
|
||||||
|
|
||||||
**Arguments:**
|
**Arguments:**
|
||||||
- `<name>`: Hook name (e.g., `session-memory`)
|
- `<name>`: Hook name (e.g., `session-memory`)
|
||||||
|
|
||||||
|
|||||||
@ -14,6 +14,8 @@ Hooks are small scripts that run when something happens. There are two kinds:
|
|||||||
|
|
||||||
- **Hooks** (this page): run inside the Gateway when agent events fire, like `/new`, `/reset`, `/stop`, or lifecycle events.
|
- **Hooks** (this page): run inside the Gateway when agent events fire, like `/new`, `/reset`, `/stop`, or lifecycle events.
|
||||||
- **Webhooks**: external HTTP webhooks that let other systems trigger work in Clawdbot. See [Webhook Hooks](/automation/webhook) or use `clawdbot webhooks` for Gmail helper commands.
|
- **Webhooks**: external HTTP webhooks that let other systems trigger work in Clawdbot. See [Webhook Hooks](/automation/webhook) or use `clawdbot webhooks` for Gmail helper commands.
|
||||||
|
|
||||||
|
Hooks can also be bundled inside plugins; see [Plugins](/plugin#plugin-hooks).
|
||||||
|
|
||||||
Common uses:
|
Common uses:
|
||||||
- Save a memory snapshot when you reset a session
|
- Save a memory snapshot when you reset a session
|
||||||
|
|||||||
@ -215,6 +215,27 @@ Plugins export either:
|
|||||||
- A function: `(api) => { ... }`
|
- A function: `(api) => { ... }`
|
||||||
- An object: `{ id, name, configSchema, register(api) { ... } }`
|
- An object: `{ id, name, configSchema, register(api) { ... } }`
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```
|
||||||
|
import { registerPluginHooksFromDir } from "clawdbot/plugin-sdk";
|
||||||
|
|
||||||
|
export default function register(api) {
|
||||||
|
registerPluginHooksFromDir(api, "./hooks");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- Hook directories follow the normal hook structure (`HOOK.md` + `handler.ts`).
|
||||||
|
- Hook eligibility rules still apply (OS/bins/env/config requirements).
|
||||||
|
- Plugin-managed hooks show up in `clawdbot hooks list` with `plugin:<id>`.
|
||||||
|
- You cannot enable/disable plugin-managed hooks via `clawdbot hooks`; enable/disable the plugin instead.
|
||||||
|
|
||||||
## Provider plugins (model auth)
|
## Provider plugins (model auth)
|
||||||
|
|
||||||
Plugins can register **model provider auth** flows so users can run OAuth or
|
Plugins can register **model provider auth** flows so users can run OAuth or
|
||||||
|
|||||||
@ -10,6 +10,7 @@ const report: HookStatusReport = {
|
|||||||
name: "session-memory",
|
name: "session-memory",
|
||||||
description: "Save session context to memory",
|
description: "Save session context to memory",
|
||||||
source: "clawdbot-bundled",
|
source: "clawdbot-bundled",
|
||||||
|
pluginId: undefined,
|
||||||
filePath: "/tmp/hooks/session-memory/HOOK.md",
|
filePath: "/tmp/hooks/session-memory/HOOK.md",
|
||||||
baseDir: "/tmp/hooks/session-memory",
|
baseDir: "/tmp/hooks/session-memory",
|
||||||
handlerPath: "/tmp/hooks/session-memory/handler.js",
|
handlerPath: "/tmp/hooks/session-memory/handler.js",
|
||||||
@ -20,6 +21,7 @@ const report: HookStatusReport = {
|
|||||||
always: false,
|
always: false,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
eligible: true,
|
eligible: true,
|
||||||
|
managedByPlugin: false,
|
||||||
requirements: {
|
requirements: {
|
||||||
bins: [],
|
bins: [],
|
||||||
anyBins: [],
|
anyBins: [],
|
||||||
@ -51,4 +53,49 @@ describe("hooks cli formatting", () => {
|
|||||||
const output = formatHooksCheck(report, {});
|
const output = formatHooksCheck(report, {});
|
||||||
expect(output).toContain("Hooks Status");
|
expect(output).toContain("Hooks Status");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("labels plugin-managed hooks with plugin id", () => {
|
||||||
|
const pluginReport: HookStatusReport = {
|
||||||
|
workspaceDir: "/tmp/workspace",
|
||||||
|
managedHooksDir: "/tmp/hooks",
|
||||||
|
hooks: [
|
||||||
|
{
|
||||||
|
name: "plugin-hook",
|
||||||
|
description: "Hook from plugin",
|
||||||
|
source: "clawdbot-plugin",
|
||||||
|
pluginId: "voice-call",
|
||||||
|
filePath: "/tmp/hooks/plugin-hook/HOOK.md",
|
||||||
|
baseDir: "/tmp/hooks/plugin-hook",
|
||||||
|
handlerPath: "/tmp/hooks/plugin-hook/handler.js",
|
||||||
|
hookKey: "plugin-hook",
|
||||||
|
emoji: "🔗",
|
||||||
|
homepage: undefined,
|
||||||
|
events: ["command:new"],
|
||||||
|
always: false,
|
||||||
|
disabled: false,
|
||||||
|
eligible: true,
|
||||||
|
managedByPlugin: true,
|
||||||
|
requirements: {
|
||||||
|
bins: [],
|
||||||
|
anyBins: [],
|
||||||
|
env: [],
|
||||||
|
config: [],
|
||||||
|
os: [],
|
||||||
|
},
|
||||||
|
missing: {
|
||||||
|
bins: [],
|
||||||
|
anyBins: [],
|
||||||
|
env: [],
|
||||||
|
config: [],
|
||||||
|
os: [],
|
||||||
|
},
|
||||||
|
configChecks: [],
|
||||||
|
install: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const output = formatHooksList(pluginReport, {});
|
||||||
|
expect(output).toContain("plugin:voice-call");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -11,6 +11,8 @@ import {
|
|||||||
type HookStatusEntry,
|
type HookStatusEntry,
|
||||||
type HookStatusReport,
|
type HookStatusReport,
|
||||||
} from "../hooks/hooks-status.js";
|
} from "../hooks/hooks-status.js";
|
||||||
|
import type { HookEntry } from "../hooks/types.js";
|
||||||
|
import { loadWorkspaceHookEntries } from "../hooks/workspace.js";
|
||||||
import { loadConfig, writeConfigFile } from "../config/io.js";
|
import { loadConfig, writeConfigFile } from "../config/io.js";
|
||||||
import {
|
import {
|
||||||
installHooksFromNpmSpec,
|
installHooksFromNpmSpec,
|
||||||
@ -18,6 +20,7 @@ import {
|
|||||||
resolveHookInstallDir,
|
resolveHookInstallDir,
|
||||||
} from "../hooks/install.js";
|
} from "../hooks/install.js";
|
||||||
import { recordHookInstall } from "../hooks/installs.js";
|
import { recordHookInstall } from "../hooks/installs.js";
|
||||||
|
import { buildPluginStatusReport } from "../plugins/status.js";
|
||||||
import { defaultRuntime } from "../runtime.js";
|
import { defaultRuntime } from "../runtime.js";
|
||||||
import { formatDocsLink } from "../terminal/links.js";
|
import { formatDocsLink } from "../terminal/links.js";
|
||||||
import { theme } from "../terminal/theme.js";
|
import { theme } from "../terminal/theme.js";
|
||||||
@ -42,6 +45,29 @@ export type HooksUpdateOptions = {
|
|||||||
dryRun?: boolean;
|
dryRun?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function mergeHookEntries(
|
||||||
|
pluginEntries: HookEntry[],
|
||||||
|
workspaceEntries: HookEntry[],
|
||||||
|
): HookEntry[] {
|
||||||
|
const merged = new Map<string, HookEntry>();
|
||||||
|
for (const entry of pluginEntries) {
|
||||||
|
merged.set(entry.hook.name, entry);
|
||||||
|
}
|
||||||
|
for (const entry of workspaceEntries) {
|
||||||
|
merged.set(entry.hook.name, entry);
|
||||||
|
}
|
||||||
|
return Array.from(merged.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildHooksReport(config: ClawdbotConfig): HookStatusReport {
|
||||||
|
const workspaceDir = resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config));
|
||||||
|
const workspaceEntries = loadWorkspaceHookEntries(workspaceDir, { config });
|
||||||
|
const pluginReport = buildPluginStatusReport({ config, workspaceDir });
|
||||||
|
const pluginEntries = pluginReport.hooks.map((hook) => hook.entry);
|
||||||
|
const entries = mergeHookEntries(pluginEntries, workspaceEntries);
|
||||||
|
return buildWorkspaceHookStatus(workspaceDir, { config, entries });
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format a single hook for display in the list
|
* Format a single hook for display in the list
|
||||||
*/
|
*/
|
||||||
@ -58,6 +84,9 @@ function formatHookLine(hook: HookStatusEntry, verbose = false): string {
|
|||||||
const desc = chalk.gray(
|
const desc = chalk.gray(
|
||||||
hook.description.length > 50 ? `${hook.description.slice(0, 47)}...` : hook.description,
|
hook.description.length > 50 ? `${hook.description.slice(0, 47)}...` : hook.description,
|
||||||
);
|
);
|
||||||
|
const sourceLabel = hook.managedByPlugin
|
||||||
|
? chalk.magenta(`plugin:${hook.pluginId ?? "unknown"}`)
|
||||||
|
: "";
|
||||||
|
|
||||||
if (verbose) {
|
if (verbose) {
|
||||||
const missing: string[] = [];
|
const missing: string[] = [];
|
||||||
@ -77,10 +106,12 @@ function formatHookLine(hook: HookStatusEntry, verbose = false): string {
|
|||||||
missing.push(`os: ${hook.missing.os.join(", ")}`);
|
missing.push(`os: ${hook.missing.os.join(", ")}`);
|
||||||
}
|
}
|
||||||
const missingStr = missing.length > 0 ? chalk.red(` [${missing.join("; ")}]`) : "";
|
const missingStr = missing.length > 0 ? chalk.red(` [${missing.join("; ")}]`) : "";
|
||||||
return `${emoji} ${name} ${status}${missingStr}\n ${desc}`;
|
const sourceSuffix = sourceLabel ? ` ${sourceLabel}` : "";
|
||||||
|
return `${emoji} ${name} ${status}${missingStr}\n ${desc}${sourceSuffix}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${emoji} ${name} ${status} - ${desc}`;
|
const sourceSuffix = sourceLabel ? ` ${sourceLabel}` : "";
|
||||||
|
return `${emoji} ${name} ${status} - ${desc}${sourceSuffix}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function readInstalledPackageVersion(dir: string): Promise<string | undefined> {
|
async function readInstalledPackageVersion(dir: string): Promise<string | undefined> {
|
||||||
@ -110,9 +141,11 @@ export function formatHooksList(report: HookStatusReport, opts: HooksListOptions
|
|||||||
eligible: h.eligible,
|
eligible: h.eligible,
|
||||||
disabled: h.disabled,
|
disabled: h.disabled,
|
||||||
source: h.source,
|
source: h.source,
|
||||||
|
pluginId: h.pluginId,
|
||||||
events: h.events,
|
events: h.events,
|
||||||
homepage: h.homepage,
|
homepage: h.homepage,
|
||||||
missing: h.missing,
|
missing: h.missing,
|
||||||
|
managedByPlugin: h.managedByPlugin,
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
return JSON.stringify(jsonReport, null, 2);
|
return JSON.stringify(jsonReport, null, 2);
|
||||||
@ -186,7 +219,11 @@ export function formatHookInfo(
|
|||||||
|
|
||||||
// Details
|
// Details
|
||||||
lines.push(chalk.bold("Details:"));
|
lines.push(chalk.bold("Details:"));
|
||||||
lines.push(` Source: ${hook.source}`);
|
if (hook.managedByPlugin) {
|
||||||
|
lines.push(` Source: ${hook.source} (${hook.pluginId ?? "unknown"})`);
|
||||||
|
} else {
|
||||||
|
lines.push(` Source: ${hook.source}`);
|
||||||
|
}
|
||||||
lines.push(` Path: ${chalk.gray(hook.filePath)}`);
|
lines.push(` Path: ${chalk.gray(hook.filePath)}`);
|
||||||
lines.push(` Handler: ${chalk.gray(hook.handlerPath)}`);
|
lines.push(` Handler: ${chalk.gray(hook.handlerPath)}`);
|
||||||
if (hook.homepage) {
|
if (hook.homepage) {
|
||||||
@ -195,6 +232,9 @@ export function formatHookInfo(
|
|||||||
if (hook.events.length > 0) {
|
if (hook.events.length > 0) {
|
||||||
lines.push(` Events: ${hook.events.join(", ")}`);
|
lines.push(` Events: ${hook.events.join(", ")}`);
|
||||||
}
|
}
|
||||||
|
if (hook.managedByPlugin) {
|
||||||
|
lines.push(` Managed by plugin; enable/disable via hooks CLI not available.`);
|
||||||
|
}
|
||||||
|
|
||||||
// Requirements
|
// Requirements
|
||||||
const hasRequirements =
|
const hasRequirements =
|
||||||
@ -302,14 +342,19 @@ export function formatHooksCheck(report: HookStatusReport, opts: HooksCheckOptio
|
|||||||
|
|
||||||
export async function enableHook(hookName: string): Promise<void> {
|
export async function enableHook(hookName: string): Promise<void> {
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
const workspaceDir = resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config));
|
const report = buildHooksReport(config);
|
||||||
const report = buildWorkspaceHookStatus(workspaceDir, { config });
|
|
||||||
const hook = report.hooks.find((h) => h.name === hookName);
|
const hook = report.hooks.find((h) => h.name === hookName);
|
||||||
|
|
||||||
if (!hook) {
|
if (!hook) {
|
||||||
throw new Error(`Hook "${hookName}" not found`);
|
throw new Error(`Hook "${hookName}" not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hook.managedByPlugin) {
|
||||||
|
throw new Error(
|
||||||
|
`Hook "${hookName}" is managed by plugin "${hook.pluginId ?? "unknown"}" and cannot be enabled/disabled.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!hook.eligible) {
|
if (!hook.eligible) {
|
||||||
throw new Error(`Hook "${hookName}" is not eligible (missing requirements)`);
|
throw new Error(`Hook "${hookName}" is not eligible (missing requirements)`);
|
||||||
}
|
}
|
||||||
@ -336,14 +381,19 @@ export async function enableHook(hookName: string): Promise<void> {
|
|||||||
|
|
||||||
export async function disableHook(hookName: string): Promise<void> {
|
export async function disableHook(hookName: string): Promise<void> {
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
const workspaceDir = resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config));
|
const report = buildHooksReport(config);
|
||||||
const report = buildWorkspaceHookStatus(workspaceDir, { config });
|
|
||||||
const hook = report.hooks.find((h) => h.name === hookName);
|
const hook = report.hooks.find((h) => h.name === hookName);
|
||||||
|
|
||||||
if (!hook) {
|
if (!hook) {
|
||||||
throw new Error(`Hook "${hookName}" not found`);
|
throw new Error(`Hook "${hookName}" not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hook.managedByPlugin) {
|
||||||
|
throw new Error(
|
||||||
|
`Hook "${hookName}" is managed by plugin "${hook.pluginId ?? "unknown"}" and cannot be enabled/disabled.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Update config
|
// Update config
|
||||||
const entries = { ...config.hooks?.internal?.entries };
|
const entries = { ...config.hooks?.internal?.entries };
|
||||||
entries[hookName] = { ...entries[hookName], enabled: false };
|
entries[hookName] = { ...entries[hookName], enabled: false };
|
||||||
@ -382,8 +432,7 @@ export function registerHooksCli(program: Command): void {
|
|||||||
.action(async (opts) => {
|
.action(async (opts) => {
|
||||||
try {
|
try {
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
const workspaceDir = resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config));
|
const report = buildHooksReport(config);
|
||||||
const report = buildWorkspaceHookStatus(workspaceDir, { config });
|
|
||||||
console.log(formatHooksList(report, opts));
|
console.log(formatHooksList(report, opts));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
||||||
@ -398,8 +447,7 @@ export function registerHooksCli(program: Command): void {
|
|||||||
.action(async (name, opts) => {
|
.action(async (name, opts) => {
|
||||||
try {
|
try {
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
const workspaceDir = resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config));
|
const report = buildHooksReport(config);
|
||||||
const report = buildWorkspaceHookStatus(workspaceDir, { config });
|
|
||||||
console.log(formatHookInfo(report, name, opts));
|
console.log(formatHookInfo(report, name, opts));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
||||||
@ -414,8 +462,7 @@ export function registerHooksCli(program: Command): void {
|
|||||||
.action(async (opts) => {
|
.action(async (opts) => {
|
||||||
try {
|
try {
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
const workspaceDir = resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config));
|
const report = buildHooksReport(config);
|
||||||
const report = buildWorkspaceHookStatus(workspaceDir, { config });
|
|
||||||
console.log(formatHooksCheck(report, opts));
|
console.log(formatHooksCheck(report, opts));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
||||||
@ -765,8 +812,7 @@ export function registerHooksCli(program: Command): void {
|
|||||||
hooks.action(async () => {
|
hooks.action(async () => {
|
||||||
try {
|
try {
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
const workspaceDir = resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config));
|
const report = buildHooksReport(config);
|
||||||
const report = buildWorkspaceHookStatus(workspaceDir, { config });
|
|
||||||
console.log(formatHooksList(report, {}));
|
console.log(formatHooksList(report, {}));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
||||||
|
|||||||
@ -48,12 +48,34 @@ describe("onboard-hooks", () => {
|
|||||||
name: "session-memory",
|
name: "session-memory",
|
||||||
description: "Save session context to memory when /new command is issued",
|
description: "Save session context to memory when /new command is issued",
|
||||||
source: "clawdbot-bundled",
|
source: "clawdbot-bundled",
|
||||||
|
pluginId: undefined,
|
||||||
|
filePath: "/mock/workspace/hooks/session-memory/HOOK.md",
|
||||||
|
baseDir: "/mock/workspace/hooks/session-memory",
|
||||||
|
handlerPath: "/mock/workspace/hooks/session-memory/handler.js",
|
||||||
|
hookKey: "session-memory",
|
||||||
emoji: "💾",
|
emoji: "💾",
|
||||||
events: ["command:new"],
|
events: ["command:new"],
|
||||||
|
homepage: undefined,
|
||||||
|
always: false,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
eligible,
|
eligible,
|
||||||
requirements: { config: ["workspace.dir"] },
|
managedByPlugin: false,
|
||||||
missing: {},
|
requirements: {
|
||||||
|
bins: [],
|
||||||
|
anyBins: [],
|
||||||
|
env: [],
|
||||||
|
config: ["workspace.dir"],
|
||||||
|
os: [],
|
||||||
|
},
|
||||||
|
missing: {
|
||||||
|
bins: [],
|
||||||
|
anyBins: [],
|
||||||
|
env: [],
|
||||||
|
config: eligible ? [] : ["workspace.dir"],
|
||||||
|
os: [],
|
||||||
|
},
|
||||||
|
configChecks: [],
|
||||||
|
install: [],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
@ -4,6 +4,7 @@ export const createTestRegistry = (overrides: Partial<PluginRegistry> = {}): Plu
|
|||||||
const base: PluginRegistry = {
|
const base: PluginRegistry = {
|
||||||
plugins: [],
|
plugins: [],
|
||||||
tools: [],
|
tools: [],
|
||||||
|
hooks: [],
|
||||||
channels: [],
|
channels: [],
|
||||||
providers: [],
|
providers: [],
|
||||||
gatewayHandlers: {},
|
gatewayHandlers: {},
|
||||||
|
|||||||
@ -73,11 +73,12 @@ export function shouldIncludeHook(params: {
|
|||||||
const { entry, config, eligibility } = params;
|
const { entry, config, eligibility } = params;
|
||||||
const hookKey = resolveHookKey(entry.hook.name, entry);
|
const hookKey = resolveHookKey(entry.hook.name, entry);
|
||||||
const hookConfig = resolveHookConfig(config, hookKey);
|
const hookConfig = resolveHookConfig(config, hookKey);
|
||||||
|
const pluginManaged = entry.hook.source === "clawdbot-plugin";
|
||||||
const osList = entry.clawdbot?.os ?? [];
|
const osList = entry.clawdbot?.os ?? [];
|
||||||
const remotePlatforms = eligibility?.remote?.platforms ?? [];
|
const remotePlatforms = eligibility?.remote?.platforms ?? [];
|
||||||
|
|
||||||
// Check if explicitly disabled
|
// Check if explicitly disabled
|
||||||
if (hookConfig?.enabled === false) return false;
|
if (!pluginManaged && hookConfig?.enabled === false) return false;
|
||||||
|
|
||||||
// Check OS requirement
|
// Check OS requirement
|
||||||
if (
|
if (
|
||||||
|
|||||||
@ -23,6 +23,7 @@ export type HookStatusEntry = {
|
|||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
source: string;
|
source: string;
|
||||||
|
pluginId?: string;
|
||||||
filePath: string;
|
filePath: string;
|
||||||
baseDir: string;
|
baseDir: string;
|
||||||
handlerPath: string;
|
handlerPath: string;
|
||||||
@ -33,6 +34,7 @@ export type HookStatusEntry = {
|
|||||||
always: boolean;
|
always: boolean;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
eligible: boolean;
|
eligible: boolean;
|
||||||
|
managedByPlugin: boolean;
|
||||||
requirements: {
|
requirements: {
|
||||||
bins: string[];
|
bins: string[];
|
||||||
anyBins: string[];
|
anyBins: string[];
|
||||||
@ -94,7 +96,8 @@ function buildHookStatus(
|
|||||||
): HookStatusEntry {
|
): HookStatusEntry {
|
||||||
const hookKey = resolveHookKey(entry);
|
const hookKey = resolveHookKey(entry);
|
||||||
const hookConfig = resolveHookConfig(config, hookKey);
|
const hookConfig = resolveHookConfig(config, hookKey);
|
||||||
const disabled = hookConfig?.enabled === false;
|
const managedByPlugin = entry.hook.source === "clawdbot-plugin";
|
||||||
|
const disabled = managedByPlugin ? false : hookConfig?.enabled === false;
|
||||||
const always = entry.clawdbot?.always === true;
|
const always = entry.clawdbot?.always === true;
|
||||||
const emoji = entry.clawdbot?.emoji ?? entry.frontmatter.emoji;
|
const emoji = entry.clawdbot?.emoji ?? entry.frontmatter.emoji;
|
||||||
const homepageRaw =
|
const homepageRaw =
|
||||||
@ -171,6 +174,7 @@ function buildHookStatus(
|
|||||||
name: entry.hook.name,
|
name: entry.hook.name,
|
||||||
description: entry.hook.description,
|
description: entry.hook.description,
|
||||||
source: entry.hook.source,
|
source: entry.hook.source,
|
||||||
|
pluginId: entry.hook.pluginId,
|
||||||
filePath: entry.hook.filePath,
|
filePath: entry.hook.filePath,
|
||||||
baseDir: entry.hook.baseDir,
|
baseDir: entry.hook.baseDir,
|
||||||
handlerPath: entry.hook.handlerPath,
|
handlerPath: entry.hook.handlerPath,
|
||||||
@ -181,6 +185,7 @@ function buildHookStatus(
|
|||||||
always,
|
always,
|
||||||
disabled,
|
disabled,
|
||||||
eligible,
|
eligible,
|
||||||
|
managedByPlugin,
|
||||||
requirements: {
|
requirements: {
|
||||||
bins: requiredBins,
|
bins: requiredBins,
|
||||||
anyBins: requiredAnyBins,
|
anyBins: requiredAnyBins,
|
||||||
|
|||||||
115
src/hooks/plugin-hooks.ts
Normal file
115
src/hooks/plugin-hooks.ts
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
import path from "node:path";
|
||||||
|
import { pathToFileURL } from "node:url";
|
||||||
|
|
||||||
|
import type { ClawdbotPluginApi } from "../plugins/types.js";
|
||||||
|
import type { HookEntry } from "./types.js";
|
||||||
|
import { shouldIncludeHook } from "./config.js";
|
||||||
|
import { loadHookEntriesFromDir } from "./workspace.js";
|
||||||
|
import type { InternalHookHandler } from "./internal-hooks.js";
|
||||||
|
|
||||||
|
export type PluginHookLoadResult = {
|
||||||
|
hooks: HookEntry[];
|
||||||
|
loaded: number;
|
||||||
|
skipped: number;
|
||||||
|
errors: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
function resolveHookDir(api: ClawdbotPluginApi, dir: string): string {
|
||||||
|
if (path.isAbsolute(dir)) return dir;
|
||||||
|
return path.resolve(path.dirname(api.source), dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePluginHookEntry(api: ClawdbotPluginApi, entry: HookEntry): HookEntry {
|
||||||
|
return {
|
||||||
|
...entry,
|
||||||
|
hook: {
|
||||||
|
...entry.hook,
|
||||||
|
source: "clawdbot-plugin",
|
||||||
|
pluginId: api.id,
|
||||||
|
},
|
||||||
|
clawdbot: {
|
||||||
|
...entry.clawdbot,
|
||||||
|
hookKey: entry.clawdbot?.hookKey ?? `${api.id}:${entry.hook.name}`,
|
||||||
|
events: entry.clawdbot?.events ?? [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadHookHandler(
|
||||||
|
entry: HookEntry,
|
||||||
|
api: ClawdbotPluginApi,
|
||||||
|
): Promise<InternalHookHandler | null> {
|
||||||
|
try {
|
||||||
|
const url = pathToFileURL(entry.hook.handlerPath).href;
|
||||||
|
const cacheBustedUrl = `${url}?t=${Date.now()}`;
|
||||||
|
const mod = (await import(cacheBustedUrl)) as Record<string, unknown>;
|
||||||
|
const exportName = entry.clawdbot?.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: ClawdbotPluginApi,
|
||||||
|
dir: string,
|
||||||
|
): Promise<PluginHookLoadResult> {
|
||||||
|
const resolvedDir = resolveHookDir(api, dir);
|
||||||
|
const hooks = loadHookEntriesFromDir({
|
||||||
|
dir: resolvedDir,
|
||||||
|
source: "clawdbot-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.clawdbot?.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;
|
||||||
|
}
|
||||||
@ -35,12 +35,15 @@ export type ParsedHookFrontmatter = Record<string, string>;
|
|||||||
export type Hook = {
|
export type Hook = {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
source: "clawdbot-bundled" | "clawdbot-managed" | "clawdbot-workspace";
|
source: "clawdbot-bundled" | "clawdbot-managed" | "clawdbot-workspace" | "clawdbot-plugin";
|
||||||
|
pluginId?: string;
|
||||||
filePath: string; // Path to HOOK.md
|
filePath: string; // Path to HOOK.md
|
||||||
baseDir: string; // Directory containing hook
|
baseDir: string; // Directory containing hook
|
||||||
handlerPath: string; // Path to handler module (handler.ts/js)
|
handlerPath: string; // Path to handler module (handler.ts/js)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type HookSource = Hook["source"];
|
||||||
|
|
||||||
export type HookEntry = {
|
export type HookEntry = {
|
||||||
hook: Hook;
|
hook: Hook;
|
||||||
frontmatter: ParsedHookFrontmatter;
|
frontmatter: ParsedHookFrontmatter;
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import type {
|
|||||||
HookEligibilityContext,
|
HookEligibilityContext,
|
||||||
HookEntry,
|
HookEntry,
|
||||||
HookSnapshot,
|
HookSnapshot,
|
||||||
|
HookSource,
|
||||||
ParsedHookFrontmatter,
|
ParsedHookFrontmatter,
|
||||||
} from "./types.js";
|
} from "./types.js";
|
||||||
|
|
||||||
@ -50,7 +51,8 @@ function resolvePackageHooks(manifest: HookPackageManifest): string[] {
|
|||||||
|
|
||||||
function loadHookFromDir(params: {
|
function loadHookFromDir(params: {
|
||||||
hookDir: string;
|
hookDir: string;
|
||||||
source: string;
|
source: HookSource;
|
||||||
|
pluginId?: string;
|
||||||
nameHint?: string;
|
nameHint?: string;
|
||||||
}): Hook | null {
|
}): Hook | null {
|
||||||
const hookMdPath = path.join(params.hookDir, "HOOK.md");
|
const hookMdPath = path.join(params.hookDir, "HOOK.md");
|
||||||
@ -82,6 +84,7 @@ function loadHookFromDir(params: {
|
|||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
source: params.source as Hook["source"],
|
source: params.source as Hook["source"],
|
||||||
|
pluginId: params.pluginId,
|
||||||
filePath: hookMdPath,
|
filePath: hookMdPath,
|
||||||
baseDir: params.hookDir,
|
baseDir: params.hookDir,
|
||||||
handlerPath,
|
handlerPath,
|
||||||
@ -95,8 +98,8 @@ function loadHookFromDir(params: {
|
|||||||
/**
|
/**
|
||||||
* Scan a directory for hooks (subdirectories containing HOOK.md)
|
* Scan a directory for hooks (subdirectories containing HOOK.md)
|
||||||
*/
|
*/
|
||||||
function loadHooksFromDir(params: { dir: string; source: string }): Hook[] {
|
function loadHooksFromDir(params: { dir: string; source: HookSource; pluginId?: string }): Hook[] {
|
||||||
const { dir, source } = params;
|
const { dir, source, pluginId } = params;
|
||||||
|
|
||||||
if (!fs.existsSync(dir)) return [];
|
if (!fs.existsSync(dir)) return [];
|
||||||
|
|
||||||
@ -119,6 +122,7 @@ function loadHooksFromDir(params: { dir: string; source: string }): Hook[] {
|
|||||||
const hook = loadHookFromDir({
|
const hook = loadHookFromDir({
|
||||||
hookDir: resolvedHookDir,
|
hookDir: resolvedHookDir,
|
||||||
source,
|
source,
|
||||||
|
pluginId,
|
||||||
nameHint: path.basename(resolvedHookDir),
|
nameHint: path.basename(resolvedHookDir),
|
||||||
});
|
});
|
||||||
if (hook) hooks.push(hook);
|
if (hook) hooks.push(hook);
|
||||||
@ -126,13 +130,50 @@ function loadHooksFromDir(params: { dir: string; source: string }): Hook[] {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hook = loadHookFromDir({ hookDir, source, nameHint: entry.name });
|
const hook = loadHookFromDir({
|
||||||
|
hookDir,
|
||||||
|
source,
|
||||||
|
pluginId,
|
||||||
|
nameHint: entry.name,
|
||||||
|
});
|
||||||
if (hook) hooks.push(hook);
|
if (hook) hooks.push(hook);
|
||||||
}
|
}
|
||||||
|
|
||||||
return hooks;
|
return hooks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function loadHookEntriesFromDir(params: {
|
||||||
|
dir: string;
|
||||||
|
source: HookSource;
|
||||||
|
pluginId?: string;
|
||||||
|
}): HookEntry[] {
|
||||||
|
const hooks = loadHooksFromDir({
|
||||||
|
dir: params.dir,
|
||||||
|
source: params.source,
|
||||||
|
pluginId: params.pluginId,
|
||||||
|
});
|
||||||
|
return hooks.map((hook) => {
|
||||||
|
let frontmatter: ParsedHookFrontmatter = {};
|
||||||
|
try {
|
||||||
|
const raw = fs.readFileSync(hook.filePath, "utf-8");
|
||||||
|
frontmatter = parseFrontmatter(raw);
|
||||||
|
} catch {
|
||||||
|
// ignore malformed hooks
|
||||||
|
}
|
||||||
|
const entry: HookEntry = {
|
||||||
|
hook: {
|
||||||
|
...hook,
|
||||||
|
source: params.source,
|
||||||
|
pluginId: params.pluginId,
|
||||||
|
},
|
||||||
|
frontmatter,
|
||||||
|
clawdbot: resolveClawdbotMetadata(frontmatter),
|
||||||
|
invocation: resolveHookInvocationPolicy(frontmatter),
|
||||||
|
};
|
||||||
|
return entry;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function loadHookEntries(
|
function loadHookEntries(
|
||||||
workspaceDir: string,
|
workspaceDir: string,
|
||||||
opts?: {
|
opts?: {
|
||||||
@ -178,7 +219,7 @@ function loadHookEntries(
|
|||||||
for (const hook of managedHooks) merged.set(hook.name, hook);
|
for (const hook of managedHooks) merged.set(hook.name, hook);
|
||||||
for (const hook of workspaceHooks) merged.set(hook.name, hook);
|
for (const hook of workspaceHooks) merged.set(hook.name, hook);
|
||||||
|
|
||||||
const hookEntries: HookEntry[] = Array.from(merged.values()).map((hook) => {
|
return Array.from(merged.values()).map((hook) => {
|
||||||
let frontmatter: ParsedHookFrontmatter = {};
|
let frontmatter: ParsedHookFrontmatter = {};
|
||||||
try {
|
try {
|
||||||
const raw = fs.readFileSync(hook.filePath, "utf-8");
|
const raw = fs.readFileSync(hook.filePath, "utf-8");
|
||||||
@ -193,7 +234,6 @@ function loadHookEntries(
|
|||||||
invocation: resolveHookInvocationPolicy(frontmatter),
|
invocation: resolveHookInvocationPolicy(frontmatter),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
return hookEntries;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildWorkspaceHookSnapshot(
|
export function buildWorkspaceHookSnapshot(
|
||||||
|
|||||||
@ -162,3 +162,5 @@ export { createMemoryGetTool, createMemorySearchTool } from "../agents/tools/mem
|
|||||||
export { registerMemoryCli } from "../cli/memory-cli.js";
|
export { registerMemoryCli } from "../cli/memory-cli.js";
|
||||||
|
|
||||||
export { formatDocsLink } from "../terminal/links.js";
|
export { formatDocsLink } from "../terminal/links.js";
|
||||||
|
export type { HookEntry } from "../hooks/types.js";
|
||||||
|
export { registerPluginHooksFromDir } from "../hooks/plugin-hooks.js";
|
||||||
|
|||||||
@ -264,6 +264,7 @@ function createPluginRecord(params: {
|
|||||||
enabled: params.enabled,
|
enabled: params.enabled,
|
||||||
status: params.enabled ? "loaded" : "disabled",
|
status: params.enabled ? "loaded" : "disabled",
|
||||||
toolNames: [],
|
toolNames: [],
|
||||||
|
hookNames: [],
|
||||||
channelIds: [],
|
channelIds: [],
|
||||||
providerIds: [],
|
providerIds: [],
|
||||||
gatewayMethods: [],
|
gatewayMethods: [],
|
||||||
|
|||||||
@ -5,12 +5,14 @@ import type {
|
|||||||
GatewayRequestHandler,
|
GatewayRequestHandler,
|
||||||
GatewayRequestHandlers,
|
GatewayRequestHandlers,
|
||||||
} from "../gateway/server-methods/types.js";
|
} from "../gateway/server-methods/types.js";
|
||||||
|
import { registerInternalHook } from "../hooks/internal-hooks.js";
|
||||||
import { resolveUserPath } from "../utils.js";
|
import { resolveUserPath } from "../utils.js";
|
||||||
import type {
|
import type {
|
||||||
ClawdbotPluginApi,
|
ClawdbotPluginApi,
|
||||||
ClawdbotPluginChannelRegistration,
|
ClawdbotPluginChannelRegistration,
|
||||||
ClawdbotPluginCliRegistrar,
|
ClawdbotPluginCliRegistrar,
|
||||||
ClawdbotPluginHttpHandler,
|
ClawdbotPluginHttpHandler,
|
||||||
|
ClawdbotPluginHookOptions,
|
||||||
ProviderPlugin,
|
ProviderPlugin,
|
||||||
ClawdbotPluginService,
|
ClawdbotPluginService,
|
||||||
ClawdbotPluginToolContext,
|
ClawdbotPluginToolContext,
|
||||||
@ -22,6 +24,8 @@ import type {
|
|||||||
PluginKind,
|
PluginKind,
|
||||||
} from "./types.js";
|
} from "./types.js";
|
||||||
import type { PluginRuntime } from "./runtime/types.js";
|
import type { PluginRuntime } from "./runtime/types.js";
|
||||||
|
import type { HookEntry } from "../hooks/types.js";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
export type PluginToolRegistration = {
|
export type PluginToolRegistration = {
|
||||||
pluginId: string;
|
pluginId: string;
|
||||||
@ -57,6 +61,13 @@ export type PluginProviderRegistration = {
|
|||||||
source: string;
|
source: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type PluginHookRegistration = {
|
||||||
|
pluginId: string;
|
||||||
|
entry: HookEntry;
|
||||||
|
events: string[];
|
||||||
|
source: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type PluginServiceRegistration = {
|
export type PluginServiceRegistration = {
|
||||||
pluginId: string;
|
pluginId: string;
|
||||||
service: ClawdbotPluginService;
|
service: ClawdbotPluginService;
|
||||||
@ -76,6 +87,7 @@ export type PluginRecord = {
|
|||||||
status: "loaded" | "disabled" | "error";
|
status: "loaded" | "disabled" | "error";
|
||||||
error?: string;
|
error?: string;
|
||||||
toolNames: string[];
|
toolNames: string[];
|
||||||
|
hookNames: string[];
|
||||||
channelIds: string[];
|
channelIds: string[];
|
||||||
providerIds: string[];
|
providerIds: string[];
|
||||||
gatewayMethods: string[];
|
gatewayMethods: string[];
|
||||||
@ -90,6 +102,7 @@ export type PluginRecord = {
|
|||||||
export type PluginRegistry = {
|
export type PluginRegistry = {
|
||||||
plugins: PluginRecord[];
|
plugins: PluginRecord[];
|
||||||
tools: PluginToolRegistration[];
|
tools: PluginToolRegistration[];
|
||||||
|
hooks: PluginHookRegistration[];
|
||||||
channels: PluginChannelRegistration[];
|
channels: PluginChannelRegistration[];
|
||||||
providers: PluginProviderRegistration[];
|
providers: PluginProviderRegistration[];
|
||||||
gatewayHandlers: GatewayRequestHandlers;
|
gatewayHandlers: GatewayRequestHandlers;
|
||||||
@ -109,6 +122,7 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
|||||||
const registry: PluginRegistry = {
|
const registry: PluginRegistry = {
|
||||||
plugins: [],
|
plugins: [],
|
||||||
tools: [],
|
tools: [],
|
||||||
|
hooks: [],
|
||||||
channels: [],
|
channels: [],
|
||||||
providers: [],
|
providers: [],
|
||||||
gatewayHandlers: {},
|
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 = (
|
const registerGatewayMethod = (
|
||||||
record: PluginRecord,
|
record: PluginRecord,
|
||||||
method: string,
|
method: string,
|
||||||
@ -287,6 +371,8 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
|||||||
runtime: registryParams.runtime,
|
runtime: registryParams.runtime,
|
||||||
logger: normalizeLogger(registryParams.logger),
|
logger: normalizeLogger(registryParams.logger),
|
||||||
registerTool: (tool, opts) => registerTool(record, tool, opts),
|
registerTool: (tool, opts) => registerTool(record, tool, opts),
|
||||||
|
registerHook: (events, handler, opts) =>
|
||||||
|
registerHook(record, events, handler, opts, params.config),
|
||||||
registerHttpHandler: (handler) => registerHttpHandler(record, handler),
|
registerHttpHandler: (handler) => registerHttpHandler(record, handler),
|
||||||
registerChannel: (registration) => registerChannel(record, registration),
|
registerChannel: (registration) => registerChannel(record, registration),
|
||||||
registerProvider: (provider) => registerProvider(record, provider),
|
registerProvider: (provider) => registerProvider(record, provider),
|
||||||
|
|||||||
@ -6,6 +6,8 @@ import type { AnyAgentTool } from "../agents/tools/common.js";
|
|||||||
import type { ChannelDock } from "../channels/dock.js";
|
import type { ChannelDock } from "../channels/dock.js";
|
||||||
import type { ChannelPlugin } from "../channels/plugins/types.js";
|
import type { ChannelPlugin } from "../channels/plugins/types.js";
|
||||||
import type { ClawdbotConfig } from "../config/config.js";
|
import type { ClawdbotConfig } from "../config/config.js";
|
||||||
|
import type { InternalHookHandler } from "../hooks/internal-hooks.js";
|
||||||
|
import type { HookEntry } from "../hooks/types.js";
|
||||||
import type { ModelProviderConfig } from "../config/types.js";
|
import type { ModelProviderConfig } from "../config/types.js";
|
||||||
import type { RuntimeEnv } from "../runtime.js";
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
import type { WizardPrompter } from "../wizard/prompts.js";
|
import type { WizardPrompter } from "../wizard/prompts.js";
|
||||||
@ -71,6 +73,13 @@ export type ClawdbotPluginToolOptions = {
|
|||||||
optional?: boolean;
|
optional?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ClawdbotPluginHookOptions = {
|
||||||
|
entry?: HookEntry;
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
register?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export type ProviderAuthKind = "oauth" | "api_key" | "token" | "device_code" | "custom";
|
export type ProviderAuthKind = "oauth" | "api_key" | "token" | "device_code" | "custom";
|
||||||
|
|
||||||
export type ProviderAuthResult = {
|
export type ProviderAuthResult = {
|
||||||
@ -179,6 +188,11 @@ export type ClawdbotPluginApi = {
|
|||||||
tool: AnyAgentTool | ClawdbotPluginToolFactory,
|
tool: AnyAgentTool | ClawdbotPluginToolFactory,
|
||||||
opts?: ClawdbotPluginToolOptions,
|
opts?: ClawdbotPluginToolOptions,
|
||||||
) => void;
|
) => void;
|
||||||
|
registerHook: (
|
||||||
|
events: string | string[],
|
||||||
|
handler: InternalHookHandler,
|
||||||
|
opts?: ClawdbotPluginHookOptions,
|
||||||
|
) => void;
|
||||||
registerHttpHandler: (handler: ClawdbotPluginHttpHandler) => void;
|
registerHttpHandler: (handler: ClawdbotPluginHttpHandler) => void;
|
||||||
registerChannel: (registration: ClawdbotPluginChannelRegistration | ChannelPlugin) => void;
|
registerChannel: (registration: ClawdbotPluginChannelRegistration | ChannelPlugin) => void;
|
||||||
registerGatewayMethod: (method: string, handler: GatewayRequestHandler) => void;
|
registerGatewayMethod: (method: string, handler: GatewayRequestHandler) => void;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user