From c8c58c05375d8e59a7f026eaa38a62fa130b0ca9 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 24 Jan 2026 09:58:06 +0000 Subject: [PATCH] fix: avoid Discord /tts conflict --- docs/tools/slash-commands.md | 1 + docs/tts.md | 3 +++ src/auto-reply/commands-registry.test.ts | 11 +++++++++ src/auto-reply/commands-registry.ts | 31 ++++++++++++++++++++---- src/discord/monitor/native-command.ts | 4 +-- src/discord/monitor/provider.ts | 6 +++-- src/slack/monitor/slash.ts | 6 ++--- src/telegram/bot-native-commands.ts | 4 +-- 8 files changed, 52 insertions(+), 14 deletions(-) diff --git a/docs/tools/slash-commands.md b/docs/tools/slash-commands.md index d3de2cd7b..804edc244 100644 --- a/docs/tools/slash-commands.md +++ b/docs/tools/slash-commands.md @@ -68,6 +68,7 @@ Text + native (when enabled): - `/debug show|set|unset|reset` (runtime overrides, owner-only; requires `commands.debug: true`) - `/usage off|tokens|full|cost` (per-response usage footer or local cost summary) - `/tts on|off|status|provider|limit|summary|audio` (control TTS; see [/tts](/tts)) + - Discord: native command is `/voice` (Discord reserves `/tts`); text `/tts` still works. - `/stop` - `/restart` - `/dock-telegram` (alias: `/dock_telegram`) (switch replies to Telegram) diff --git a/docs/tts.md b/docs/tts.md index 0a7fef7ff..a9aa141a5 100644 --- a/docs/tts.md +++ b/docs/tts.md @@ -260,6 +260,9 @@ Reply -> TTS enabled? There is a single command: `/tts`. See [Slash commands](/tools/slash-commands) for enablement details. +Discord note: `/tts` is a built-in Discord command, so Clawdbot registers +`/voice` as the native command there. Text `/tts ...` still works. + ``` /tts on /tts off diff --git a/src/auto-reply/commands-registry.test.ts b/src/auto-reply/commands-registry.test.ts index e1192c9cd..6a6efbced 100644 --- a/src/auto-reply/commands-registry.test.ts +++ b/src/auto-reply/commands-registry.test.ts @@ -3,6 +3,7 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { buildCommandText, buildCommandTextFromArgs, + findCommandByNativeName, getCommandDetection, listChatCommands, listChatCommandsForConfig, @@ -85,6 +86,16 @@ describe("commands registry", () => { expect(native.find((spec) => spec.name === "demo_skill")).toBeTruthy(); }); + it("applies provider-specific native names", () => { + const native = listNativeCommandSpecsForConfig( + { commands: { native: true } }, + { provider: "discord" }, + ); + expect(native.find((spec) => spec.name === "voice")).toBeTruthy(); + expect(findCommandByNativeName("voice", "discord")?.key).toBe("tts"); + expect(findCommandByNativeName("tts", "discord")).toBeUndefined(); + }); + it("detects known text commands", () => { const detection = getCommandDetection(); expect(detection.exact.has("/commands")).toBe(true); diff --git a/src/auto-reply/commands-registry.ts b/src/auto-reply/commands-registry.ts index 983f2ea9c..5bca565f0 100644 --- a/src/auto-reply/commands-registry.ts +++ b/src/auto-reply/commands-registry.ts @@ -105,13 +105,29 @@ export function listChatCommandsForConfig( return [...base, ...buildSkillCommandDefinitions(params.skillCommands)]; } +const NATIVE_NAME_OVERRIDES: Record> = { + discord: { + tts: "voice", + }, +}; + +function resolveNativeName(command: ChatCommandDefinition, provider?: string): string | undefined { + if (!command.nativeName) return undefined; + if (provider) { + const override = NATIVE_NAME_OVERRIDES[provider]?.[command.key]; + if (override) return override; + } + return command.nativeName; +} + export function listNativeCommandSpecs(params?: { skillCommands?: SkillCommandSpec[]; + provider?: string; }): NativeCommandSpec[] { return listChatCommands({ skillCommands: params?.skillCommands }) .filter((command) => command.scope !== "text" && command.nativeName) .map((command) => ({ - name: command.nativeName ?? command.key, + name: resolveNativeName(command, params?.provider) ?? command.key, description: command.description, acceptsArgs: Boolean(command.acceptsArgs), args: command.args, @@ -120,22 +136,27 @@ export function listNativeCommandSpecs(params?: { export function listNativeCommandSpecsForConfig( cfg: ClawdbotConfig, - params?: { skillCommands?: SkillCommandSpec[] }, + params?: { skillCommands?: SkillCommandSpec[]; provider?: string }, ): NativeCommandSpec[] { return listChatCommandsForConfig(cfg, params) .filter((command) => command.scope !== "text" && command.nativeName) .map((command) => ({ - name: command.nativeName ?? command.key, + name: resolveNativeName(command, params?.provider) ?? command.key, description: command.description, acceptsArgs: Boolean(command.acceptsArgs), args: command.args, })); } -export function findCommandByNativeName(name: string): ChatCommandDefinition | undefined { +export function findCommandByNativeName( + name: string, + provider?: string, +): ChatCommandDefinition | undefined { const normalized = name.trim().toLowerCase(); return getChatCommands().find( - (command) => command.scope !== "text" && command.nativeName?.toLowerCase() === normalized, + (command) => + command.scope !== "text" && + resolveNativeName(command, provider)?.toLowerCase() === normalized, ); } diff --git a/src/discord/monitor/native-command.ts b/src/discord/monitor/native-command.ts index 8f63a0817..94ff20a2e 100644 --- a/src/discord/monitor/native-command.ts +++ b/src/discord/monitor/native-command.ts @@ -254,7 +254,7 @@ async function handleDiscordCommandArgInteraction( return; } const commandDefinition = - findCommandByNativeName(parsed.command) ?? + findCommandByNativeName(parsed.command, "discord") ?? listChatCommands().find((entry) => entry.key === parsed.command); if (!commandDefinition) { await safeDiscordInteractionCall("command arg update", () => @@ -395,7 +395,7 @@ export function createDiscordNativeCommand(params: { }) { const { command, cfg, discordConfig, accountId, sessionPrefix, ephemeralDefault } = params; const commandDefinition = - findCommandByNativeName(command.name) ?? + findCommandByNativeName(command.name, "discord") ?? ({ key: command.name, nativeName: command.name, diff --git a/src/discord/monitor/provider.ts b/src/discord/monitor/provider.ts index 899622236..02e91ff0b 100644 --- a/src/discord/monitor/provider.ts +++ b/src/discord/monitor/provider.ts @@ -346,11 +346,13 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) { const maxDiscordCommands = 100; let skillCommands = nativeEnabled && nativeSkillsEnabled ? listSkillCommandsForAgents({ cfg }) : []; - let commandSpecs = nativeEnabled ? listNativeCommandSpecsForConfig(cfg, { skillCommands }) : []; + let commandSpecs = nativeEnabled + ? listNativeCommandSpecsForConfig(cfg, { skillCommands, provider: "discord" }) + : []; const initialCommandCount = commandSpecs.length; if (nativeEnabled && nativeSkillsEnabled && commandSpecs.length > maxDiscordCommands) { skillCommands = []; - commandSpecs = listNativeCommandSpecsForConfig(cfg, { skillCommands: [] }); + commandSpecs = listNativeCommandSpecsForConfig(cfg, { skillCommands: [], provider: "discord" }); runtime.log?.( warn( `discord: ${initialCommandCount} commands exceeds limit; removing per-skill commands and keeping /skill.`, diff --git a/src/slack/monitor/slash.ts b/src/slack/monitor/slash.ts index decd55e59..e38f00b4f 100644 --- a/src/slack/monitor/slash.ts +++ b/src/slack/monitor/slash.ts @@ -476,14 +476,14 @@ export function registerSlackMonitorSlashCommands(params: { const skillCommands = nativeEnabled && nativeSkillsEnabled ? listSkillCommandsForAgents({ cfg }) : []; const nativeCommands = nativeEnabled - ? listNativeCommandSpecsForConfig(cfg, { skillCommands }) + ? listNativeCommandSpecsForConfig(cfg, { skillCommands, provider: "slack" }) : []; if (nativeCommands.length > 0) { for (const command of nativeCommands) { ctx.app.command( `/${command.name}`, async ({ command: cmd, ack, respond }: SlackCommandMiddlewareArgs) => { - const commandDefinition = findCommandByNativeName(command.name); + const commandDefinition = findCommandByNativeName(command.name, "slack"); const rawText = cmd.text?.trim() ?? ""; const commandArgs = commandDefinition ? parseCommandArgs(commandDefinition, rawText) @@ -557,7 +557,7 @@ export function registerSlackMonitorSlashCommands(params: { }); return; } - const commandDefinition = findCommandByNativeName(parsed.command); + const commandDefinition = findCommandByNativeName(parsed.command, "slack"); const commandArgs: CommandArgs = { values: { [parsed.arg]: parsed.value }, }; diff --git a/src/telegram/bot-native-commands.ts b/src/telegram/bot-native-commands.ts index c3d3a7b74..9f5dd5782 100644 --- a/src/telegram/bot-native-commands.ts +++ b/src/telegram/bot-native-commands.ts @@ -85,7 +85,7 @@ export const registerTelegramNativeCommands = ({ const skillCommands = nativeEnabled && nativeSkillsEnabled ? listSkillCommandsForAgents({ cfg }) : []; const nativeCommands = nativeEnabled - ? listNativeCommandSpecsForConfig(cfg, { skillCommands }) + ? listNativeCommandSpecsForConfig(cfg, { skillCommands, provider: "telegram" }) : []; const reservedCommands = new Set( listNativeCommandSpecs().map((command) => command.name.toLowerCase()), @@ -216,7 +216,7 @@ export const registerTelegramNativeCommands = ({ return; } - const commandDefinition = findCommandByNativeName(command.name); + const commandDefinition = findCommandByNativeName(command.name, "telegram"); const rawText = ctx.match?.trim() ?? ""; const commandArgs = commandDefinition ? parseCommandArgs(commandDefinition, rawText)