refactor: align contacts-search command handling

This commit is contained in:
Peter Steinberger 2026-01-24 08:34:07 +00:00
parent 0dc131e9f3
commit e083e49756
15 changed files with 86 additions and 192 deletions

View File

@ -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 });
},
);

View File

@ -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)}`;
}
};
}

View File

@ -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<string, unknown>;
};
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;

View File

@ -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<typeof requireActivePluginRegistry> | null = null;
let cachedRegistry: ReturnType<typeof getActivePluginRegistry> | null = null;
let cachedNativeCommandSurfaces: Set<string> | null = null;
let cachedNativeRegistry: ReturnType<typeof requireActivePluginRegistry> | null = null;
let cachedNativeRegistry: ReturnType<typeof getActivePluginRegistry> | 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<string> {
const registry = requireActivePluginRegistry();
const registry = getActivePluginRegistry();
if (cachedNativeCommandSurfaces && registry === cachedNativeRegistry) {
return cachedNativeCommandSurfaces;
}

View File

@ -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);

View File

@ -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<CommandHandlerResult | null> => {
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 },
};
};

View File

@ -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,
},
},
{

View File

@ -11,7 +11,6 @@ export const createTestRegistry = (overrides: Partial<PluginRegistry> = {}): Plu
gatewayHandlers: {},
httpHandlers: [],
cliRegistrars: [],
chatCommands: [],
services: [],
commands: [],
diagnostics: [],

View File

@ -139,7 +139,6 @@ const createStubPluginRegistry = (): PluginRegistry => ({
gatewayHandlers: {},
httpHandlers: [],
cliRegistrars: [],
chatCommands: [],
services: [],
commands: [],
diagnostics: [],

View File

@ -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,

View File

@ -147,7 +147,6 @@ function createPluginRecord(params: {
providerIds: [],
gatewayMethods: [],
cliCommands: [],
chatCommands: [],
services: [],
commands: [],
httpHandlers: 0,

View File

@ -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,

View File

@ -10,7 +10,6 @@ const createEmptyRegistry = (): PluginRegistry => ({
gatewayHandlers: {},
httpHandlers: [],
cliRegistrars: [],
chatCommands: [],
services: [],
commands: [],
diagnostics: [],

View File

@ -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<void>;
export type PluginChatCommandHandler = (
params: HandleCommandsParams,
allowTextCommands: boolean,
) => Promise<CommandHandlerResult | null> | 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<string, unknown>;
};

View File

@ -18,7 +18,6 @@ export const createTestRegistry = (channels: PluginRegistry["channels"] = []): P
gatewayHandlers: {},
httpHandlers: [],
cliRegistrars: [],
chatCommands: [],
services: [],
commands: [],
diagnostics: [],