From a2cfde2b25b24e44f17fd3ccb32d57518b98fa33 Mon Sep 17 00:00:00 2001 From: wassname <1103714+wassname@users.noreply.github.com> Date: Fri, 1 May 2026 18:58:32 +0800 Subject: [PATCH] Translate dashes to underscores for Telegram bot commands Pi command names like compact-now were silently dropped from setMyCommands because Telegram requires names to match ^[a-z0-9_]{1,32}$ - no dashes. They showed up in /help (which iterates pi.getCommands() directly) but were untypable via autocomplete and, even when typed manually, fell through to the agent as plain text rather than dispatching. Now: register the underscore-translated form with Telegram, show the underscore form in /help so it matches what the user sees in the menu, and on dispatch rewrite the message text from /foo_bar back to /foo-bar before enqueueing so the harness's slash-command parser finds the registered command. Co-Authored-By: Claude Opus 4.7 --- index.ts | 25 ++++++++++++++++++++++++- lib/registration.ts | 20 +++++++++++++++++++- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/index.ts b/index.ts index 289eb16..94a1de2 100644 --- a/index.ts +++ b/index.ts @@ -76,9 +76,11 @@ import { } from "./lib/queue.ts"; import { buildTelegramBotCommands, + fromTelegramCommandName, registerTelegramAttachmentTool, registerTelegramCommands, registerTelegramLifecycleHooks, + toTelegramCommandName, } from "./lib/registration.ts"; import { MAX_MESSAGE_LENGTH, @@ -1892,7 +1894,9 @@ export default function (pi: ExtensionAPI) { ); for (const c of sorted) { const desc = c.description ? ` — ${c.description}` : ""; - lines.push(`/${c.name}${desc}`); + // Show the Telegram-form (dashes -> underscores) since that's what + // the user has to type. The bridge translates back on dispatch. + lines.push(`/${toTelegramCommandName(c.name)}${desc}`); } } lines.push("", "/help re-syncs Telegram's command menu."); @@ -1973,6 +1977,25 @@ export default function (pi: ExtensionAPI) { ); if (handled) return; + // Pi commands with dashes are exposed in Telegram with underscores + // (Telegram bot command names disallow dashes). If the user typed the + // underscore form, rewrite the message text to the original dashed form + // so the harness's slash-command parser dispatches it correctly. + if (command) { + const piCommandNames = new Set(pi.getCommands().map((c) => c.name)); + const original = fromTelegramCommandName(command.name, piCommandNames); + if (original !== command.name) { + const rewritten = command.args + ? `/${original} ${command.args}` + : `/${original}`; + if (firstMessage.text !== undefined) { + firstMessage.text = rewritten; + } else if (firstMessage.caption !== undefined) { + firstMessage.caption = rewritten; + } + } + } + await enqueueTelegramTurn(messages, ctx); } diff --git a/lib/registration.ts b/lib/registration.ts index cc51f95..9877422 100644 --- a/lib/registration.ts +++ b/lib/registration.ts @@ -73,8 +73,26 @@ export interface TelegramBotCommand { export const TELEGRAM_BOT_COMMAND_LIMIT = 100; +// Telegram requires command names to match ^[a-z0-9_]{1,32}$ (no dashes). +// Pi commands frequently use dashes (e.g. "compact-now"), so we transform +// dash -> underscore for the Telegram-facing name and reverse it on the way +// back so the harness still receives its original name. const telegramCommandNamePattern = /^[a-z0-9_]{1,32}$/; +export function toTelegramCommandName(name: string): string { + return name.replace(/-/g, "_"); +} + +export function fromTelegramCommandName( + telegramName: string, + piCommandNames: ReadonlySet, +): string { + if (piCommandNames.has(telegramName)) return telegramName; + const dashed = telegramName.replace(/_/g, "-"); + if (piCommandNames.has(dashed)) return dashed; + return telegramName; +} + const bridgeLocalBotCommands: TelegramBotCommand[] = [ { command: "start", description: "Show help and refresh this menu" }, { command: "help", description: "Show help" }, @@ -104,7 +122,7 @@ export function buildTelegramBotCommands( } for (const command of piCommands) { addCommand({ - command: command.name, + command: toTelegramCommandName(command.name), description: command.description ?? command.name, }); }