- Extended PluginHookMessageSentEvent to include usage stats (tokens, model, provider, duration) - Extended PluginHookMessageContext to include sessionKey and agentId - Wired up runMessageSent hook call in dispatch-from-config.ts after final replies - Added discord-status example plugin demonstrating how to use the hook The message_sent hook now fires after each reply is delivered, enabling plugins to: - Track token usage across sessions - Update external status indicators (e.g., Discord bot presence) - Log message delivery for analytics/monitoring Note: Full Discord presence support requires exposing setPresence via PluginRuntime. The example plugin currently logs the status it would set.
545 lines
15 KiB
TypeScript
545 lines
15 KiB
TypeScript
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
import type { Command } from "commander";
|
|
|
|
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
|
|
|
import type { AuthProfileCredential, OAuthCredential } from "../agents/auth-profiles/types.js";
|
|
import type { AnyAgentTool } from "../agents/tools/common.js";
|
|
import type { ChannelDock } from "../channels/dock.js";
|
|
import type { ChannelPlugin } from "../channels/plugins/types.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 { RuntimeEnv } from "../runtime.js";
|
|
import type { ReplyPayload } from "../auto-reply/types.js";
|
|
import type { WizardPrompter } from "../wizard/prompts.js";
|
|
import type { createVpsAwareOAuthHandlers } from "../commands/oauth-flow.js";
|
|
import type { GatewayRequestHandler } from "../gateway/server-methods/types.js";
|
|
import type { PluginRuntime } from "./runtime/types.js";
|
|
|
|
export type { PluginRuntime } from "./runtime/types.js";
|
|
|
|
export type PluginLogger = {
|
|
debug?: (message: string) => void;
|
|
info: (message: string) => void;
|
|
warn: (message: string) => void;
|
|
error: (message: string) => void;
|
|
};
|
|
|
|
export type PluginConfigUiHint = {
|
|
label?: string;
|
|
help?: string;
|
|
advanced?: boolean;
|
|
sensitive?: boolean;
|
|
placeholder?: string;
|
|
};
|
|
|
|
export type PluginKind = "memory";
|
|
|
|
export type PluginConfigValidation =
|
|
| { ok: true; value?: unknown }
|
|
| { ok: false; errors: string[] };
|
|
|
|
export type ClawdbotPluginConfigSchema = {
|
|
safeParse?: (value: unknown) => {
|
|
success: boolean;
|
|
data?: unknown;
|
|
error?: {
|
|
issues?: Array<{ path: Array<string | number>; message: string }>;
|
|
};
|
|
};
|
|
parse?: (value: unknown) => unknown;
|
|
validate?: (value: unknown) => PluginConfigValidation;
|
|
uiHints?: Record<string, PluginConfigUiHint>;
|
|
jsonSchema?: Record<string, unknown>;
|
|
};
|
|
|
|
export type ClawdbotPluginToolContext = {
|
|
config?: ClawdbotConfig;
|
|
workspaceDir?: string;
|
|
agentDir?: string;
|
|
agentId?: string;
|
|
sessionKey?: string;
|
|
messageChannel?: string;
|
|
agentAccountId?: string;
|
|
sandboxed?: boolean;
|
|
};
|
|
|
|
export type ClawdbotPluginToolFactory = (
|
|
ctx: ClawdbotPluginToolContext,
|
|
) => AnyAgentTool | AnyAgentTool[] | null | undefined;
|
|
|
|
export type ClawdbotPluginToolOptions = {
|
|
name?: string;
|
|
names?: string[];
|
|
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 ProviderAuthResult = {
|
|
profiles: Array<{ profileId: string; credential: AuthProfileCredential }>;
|
|
configPatch?: Partial<ClawdbotConfig>;
|
|
defaultModel?: string;
|
|
notes?: string[];
|
|
};
|
|
|
|
export type ProviderAuthContext = {
|
|
config: ClawdbotConfig;
|
|
agentDir?: string;
|
|
workspaceDir?: string;
|
|
prompter: WizardPrompter;
|
|
runtime: RuntimeEnv;
|
|
isRemote: boolean;
|
|
openUrl: (url: string) => Promise<void>;
|
|
oauth: {
|
|
createVpsAwareHandlers: typeof createVpsAwareOAuthHandlers;
|
|
};
|
|
};
|
|
|
|
export type ProviderAuthMethod = {
|
|
id: string;
|
|
label: string;
|
|
hint?: string;
|
|
kind: ProviderAuthKind;
|
|
run: (ctx: ProviderAuthContext) => Promise<ProviderAuthResult>;
|
|
};
|
|
|
|
export type ProviderPlugin = {
|
|
id: string;
|
|
label: string;
|
|
docsPath?: string;
|
|
aliases?: string[];
|
|
envVars?: string[];
|
|
models?: ModelProviderConfig;
|
|
auth: ProviderAuthMethod[];
|
|
formatApiKey?: (cred: AuthProfileCredential) => string;
|
|
refreshOAuth?: (cred: OAuthCredential) => Promise<OAuthCredential>;
|
|
};
|
|
|
|
export type ClawdbotPluginGatewayMethod = {
|
|
method: string;
|
|
handler: GatewayRequestHandler;
|
|
};
|
|
|
|
// =============================================================================
|
|
// Plugin Commands
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Context passed to plugin command handlers.
|
|
*/
|
|
export type PluginCommandContext = {
|
|
/** The sender's identifier (e.g., Telegram user ID) */
|
|
senderId?: string;
|
|
/** The channel/surface (e.g., "telegram", "discord") */
|
|
channel: string;
|
|
/** Whether the sender is on the allowlist */
|
|
isAuthorizedSender: boolean;
|
|
/** Raw command arguments after the command name */
|
|
args?: string;
|
|
/** The full normalized command body */
|
|
commandBody: string;
|
|
/** Current clawdbot configuration */
|
|
config: ClawdbotConfig;
|
|
};
|
|
|
|
/**
|
|
* Result returned by a plugin command handler.
|
|
*/
|
|
export type PluginCommandResult = ReplyPayload;
|
|
|
|
/**
|
|
* Handler function for plugin commands.
|
|
*/
|
|
export type PluginCommandHandler = (
|
|
ctx: PluginCommandContext,
|
|
) => PluginCommandResult | Promise<PluginCommandResult>;
|
|
|
|
/**
|
|
* Definition for a plugin-registered command.
|
|
*/
|
|
export type ClawdbotPluginCommandDefinition = {
|
|
/** Command name without leading slash (e.g., "tts") */
|
|
name: string;
|
|
/** Description shown in /help and command menus */
|
|
description: string;
|
|
/** Whether this command accepts arguments */
|
|
acceptsArgs?: boolean;
|
|
/** Whether only authorized senders can use this command (default: true) */
|
|
requireAuth?: boolean;
|
|
/** The handler function */
|
|
handler: PluginCommandHandler;
|
|
};
|
|
|
|
export type ClawdbotPluginHttpHandler = (
|
|
req: IncomingMessage,
|
|
res: ServerResponse,
|
|
) => Promise<boolean> | boolean;
|
|
|
|
export type ClawdbotPluginHttpRouteHandler = (
|
|
req: IncomingMessage,
|
|
res: ServerResponse,
|
|
) => Promise<void> | void;
|
|
|
|
export type ClawdbotPluginCliContext = {
|
|
program: Command;
|
|
config: ClawdbotConfig;
|
|
workspaceDir?: string;
|
|
logger: PluginLogger;
|
|
};
|
|
|
|
export type ClawdbotPluginCliRegistrar = (ctx: ClawdbotPluginCliContext) => void | Promise<void>;
|
|
|
|
export type ClawdbotPluginServiceContext = {
|
|
config: ClawdbotConfig;
|
|
workspaceDir?: string;
|
|
stateDir: string;
|
|
logger: PluginLogger;
|
|
};
|
|
|
|
export type ClawdbotPluginService = {
|
|
id: string;
|
|
start: (ctx: ClawdbotPluginServiceContext) => void | Promise<void>;
|
|
stop?: (ctx: ClawdbotPluginServiceContext) => void | Promise<void>;
|
|
};
|
|
|
|
export type ClawdbotPluginChannelRegistration = {
|
|
plugin: ChannelPlugin;
|
|
dock?: ChannelDock;
|
|
};
|
|
|
|
export type ClawdbotPluginDefinition = {
|
|
id?: string;
|
|
name?: string;
|
|
description?: string;
|
|
version?: string;
|
|
kind?: PluginKind;
|
|
configSchema?: ClawdbotPluginConfigSchema;
|
|
register?: (api: ClawdbotPluginApi) => void | Promise<void>;
|
|
activate?: (api: ClawdbotPluginApi) => void | Promise<void>;
|
|
};
|
|
|
|
export type ClawdbotPluginModule =
|
|
| ClawdbotPluginDefinition
|
|
| ((api: ClawdbotPluginApi) => void | Promise<void>);
|
|
|
|
export type ClawdbotPluginApi = {
|
|
id: string;
|
|
name: string;
|
|
version?: string;
|
|
description?: string;
|
|
source: string;
|
|
config: ClawdbotConfig;
|
|
pluginConfig?: Record<string, unknown>;
|
|
runtime: PluginRuntime;
|
|
logger: PluginLogger;
|
|
registerTool: (
|
|
tool: AnyAgentTool | ClawdbotPluginToolFactory,
|
|
opts?: ClawdbotPluginToolOptions,
|
|
) => void;
|
|
registerHook: (
|
|
events: string | string[],
|
|
handler: InternalHookHandler,
|
|
opts?: ClawdbotPluginHookOptions,
|
|
) => void;
|
|
registerHttpHandler: (handler: ClawdbotPluginHttpHandler) => void;
|
|
registerHttpRoute: (params: { path: string; handler: ClawdbotPluginHttpRouteHandler }) => void;
|
|
registerChannel: (registration: ClawdbotPluginChannelRegistration | ChannelPlugin) => void;
|
|
registerGatewayMethod: (method: string, handler: GatewayRequestHandler) => void;
|
|
registerCli: (registrar: ClawdbotPluginCliRegistrar, opts?: { commands?: string[] }) => void;
|
|
registerService: (service: ClawdbotPluginService) => void;
|
|
registerProvider: (provider: ProviderPlugin) => void;
|
|
/**
|
|
* Register a custom command that bypasses the LLM agent.
|
|
* Plugin commands are processed before built-in commands and before agent invocation.
|
|
* Use this for simple state-toggling or status commands that don't need AI reasoning.
|
|
*/
|
|
registerCommand: (command: ClawdbotPluginCommandDefinition) => void;
|
|
resolvePath: (input: string) => string;
|
|
/** Register a lifecycle hook handler */
|
|
on: <K extends PluginHookName>(
|
|
hookName: K,
|
|
handler: PluginHookHandlerMap[K],
|
|
opts?: { priority?: number },
|
|
) => void;
|
|
};
|
|
|
|
export type PluginOrigin = "bundled" | "global" | "workspace" | "config";
|
|
|
|
export type PluginDiagnostic = {
|
|
level: "warn" | "error";
|
|
message: string;
|
|
pluginId?: string;
|
|
source?: string;
|
|
};
|
|
|
|
// ============================================================================
|
|
// Plugin Hooks
|
|
// ============================================================================
|
|
|
|
export type PluginHookName =
|
|
| "before_agent_start"
|
|
| "agent_end"
|
|
| "before_compaction"
|
|
| "after_compaction"
|
|
| "message_received"
|
|
| "message_sending"
|
|
| "message_sent"
|
|
| "before_tool_call"
|
|
| "after_tool_call"
|
|
| "tool_result_persist"
|
|
| "session_start"
|
|
| "session_end"
|
|
| "gateway_start"
|
|
| "gateway_stop";
|
|
|
|
// Agent context shared across agent hooks
|
|
export type PluginHookAgentContext = {
|
|
agentId?: string;
|
|
sessionKey?: string;
|
|
workspaceDir?: string;
|
|
messageProvider?: string;
|
|
};
|
|
|
|
// before_agent_start hook
|
|
export type PluginHookBeforeAgentStartEvent = {
|
|
prompt: string;
|
|
messages?: unknown[];
|
|
};
|
|
|
|
export type PluginHookBeforeAgentStartResult = {
|
|
systemPrompt?: string;
|
|
prependContext?: string;
|
|
};
|
|
|
|
// agent_end hook
|
|
export type PluginHookAgentEndEvent = {
|
|
messages: unknown[];
|
|
success: boolean;
|
|
error?: string;
|
|
durationMs?: number;
|
|
};
|
|
|
|
// Compaction hooks
|
|
export type PluginHookBeforeCompactionEvent = {
|
|
messageCount: number;
|
|
tokenCount?: number;
|
|
};
|
|
|
|
export type PluginHookAfterCompactionEvent = {
|
|
messageCount: number;
|
|
tokenCount?: number;
|
|
compactedCount: number;
|
|
};
|
|
|
|
// Message context
|
|
export type PluginHookMessageContext = {
|
|
channelId: string;
|
|
accountId?: string;
|
|
conversationId?: string;
|
|
sessionKey?: string;
|
|
agentId?: string;
|
|
};
|
|
|
|
// message_received hook
|
|
export type PluginHookMessageReceivedEvent = {
|
|
from: string;
|
|
content: string;
|
|
timestamp?: number;
|
|
metadata?: Record<string, unknown>;
|
|
};
|
|
|
|
// message_sending hook
|
|
export type PluginHookMessageSendingEvent = {
|
|
to: string;
|
|
content: string;
|
|
metadata?: Record<string, unknown>;
|
|
};
|
|
|
|
export type PluginHookMessageSendingResult = {
|
|
content?: string;
|
|
cancel?: boolean;
|
|
};
|
|
|
|
// message_sent hook
|
|
export type PluginHookMessageSentEvent = {
|
|
to: string;
|
|
content: string;
|
|
success: boolean;
|
|
error?: string;
|
|
/** Usage statistics from the agent run (if available) */
|
|
usage?: {
|
|
inputTokens?: number;
|
|
outputTokens?: number;
|
|
totalTokens?: number;
|
|
cacheReadTokens?: number;
|
|
cacheCreationTokens?: number;
|
|
/** Model used for this response */
|
|
model?: string;
|
|
/** Provider used (e.g., "anthropic", "openai") */
|
|
provider?: string;
|
|
};
|
|
/** Duration of the agent run in milliseconds */
|
|
durationMs?: number;
|
|
};
|
|
|
|
// Tool context
|
|
export type PluginHookToolContext = {
|
|
agentId?: string;
|
|
sessionKey?: string;
|
|
toolName: string;
|
|
};
|
|
|
|
// before_tool_call hook
|
|
export type PluginHookBeforeToolCallEvent = {
|
|
toolName: string;
|
|
params: Record<string, unknown>;
|
|
};
|
|
|
|
export type PluginHookBeforeToolCallResult = {
|
|
params?: Record<string, unknown>;
|
|
block?: boolean;
|
|
blockReason?: string;
|
|
};
|
|
|
|
// after_tool_call hook
|
|
export type PluginHookAfterToolCallEvent = {
|
|
toolName: string;
|
|
params: Record<string, unknown>;
|
|
result?: unknown;
|
|
error?: string;
|
|
durationMs?: number;
|
|
};
|
|
|
|
// tool_result_persist hook
|
|
export type PluginHookToolResultPersistContext = {
|
|
agentId?: string;
|
|
sessionKey?: string;
|
|
toolName?: string;
|
|
toolCallId?: string;
|
|
};
|
|
|
|
export type PluginHookToolResultPersistEvent = {
|
|
toolName?: string;
|
|
toolCallId?: string;
|
|
/**
|
|
* The toolResult message about to be written to the session transcript.
|
|
* Handlers may return a modified message (e.g. drop non-essential fields).
|
|
*/
|
|
message: AgentMessage;
|
|
/** True when the tool result was synthesized by a guard/repair step. */
|
|
isSynthetic?: boolean;
|
|
};
|
|
|
|
export type PluginHookToolResultPersistResult = {
|
|
message?: AgentMessage;
|
|
};
|
|
|
|
// Session context
|
|
export type PluginHookSessionContext = {
|
|
agentId?: string;
|
|
sessionId: string;
|
|
};
|
|
|
|
// session_start hook
|
|
export type PluginHookSessionStartEvent = {
|
|
sessionId: string;
|
|
resumedFrom?: string;
|
|
};
|
|
|
|
// session_end hook
|
|
export type PluginHookSessionEndEvent = {
|
|
sessionId: string;
|
|
messageCount: number;
|
|
durationMs?: number;
|
|
};
|
|
|
|
// Gateway context
|
|
export type PluginHookGatewayContext = {
|
|
port?: number;
|
|
};
|
|
|
|
// gateway_start hook
|
|
export type PluginHookGatewayStartEvent = {
|
|
port: number;
|
|
};
|
|
|
|
// gateway_stop hook
|
|
export type PluginHookGatewayStopEvent = {
|
|
reason?: string;
|
|
};
|
|
|
|
// Hook handler types mapped by hook name
|
|
export type PluginHookHandlerMap = {
|
|
before_agent_start: (
|
|
event: PluginHookBeforeAgentStartEvent,
|
|
ctx: PluginHookAgentContext,
|
|
) => Promise<PluginHookBeforeAgentStartResult | void> | PluginHookBeforeAgentStartResult | void;
|
|
agent_end: (event: PluginHookAgentEndEvent, ctx: PluginHookAgentContext) => Promise<void> | void;
|
|
before_compaction: (
|
|
event: PluginHookBeforeCompactionEvent,
|
|
ctx: PluginHookAgentContext,
|
|
) => Promise<void> | void;
|
|
after_compaction: (
|
|
event: PluginHookAfterCompactionEvent,
|
|
ctx: PluginHookAgentContext,
|
|
) => Promise<void> | void;
|
|
message_received: (
|
|
event: PluginHookMessageReceivedEvent,
|
|
ctx: PluginHookMessageContext,
|
|
) => Promise<void> | void;
|
|
message_sending: (
|
|
event: PluginHookMessageSendingEvent,
|
|
ctx: PluginHookMessageContext,
|
|
) => Promise<PluginHookMessageSendingResult | void> | PluginHookMessageSendingResult | void;
|
|
message_sent: (
|
|
event: PluginHookMessageSentEvent,
|
|
ctx: PluginHookMessageContext,
|
|
) => Promise<void> | void;
|
|
before_tool_call: (
|
|
event: PluginHookBeforeToolCallEvent,
|
|
ctx: PluginHookToolContext,
|
|
) => Promise<PluginHookBeforeToolCallResult | void> | PluginHookBeforeToolCallResult | void;
|
|
after_tool_call: (
|
|
event: PluginHookAfterToolCallEvent,
|
|
ctx: PluginHookToolContext,
|
|
) => Promise<void> | void;
|
|
tool_result_persist: (
|
|
event: PluginHookToolResultPersistEvent,
|
|
ctx: PluginHookToolResultPersistContext,
|
|
) => PluginHookToolResultPersistResult | void;
|
|
session_start: (
|
|
event: PluginHookSessionStartEvent,
|
|
ctx: PluginHookSessionContext,
|
|
) => Promise<void> | void;
|
|
session_end: (
|
|
event: PluginHookSessionEndEvent,
|
|
ctx: PluginHookSessionContext,
|
|
) => Promise<void> | void;
|
|
gateway_start: (
|
|
event: PluginHookGatewayStartEvent,
|
|
ctx: PluginHookGatewayContext,
|
|
) => Promise<void> | void;
|
|
gateway_stop: (
|
|
event: PluginHookGatewayStopEvent,
|
|
ctx: PluginHookGatewayContext,
|
|
) => Promise<void> | void;
|
|
};
|
|
|
|
export type PluginHookRegistration<K extends PluginHookName = PluginHookName> = {
|
|
pluginId: string;
|
|
hookName: K;
|
|
handler: PluginHookHandlerMap[K];
|
|
priority?: number;
|
|
source: string;
|
|
};
|