From 1838582546d62773b0cdf8f5174b061b9f59266c Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 9 Jan 2026 17:14:33 +0100 Subject: [PATCH] refactor(auto-reply): centralize chat command aliases --- src/auto-reply/command-detection.test.ts | 16 +- src/auto-reply/commands-registry.test.ts | 26 ++- src/auto-reply/commands-registry.ts | 258 +++++++++++++---------- 3 files changed, 175 insertions(+), 125 deletions(-) diff --git a/src/auto-reply/command-detection.test.ts b/src/auto-reply/command-detection.test.ts index fe8ec2f92..b43bbed1b 100644 --- a/src/auto-reply/command-detection.test.ts +++ b/src/auto-reply/command-detection.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from "vitest"; import { hasControlCommand } from "./command-detection.js"; +import { listChatCommands } from "./commands-registry.js"; import { parseActivationCommand } from "./group-activation.js"; import { parseSendPolicyCommand } from "./send-policy.js"; @@ -37,16 +38,17 @@ describe("control command parsing", () => { }); it("treats bare commands as non-control", () => { - expect(hasControlCommand("/send")).toBe(true); expect(hasControlCommand("send")).toBe(false); - expect(hasControlCommand("/help")).toBe(true); - expect(hasControlCommand("/help:")).toBe(true); expect(hasControlCommand("help")).toBe(false); - expect(hasControlCommand("/status")).toBe(true); - expect(hasControlCommand("/status:")).toBe(true); - expect(hasControlCommand("/usage")).toBe(true); - expect(hasControlCommand("/usage:")).toBe(true); expect(hasControlCommand("status")).toBe(false); + expect(hasControlCommand("usage")).toBe(false); + + for (const command of listChatCommands()) { + for (const alias of command.textAliases) { + expect(hasControlCommand(alias)).toBe(true); + expect(hasControlCommand(`${alias}:`)).toBe(true); + } + } }); it("requires commands to be the full message", () => { diff --git a/src/auto-reply/commands-registry.test.ts b/src/auto-reply/commands-registry.test.ts index 3d0a8eae6..9952af7eb 100644 --- a/src/auto-reply/commands-registry.test.ts +++ b/src/auto-reply/commands-registry.test.ts @@ -3,6 +3,7 @@ import { describe, expect, it } from "vitest"; import { buildCommandText, getCommandDetection, + listChatCommands, listNativeCommandSpecs, shouldHandleTextCommands, } from "./commands-registry.js"; @@ -21,16 +22,21 @@ describe("commands registry", () => { it("detects known text commands", () => { const detection = getCommandDetection(); - expect(detection.exact.has("/help")).toBe(true); - expect(detection.regex.test("/status")).toBe(true); - expect(detection.regex.test("/status:")).toBe(true); - expect(detection.regex.test("/usage")).toBe(true); - expect(detection.regex.test("/usage:")).toBe(true); - expect(detection.regex.test("/stop")).toBe(true); - expect(detection.regex.test("/send:")).toBe(true); - expect(detection.regex.test("/debug set foo=bar")).toBe(true); - expect(detection.regex.test("/models")).toBe(true); - expect(detection.regex.test("/models list")).toBe(true); + for (const command of listChatCommands()) { + for (const alias of command.textAliases) { + expect(detection.exact.has(alias.toLowerCase())).toBe(true); + expect(detection.regex.test(alias)).toBe(true); + expect(detection.regex.test(`${alias}:`)).toBe(true); + + if (command.acceptsArgs) { + expect(detection.regex.test(`${alias} list`)).toBe(true); + expect(detection.regex.test(`${alias}: list`)).toBe(true); + } else { + expect(detection.regex.test(`${alias} list`)).toBe(false); + expect(detection.regex.test(`${alias}: list`)).toBe(false); + } + } + } expect(detection.regex.test("try /status")).toBe(false); }); diff --git a/src/auto-reply/commands-registry.ts b/src/auto-reply/commands-registry.ts index bbdda249f..27f03c15f 100644 --- a/src/auto-reply/commands-registry.ts +++ b/src/auto-reply/commands-registry.ts @@ -14,114 +14,156 @@ export type NativeCommandSpec = { acceptsArgs: boolean; }; -const CHAT_COMMANDS: ChatCommandDefinition[] = [ - { - key: "help", - nativeName: "help", - description: "Show available commands.", - textAliases: ["/help"], - }, - { - key: "status", - nativeName: "status", - description: "Show current status.", - textAliases: ["/status", "/usage"], - }, - { - key: "debug", - nativeName: "debug", - description: "Set runtime debug overrides.", - textAliases: ["/debug"], - acceptsArgs: true, - }, - { - key: "cost", - nativeName: "cost", - description: "Toggle per-response usage line.", - textAliases: ["/cost"], - acceptsArgs: true, - }, - { - key: "stop", - nativeName: "stop", - description: "Stop the current run.", - textAliases: ["/stop"], - }, - { - key: "restart", - nativeName: "restart", - description: "Restart Clawdbot.", - textAliases: ["/restart"], - }, - { - key: "activation", - nativeName: "activation", - description: "Set group activation mode.", - textAliases: ["/activation"], - acceptsArgs: true, - }, - { - key: "send", - nativeName: "send", - description: "Set send policy.", - textAliases: ["/send"], - acceptsArgs: true, - }, - { - key: "reset", - nativeName: "reset", - description: "Reset the current session.", - textAliases: ["/reset"], - }, - { - key: "new", - nativeName: "new", - description: "Start a new session.", - textAliases: ["/new"], - }, - { - key: "think", - nativeName: "think", - description: "Set thinking level.", - textAliases: ["/thinking", "/think", "/t"], - acceptsArgs: true, - }, - { - key: "verbose", - nativeName: "verbose", - description: "Toggle verbose mode.", - textAliases: ["/verbose", "/v"], - acceptsArgs: true, - }, - { - key: "reasoning", - nativeName: "reasoning", - description: "Toggle reasoning visibility.", - textAliases: ["/reasoning", "/reason"], - acceptsArgs: true, - }, - { - key: "elevated", - nativeName: "elevated", - description: "Toggle elevated mode.", - textAliases: ["/elevated", "/elev"], - acceptsArgs: true, - }, - { - key: "model", - nativeName: "model", - description: "Show or set the model.", - textAliases: ["/model", "/models"], - acceptsArgs: true, - }, - { - key: "queue", - nativeName: "queue", - description: "Adjust queue settings.", - textAliases: ["/queue"], - acceptsArgs: true, - }, -]; +function defineChatCommand( + command: Omit & { textAlias: string }, +): ChatCommandDefinition { + return { + key: command.key, + nativeName: command.nativeName, + description: command.description, + acceptsArgs: command.acceptsArgs, + textAliases: [command.textAlias], + }; +} + +function registerAlias( + commands: ChatCommandDefinition[], + key: string, + ...aliases: string[] +): void { + const command = commands.find((entry) => entry.key === key); + if (!command) { + throw new Error(`registerAlias: unknown command key: ${key}`); + } + const existing = new Set(command.textAliases.map((alias) => alias.trim())); + for (const alias of aliases) { + const trimmed = alias.trim(); + if (!trimmed) continue; + if (existing.has(trimmed)) continue; + existing.add(trimmed); + command.textAliases.push(trimmed); + } +} + +export const CHAT_COMMANDS: ChatCommandDefinition[] = (() => { + const commands: ChatCommandDefinition[] = [ + defineChatCommand({ + key: "help", + nativeName: "help", + description: "Show available commands.", + textAlias: "/help", + }), + defineChatCommand({ + key: "status", + nativeName: "status", + description: "Show current status.", + textAlias: "/status", + }), + defineChatCommand({ + key: "debug", + nativeName: "debug", + description: "Set runtime debug overrides.", + textAlias: "/debug", + acceptsArgs: true, + }), + defineChatCommand({ + key: "cost", + nativeName: "cost", + description: "Toggle per-response usage line.", + textAlias: "/cost", + acceptsArgs: true, + }), + defineChatCommand({ + key: "stop", + nativeName: "stop", + description: "Stop the current run.", + textAlias: "/stop", + }), + defineChatCommand({ + key: "restart", + nativeName: "restart", + description: "Restart Clawdbot.", + textAlias: "/restart", + }), + defineChatCommand({ + key: "activation", + nativeName: "activation", + description: "Set group activation mode.", + textAlias: "/activation", + acceptsArgs: true, + }), + defineChatCommand({ + key: "send", + nativeName: "send", + description: "Set send policy.", + textAlias: "/send", + acceptsArgs: true, + }), + defineChatCommand({ + key: "reset", + nativeName: "reset", + description: "Reset the current session.", + textAlias: "/reset", + }), + defineChatCommand({ + key: "new", + nativeName: "new", + description: "Start a new session.", + textAlias: "/new", + }), + defineChatCommand({ + key: "think", + nativeName: "think", + description: "Set thinking level.", + textAlias: "/think", + acceptsArgs: true, + }), + defineChatCommand({ + key: "verbose", + nativeName: "verbose", + description: "Toggle verbose mode.", + textAlias: "/verbose", + acceptsArgs: true, + }), + defineChatCommand({ + key: "reasoning", + nativeName: "reasoning", + description: "Toggle reasoning visibility.", + textAlias: "/reasoning", + acceptsArgs: true, + }), + defineChatCommand({ + key: "elevated", + nativeName: "elevated", + description: "Toggle elevated mode.", + textAlias: "/elevated", + acceptsArgs: true, + }), + defineChatCommand({ + key: "model", + nativeName: "model", + description: "Show or set the model.", + textAlias: "/model", + acceptsArgs: true, + }), + defineChatCommand({ + key: "queue", + nativeName: "queue", + description: "Adjust queue settings.", + textAlias: "/queue", + acceptsArgs: true, + }), + ]; + + registerAlias(commands, "status", "/usage"); + registerAlias(commands, "think", "/thinking", "/t"); + registerAlias(commands, "verbose", "/v"); + registerAlias(commands, "reasoning", "/reason"); + registerAlias(commands, "elevated", "/elev"); + registerAlias(commands, "model", "/models"); + + return commands; +})(); const NATIVE_COMMAND_SURFACES = new Set(["discord", "slack", "telegram"]);