diff --git a/extensions/contacts-search/index.ts b/extensions/contacts-search/index.ts index fec26ad3c..c2ea4c73e 100644 --- a/extensions/contacts-search/index.ts +++ b/extensions/contacts-search/index.ts @@ -1,9 +1,4 @@ -import type { - ChatCommandDefinition, - ClawdbotPluginApi, - PluginHookMessageContext, - PluginHookMessageReceivedEvent, -} from "clawdbot/plugin-sdk"; +import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk"; import { emptyPluginConfigSchema } from "clawdbot/plugin-sdk"; import { @@ -12,26 +7,9 @@ import { } from "./src/contacts/index.js"; import { registerContactsCli } from "./src/cli/contacts-cli.js"; import { registerSearchCli } from "./src/cli/search-cli.js"; -import { handleSearchCommand } from "./src/commands/search-command.js"; +import { runSearchCommand } from "./src/commands/search-command.js"; import { indexInboundMessage } from "./src/hooks/message-indexer.js"; -const SEARCH_COMMAND: ChatCommandDefinition = { - key: "search", - description: "Search messages across platforms.", - textAliases: ["/search"], - scope: "text", - acceptsArgs: true, - args: [ - { - name: "query", - description: "Search query", - type: "string", - required: true, - captureRemaining: true, - }, - ], -}; - const contactsSearchPlugin = { id: "contacts-search", name: "Contacts + Search", @@ -49,11 +27,16 @@ const contactsSearchPlugin = { { commands: ["contacts", "search"] }, ); - api.registerChatCommand(SEARCH_COMMAND, handleSearchCommand); + api.registerCommand({ + name: "search", + description: "Search messages across platforms.", + acceptsArgs: true, + handler: async (ctx) => ({ text: runSearchCommand(ctx.commandBody) }), + }); api.on( "message_received", - (event: PluginHookMessageReceivedEvent, ctx: PluginHookMessageContext) => { + (event, ctx) => { indexInboundMessage({ event, ctx, logger: api.logger }); }, ); diff --git a/extensions/contacts-search/src/commands/search-command.ts b/extensions/contacts-search/src/commands/search-command.ts index 7e912f997..806ebdb60 100644 --- a/extensions/contacts-search/src/commands/search-command.ts +++ b/extensions/contacts-search/src/commands/search-command.ts @@ -1,5 +1,3 @@ -import type { PluginChatCommandHandler } from "clawdbot/plugin-sdk"; - import { getContactStore } from "../contacts/index.js"; import { parseSearchArgs } from "./search-args.js"; @@ -24,32 +22,15 @@ function formatTimestamp(ts: number): string { return date.toLocaleDateString([], { month: "short", day: "numeric" }); } -/** - * Handle the /search command for cross-platform message search. - */ -export const handleSearchCommand: PluginChatCommandHandler = async (params, allowTextCommands) => { - if (!allowTextCommands) return null; - - const normalized = params.command.commandBodyNormalized; - if (normalized !== "/search" && !normalized.startsWith("/search ")) return null; - - if (!params.command.isAuthorizedSender) { - return { shouldContinue: false }; - } - - // Parse arguments from commandBodyNormalized (mentions already stripped) - const parsed = parseSearchArgs(params.command.commandBodyNormalized); +export function runSearchCommand(commandBody: string): string { + const parsed = parseSearchArgs(commandBody); if (parsed.error) { - return { - shouldContinue: false, - reply: { text: `āŒ ${parsed.error}` }, - }; + return `āŒ ${parsed.error}`; } try { const store = getContactStore(); - // Search messages const results = store.searchMessages({ query: parsed.query, from: parsed.from, @@ -66,13 +47,9 @@ export const handleSearchCommand: PluginChatCommandHandler = async (params, allo msg += `\n\nāš ļø Note: No contacts found matching "${parsed.from}"`; } } - return { - shouldContinue: false, - reply: { text: msg }, - }; + return msg; } - // Format results const lines = [`šŸ” Search Results (${results.length})\n`]; for (const result of results) { @@ -90,14 +67,8 @@ export const handleSearchCommand: PluginChatCommandHandler = async (params, allo lines.push('Use the CLI for more results: clawdbot search "' + parsed.query + '" --limit 50'); } - return { - shouldContinue: false, - reply: { text: lines.join("\n").trim() }, - }; + return lines.join("\n").trim(); } catch (err) { - return { - shouldContinue: false, - reply: { text: `āŒ Search error: ${err instanceof Error ? err.message : String(err)}` }, - }; + return `āŒ Search error: ${err instanceof Error ? err.message : String(err)}`; } -}; +} diff --git a/extensions/contacts-search/src/hooks/message-indexer.ts b/extensions/contacts-search/src/hooks/message-indexer.ts index 6350fcce4..ce93d11a0 100644 --- a/extensions/contacts-search/src/hooks/message-indexer.ts +++ b/extensions/contacts-search/src/hooks/message-indexer.ts @@ -1,10 +1,5 @@ import { createHash, randomUUID } from "node:crypto"; -import type { - PluginHookMessageContext, - PluginHookMessageReceivedEvent, -} from "clawdbot/plugin-sdk"; - import { importContactFromMessage, getContactStore } from "../contacts/index.js"; import type { Platform } from "../contacts/types.js"; @@ -35,15 +30,32 @@ function resolveMessageId(params: { } export function indexInboundMessage(params: { - event: PluginHookMessageReceivedEvent; - ctx: PluginHookMessageContext; + event: { + from: string; + content: string; + timestamp?: number; + metadata?: Record; + }; + ctx: { + channelId: string; + accountId?: string; + conversationId?: string; + }; logger?: { warn?: (message: string) => void }; }): void { const { event, ctx, logger } = params; const channelId = (ctx.channelId ?? "").trim(); if (!channelId) return; - const senderId = (event.senderId ?? event.from ?? "").trim(); + const metadata = event.metadata ?? {}; + const meta = metadata as { + senderId?: string; + messageId?: string; + senderUsername?: string; + senderE164?: string; + senderName?: string; + }; + const senderId = String(meta.senderId ?? event.from ?? "").trim(); if (!senderId) return; const content = typeof event.content === "string" ? event.content.trim() : ""; @@ -52,8 +64,9 @@ export function indexInboundMessage(params: { typeof event.timestamp === "number" && Number.isFinite(event.timestamp) ? event.timestamp : Date.now(); + const metadataMessageId = meta.messageId; const messageId = resolveMessageId({ - messageId: event.messageId, + messageId: typeof metadataMessageId === "string" ? metadataMessageId : undefined, platform, senderId, timestamp, @@ -66,9 +79,9 @@ export function indexInboundMessage(params: { importContactFromMessage(store, { platform, platformId: senderId, - username: event.senderUsername ?? null, - phone: event.senderE164 ?? null, - displayName: event.senderName ?? null, + username: typeof meta.senderUsername === "string" ? meta.senderUsername : null, + phone: typeof meta.senderE164 === "string" ? meta.senderE164 : null, + displayName: typeof meta.senderName === "string" ? meta.senderName : null, }); if (!content) return; diff --git a/src/auto-reply/commands-registry.data.ts b/src/auto-reply/commands-registry.data.ts index 7739dbb1b..3e2ad8775 100644 --- a/src/auto-reply/commands-registry.data.ts +++ b/src/auto-reply/commands-registry.data.ts @@ -1,5 +1,5 @@ import { listChannelDocks } from "../channels/dock.js"; -import { requireActivePluginRegistry } from "../plugins/runtime.js"; +import { getActivePluginRegistry } from "../plugins/runtime.js"; import { listThinkingLevels } from "./thinking.js"; import { COMMAND_ARG_FORMATTERS } from "./commands-args.js"; import type { ChatCommandDefinition, CommandScope } from "./commands-registry.types.js"; @@ -113,9 +113,9 @@ function assertCommandRegistry(commands: ChatCommandDefinition[]): void { } let cachedCommands: ChatCommandDefinition[] | null = null; -let cachedRegistry: ReturnType | null = null; +let cachedRegistry: ReturnType | null = null; let cachedNativeCommandSurfaces: Set | null = null; -let cachedNativeRegistry: ReturnType | null = null; +let cachedNativeRegistry: ReturnType | null = null; function buildChatCommands(): ChatCommandDefinition[] { const commands: ChatCommandDefinition[] = [ @@ -563,11 +563,6 @@ function buildChatCommands(): ChatCommandDefinition[] { .map((dock) => defineDockCommand(dock)), ]; - const registry = requireActivePluginRegistry(); - if (registry.chatCommands.length > 0) { - commands.push(...registry.chatCommands.map((entry) => entry.command)); - } - registerAlias(commands, "whoami", "/id"); registerAlias(commands, "think", "/thinking", "/t"); registerAlias(commands, "verbose", "/v"); @@ -579,7 +574,7 @@ function buildChatCommands(): ChatCommandDefinition[] { } export function getChatCommands(): ChatCommandDefinition[] { - const registry = requireActivePluginRegistry(); + const registry = getActivePluginRegistry(); if (cachedCommands && registry === cachedRegistry) return cachedCommands; const commands = buildChatCommands(); cachedCommands = commands; @@ -589,7 +584,7 @@ export function getChatCommands(): ChatCommandDefinition[] { } export function getNativeCommandSurfaces(): Set { - const registry = requireActivePluginRegistry(); + const registry = getActivePluginRegistry(); if (cachedNativeCommandSurfaces && registry === cachedNativeRegistry) { return cachedNativeCommandSurfaces; } diff --git a/src/auto-reply/commands-registry.test.ts b/src/auto-reply/commands-registry.test.ts index ef4819939..e1192c9cd 100644 --- a/src/auto-reply/commands-registry.test.ts +++ b/src/auto-reply/commands-registry.test.ts @@ -85,26 +85,6 @@ describe("commands registry", () => { expect(native.find((spec) => spec.name === "demo_skill")).toBeTruthy(); }); - it("includes plugin chat commands", () => { - const registry = createTestRegistry([]); - registry.chatCommands = [ - { - pluginId: "demo-plugin", - source: "test", - command: { - key: "demo", - description: "Demo command", - textAliases: ["/demo"], - scope: "text", - }, - handler: async () => null, - }, - ]; - setActivePluginRegistry(registry); - const commands = listChatCommands(); - expect(commands.find((spec) => spec.key === "demo")).toBeTruthy(); - }); - it("detects known text commands", () => { const detection = getCommandDetection(); expect(detection.exact.has("/commands")).toBe(true); diff --git a/src/auto-reply/reply/commands-plugin.ts b/src/auto-reply/reply/commands-plugin.ts index 0828e8c44..86a99e6bc 100644 --- a/src/auto-reply/reply/commands-plugin.ts +++ b/src/auto-reply/reply/commands-plugin.ts @@ -1,26 +1,41 @@ -import { requireActivePluginRegistry } from "../../plugins/runtime.js"; -import { parseCommandArgs, resolveTextCommand } from "../commands-registry.js"; -import type { CommandHandler } from "./commands-types.js"; +/** + * Plugin Command Handler + * + * Handles commands registered by plugins, bypassing the LLM agent. + * This handler is called before built-in command handlers. + */ -export const handlePluginCommand: CommandHandler = async (params, allowTextCommands) => { - if (!allowTextCommands) return null; - const registry = requireActivePluginRegistry(); - if (registry.chatCommands.length === 0) return null; +import { matchPluginCommand, executePluginCommand } from "../../plugins/commands.js"; +import type { CommandHandler, CommandHandlerResult } from "./commands-types.js"; - const raw = params.command.commandBodyNormalized; - if (!raw.startsWith("/")) return null; +/** + * Handle plugin-registered commands. + * Returns a result if a plugin command was matched and executed, + * or null to continue to the next handler. + */ +export const handlePluginCommand: CommandHandler = async ( + params, + _allowTextCommands, +): Promise => { + const { command, cfg } = params; - const resolved = resolveTextCommand(raw, params.cfg); - if (!resolved) return null; + // Try to match a plugin command + const match = matchPluginCommand(command.commandBodyNormalized); + if (!match) return null; - const registration = registry.chatCommands.find( - (entry) => entry.command.key === resolved.command.key, - ); - if (!registration) return null; + // Execute the plugin command (always returns a result) + const result = await executePluginCommand({ + command: match.command, + args: match.args, + senderId: command.senderId, + channel: command.channel, + isAuthorizedSender: command.isAuthorizedSender, + commandBody: command.commandBodyNormalized, + config: cfg, + }); - if (resolved.args) { - params.ctx.CommandArgs = parseCommandArgs(resolved.command, resolved.args); - } - - return await registration.handler(params, allowTextCommands); + return { + shouldContinue: false, + reply: { text: result.text }, + }; }; diff --git a/src/auto-reply/reply/dispatch-from-config.ts b/src/auto-reply/reply/dispatch-from-config.ts index 8836be5e7..5885d729e 100644 --- a/src/auto-reply/reply/dispatch-from-config.ts +++ b/src/auto-reply/reply/dispatch-from-config.ts @@ -106,11 +106,6 @@ export async function dispatchReplyFromConfig(params: { from: ctx.From ?? "", content, timestamp, - messageId: messageIdForHook, - senderId: ctx.SenderId, - senderName: ctx.SenderName, - senderUsername: ctx.SenderUsername, - senderE164: ctx.SenderE164, metadata: { to: ctx.To, provider: ctx.Provider, @@ -118,6 +113,11 @@ export async function dispatchReplyFromConfig(params: { threadId: ctx.MessageThreadId, originatingChannel: ctx.OriginatingChannel, originatingTo: ctx.OriginatingTo, + messageId: messageIdForHook, + senderId: ctx.SenderId, + senderName: ctx.SenderName, + senderUsername: ctx.SenderUsername, + senderE164: ctx.SenderE164, }, }, { diff --git a/src/gateway/server/__tests__/test-utils.ts b/src/gateway/server/__tests__/test-utils.ts index e221a31e2..697c9b73b 100644 --- a/src/gateway/server/__tests__/test-utils.ts +++ b/src/gateway/server/__tests__/test-utils.ts @@ -11,7 +11,6 @@ export const createTestRegistry = (overrides: Partial = {}): Plu gatewayHandlers: {}, httpHandlers: [], cliRegistrars: [], - chatCommands: [], services: [], commands: [], diagnostics: [], diff --git a/src/gateway/test-helpers.mocks.ts b/src/gateway/test-helpers.mocks.ts index 69e4123c1..2a402201a 100644 --- a/src/gateway/test-helpers.mocks.ts +++ b/src/gateway/test-helpers.mocks.ts @@ -139,7 +139,6 @@ const createStubPluginRegistry = (): PluginRegistry => ({ gatewayHandlers: {}, httpHandlers: [], cliRegistrars: [], - chatCommands: [], services: [], commands: [], diagnostics: [], diff --git a/src/plugin-sdk/index.ts b/src/plugin-sdk/index.ts index 6845f23f8..61dd1bc0b 100644 --- a/src/plugin-sdk/index.ts +++ b/src/plugin-sdk/index.ts @@ -62,9 +62,6 @@ export type { ClawdbotPluginApi, ClawdbotPluginService, ClawdbotPluginServiceContext, - PluginChatCommandHandler, - PluginHookMessageContext, - PluginHookMessageReceivedEvent, } from "../plugins/types.js"; export type { PluginRuntime } from "../plugins/runtime/types.js"; export { emptyPluginConfigSchema } from "../plugins/config-schema.js"; @@ -109,7 +106,6 @@ export type { WizardPrompter } from "../wizard/prompts.js"; export { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js"; export { resolveAckReaction } from "../agents/identity.js"; export type { ReplyPayload } from "../auto-reply/types.js"; -export type { ChatCommandDefinition } from "../auto-reply/commands-registry.types.js"; export { SILENT_REPLY_TOKEN, isSilentReplyText } from "../auto-reply/tokens.js"; export { buildPendingHistoryContextFromMap, diff --git a/src/plugins/loader.ts b/src/plugins/loader.ts index c55038594..931c15d59 100644 --- a/src/plugins/loader.ts +++ b/src/plugins/loader.ts @@ -147,7 +147,6 @@ function createPluginRecord(params: { providerIds: [], gatewayMethods: [], cliCommands: [], - chatCommands: [], services: [], commands: [], httpHandlers: 0, diff --git a/src/plugins/registry.ts b/src/plugins/registry.ts index 7e87888f0..048e490f3 100644 --- a/src/plugins/registry.ts +++ b/src/plugins/registry.ts @@ -1,5 +1,4 @@ import type { AnyAgentTool } from "../agents/tools/common.js"; -import type { ChatCommandDefinition } from "../auto-reply/commands-registry.types.js"; import type { ChannelDock } from "../channels/dock.js"; import type { ChannelPlugin } from "../channels/plugins/types.js"; import type { @@ -17,7 +16,6 @@ import type { ClawdbotPluginHookOptions, ProviderPlugin, ClawdbotPluginService, - PluginChatCommandHandler, ClawdbotPluginToolContext, ClawdbotPluginToolFactory, PluginConfigUiHint, @@ -49,13 +47,6 @@ export type PluginCliRegistration = { source: string; }; -export type PluginChatCommandRegistration = { - pluginId: string; - command: ChatCommandDefinition; - handler: PluginChatCommandHandler; - source: string; -}; - export type PluginHttpRegistration = { pluginId: string; handler: ClawdbotPluginHttpHandler; @@ -112,7 +103,6 @@ export type PluginRecord = { providerIds: string[]; gatewayMethods: string[]; cliCommands: string[]; - chatCommands: string[]; services: string[]; commands: string[]; httpHandlers: number; @@ -132,7 +122,6 @@ export type PluginRegistry = { gatewayHandlers: GatewayRequestHandlers; httpHandlers: PluginHttpRegistration[]; cliRegistrars: PluginCliRegistration[]; - chatCommands: PluginChatCommandRegistration[]; services: PluginServiceRegistration[]; commands: PluginCommandRegistration[]; diagnostics: PluginDiagnostic[]; @@ -155,7 +144,6 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) { gatewayHandlers: {}, httpHandlers: [], cliRegistrars: [], - chatCommands: [], services: [], commands: [], diagnostics: [], @@ -364,30 +352,6 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) { }); }; - const registerChatCommand = ( - record: PluginRecord, - command: ChatCommandDefinition, - handler: PluginChatCommandHandler, - ) => { - const key = command.key?.trim(); - if (!key) { - pushDiagnostic({ - level: "error", - pluginId: record.id, - source: record.source, - message: "chat command registration missing key", - }); - return; - } - record.chatCommands.push(key); - registry.chatCommands.push({ - pluginId: record.id, - command, - handler, - source: record.source, - }); - }; - const registerService = (record: PluginRecord, service: ClawdbotPluginService) => { const id = service.id.trim(); if (!id) return; @@ -479,7 +443,6 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) { registerProvider: (provider) => registerProvider(record, provider), registerGatewayMethod: (method, handler) => registerGatewayMethod(record, method, handler), registerCli: (registrar, opts) => registerCli(record, registrar, opts), - registerChatCommand: (command, handler) => registerChatCommand(record, command, handler), registerService: (service) => registerService(record, service), registerCommand: (command) => registerCommand(record, command), resolvePath: (input: string) => resolveUserPath(input), @@ -496,7 +459,6 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) { registerProvider, registerGatewayMethod, registerCli, - registerChatCommand, registerService, registerCommand, registerHook, diff --git a/src/plugins/runtime.ts b/src/plugins/runtime.ts index 8b21b16bd..0da06ae63 100644 --- a/src/plugins/runtime.ts +++ b/src/plugins/runtime.ts @@ -10,7 +10,6 @@ const createEmptyRegistry = (): PluginRegistry => ({ gatewayHandlers: {}, httpHandlers: [], cliRegistrars: [], - chatCommands: [], services: [], commands: [], diagnostics: [], diff --git a/src/plugins/types.ts b/src/plugins/types.ts index e8009b987..ea7a392f2 100644 --- a/src/plugins/types.ts +++ b/src/plugins/types.ts @@ -5,11 +5,6 @@ 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 { ChatCommandDefinition } from "../auto-reply/commands-registry.types.js"; -import type { - CommandHandlerResult, - HandleCommandsParams, -} from "../auto-reply/reply/commands-types.js"; import type { ChannelDock } from "../channels/dock.js"; import type { ChannelPlugin } from "../channels/plugins/types.js"; import type { ClawdbotConfig } from "../config/config.js"; @@ -201,11 +196,6 @@ export type ClawdbotPluginCliContext = { export type ClawdbotPluginCliRegistrar = (ctx: ClawdbotPluginCliContext) => void | Promise; -export type PluginChatCommandHandler = ( - params: HandleCommandsParams, - allowTextCommands: boolean, -) => Promise | CommandHandlerResult | null; - export type ClawdbotPluginServiceContext = { config: ClawdbotConfig; workspaceDir?: string; @@ -262,7 +252,6 @@ export type ClawdbotPluginApi = { registerChannel: (registration: ClawdbotPluginChannelRegistration | ChannelPlugin) => void; registerGatewayMethod: (method: string, handler: GatewayRequestHandler) => void; registerCli: (registrar: ClawdbotPluginCliRegistrar, opts?: { commands?: string[] }) => void; - registerChatCommand: (command: ChatCommandDefinition, handler: PluginChatCommandHandler) => void; registerService: (service: ClawdbotPluginService) => void; registerProvider: (provider: ProviderPlugin) => void; /** @@ -360,11 +349,6 @@ export type PluginHookMessageReceivedEvent = { from: string; content: string; timestamp?: number; - messageId?: string; - senderId?: string; - senderName?: string; - senderUsername?: string; - senderE164?: string; metadata?: Record; }; diff --git a/src/test-utils/channel-plugins.ts b/src/test-utils/channel-plugins.ts index 96bfe8f0a..6bac4cfc2 100644 --- a/src/test-utils/channel-plugins.ts +++ b/src/test-utils/channel-plugins.ts @@ -18,7 +18,6 @@ export const createTestRegistry = (channels: PluginRegistry["channels"] = []): P gatewayHandlers: {}, httpHandlers: [], cliRegistrars: [], - chatCommands: [], services: [], commands: [], diagnostics: [],