/** * Telegram command routing helpers * Owns Telegram slash-command normalization, bot command metadata, and pi-side command registration behind runtime ports */ import { pairTelegramUserIfNeeded, type TelegramTopicBinding } from "./config.ts"; import type { ExtensionAPI, ExtensionCommandContext } from "./pi.ts"; import { createTelegramControlItemBuilder, createTelegramControlQueueController, type PendingTelegramControlItem, } from "./queue.ts"; export interface ParsedTelegramCommand { name: string; args: string; } export interface TelegramBotCommandDefinition { command: string; description: string; } export const TELEGRAM_BOT_COMMANDS: readonly TelegramBotCommandDefinition[] = [ { command: "start", description: "Show help and pair the Telegram bridge", }, { command: "status", description: "Show model, usage, cost, and context status", }, { command: "model", description: "Open the interactive model selector" }, { command: "compact", description: "Compact the current pi session" }, { command: "stop", description: "Abort the current pi task and clear queued turns", }, ]; export interface TelegramBotCommandRegistrationDeps { setMyCommands: ( commands: readonly TelegramBotCommandDefinition[], ) => Promise; } export async function registerTelegramBotCommands( deps: TelegramBotCommandRegistrationDeps, ): Promise { await deps.setMyCommands(TELEGRAM_BOT_COMMANDS); } export function createTelegramBotCommandRegistrar( deps: TelegramBotCommandRegistrationDeps, ): () => Promise { return () => registerTelegramBotCommands(deps); } export interface TelegramBridgeCommandStartPollingOptions { force?: boolean; } export interface TelegramBridgeCommandStartPollingResult { ok: boolean; message?: string; canTakeover?: boolean; owner?: string; } export interface TelegramBridgeCommandRegistrationDeps { promptForConfig: (ctx: ExtensionCommandContext) => Promise; bindCurrentTopic: (binding: TelegramTopicBinding) => Promise; getStatusLines: () => string[]; reloadConfig: () => Promise; hasBotToken: () => boolean; startPolling: ( ctx: ExtensionCommandContext, options?: TelegramBridgeCommandStartPollingOptions, ) => | void | Promise | TelegramBridgeCommandStartPollingResult; stopPolling: () => Promise; updateStatus: (ctx: ExtensionCommandContext) => void; generateBindCode: (ctx: ExtensionCommandContext) => Promise; unbindCurrentTopic: (ctx: ExtensionCommandContext) => Promise; getCurrentTopicStatus: (ctx: ExtensionCommandContext) => Promise; setBroadcastEnabled: (ctx: ExtensionCommandContext, enabled: boolean) => Promise; getBroadcastStatus: (ctx: ExtensionCommandContext) => Promise; } function formatTelegramTakeoverTitle(ctx: ExtensionCommandContext): string { return ctx.ui.theme.fg("accent", "pi-telegram"); } function formatTelegramTakeoverPrompt( ctx: ExtensionCommandContext, owner?: string, ): string { const theme = ctx.ui.theme; const action = theme.fg("warning", "move singleton lock here?"); const from = theme.fg("muted", "from:"); const to = theme.fg("muted", "to:"); const source = owner ?? "another pi instance"; return `${action}\n\n${from} ${source}\n${to} ${ctx.cwd}`; } export function registerTelegramBridgeCommands( pi: ExtensionAPI, deps: TelegramBridgeCommandRegistrationDeps, ): void { pi.registerCommand("tg-setup", { description: "Configure Telegram bot token", handler: async (_args, ctx) => { await deps.promptForConfig(ctx); }, }); pi.registerCommand("tg-status", { description: "Show Telegram bridge status", handler: async (_args, ctx) => { ctx.ui.notify(deps.getStatusLines().join("\n"), "info"); }, }); pi.registerCommand("tg-connect-topic", { description: "Bind this pi session to a Telegram chat/topic", handler: async (args, ctx) => { const [chatIdText, threadIdText] = args.trim().split(/\s+/); const chatId = Number(chatIdText); const threadId = threadIdText === undefined || threadIdText === "" ? undefined : Number(threadIdText); if (!Number.isFinite(chatId) || (threadIdText !== undefined && threadIdText !== "" && !Number.isFinite(threadId))) { ctx.ui.notify("Usage: /tg-connect-topic [threadId]", "error"); return; } await deps.bindCurrentTopic({ chatId, threadId, updatedAt: Date.now() }); ctx.ui.notify(`Telegram topic bound: chat ${chatId}${threadId !== undefined ? ` thread ${threadId}` : ""}`, "info"); deps.updateStatus(ctx); }, }); pi.registerCommand("tg-bind-code", { description: "Generate a Telegram /bind code for this pi session", handler: async (_args, ctx) => { const code = await deps.generateBindCode(ctx); ctx.ui.notify(`Send this in the target Telegram topic: /bind ${code}`, "info"); deps.updateStatus(ctx); }, }); pi.registerCommand("tg-topic-unbind", { description: "Remove this pi session's Telegram topic binding", handler: async (_args, ctx) => { ctx.ui.notify(await deps.unbindCurrentTopic(ctx), "info"); deps.updateStatus(ctx); }, }); pi.registerCommand("tg-topic-status", { description: "Show this pi session's Telegram topic binding", handler: async (_args, ctx) => { ctx.ui.notify(await deps.getCurrentTopicStatus(ctx), "info"); deps.updateStatus(ctx); }, }); pi.registerCommand("tg-broadcast-on", { description: "Mirror local pi prompts and final replies to the bound Telegram topic", handler: async (_args, ctx) => { ctx.ui.notify(await deps.setBroadcastEnabled(ctx, true), "info"); deps.updateStatus(ctx); }, }); pi.registerCommand("tg-broadcast-off", { description: "Stop mirroring local pi prompts and final replies to Telegram", handler: async (_args, ctx) => { ctx.ui.notify(await deps.setBroadcastEnabled(ctx, false), "info"); deps.updateStatus(ctx); }, }); pi.registerCommand("tg-broadcast-status", { description: "Show Telegram local-chat mirroring status", handler: async (_args, ctx) => { ctx.ui.notify(await deps.getBroadcastStatus(ctx), "info"); deps.updateStatus(ctx); }, }); pi.registerCommand("tg-connect", { description: "Start the Telegram bridge in this pi session", handler: async (_args, ctx) => { await deps.reloadConfig(); if (!deps.hasBotToken()) { await deps.promptForConfig(ctx); return; } let result = await deps.startPolling(ctx); if (result && !result.ok && result.canTakeover) { const confirmed = await ctx.ui.confirm( formatTelegramTakeoverTitle(ctx), formatTelegramTakeoverPrompt(ctx, result.owner), ); if (!confirmed) { ctx.ui.notify("Telegram bridge takeover cancelled.", "info"); deps.updateStatus(ctx); return; } result = await deps.startPolling(ctx, { force: true }); } if (result?.message) { ctx.ui.notify(result.message, result.ok ? "info" : "warning"); } deps.updateStatus(ctx); }, }); pi.registerCommand("tg-disconnect", { description: "Stop the Telegram bridge in this pi session", handler: async (_args, ctx) => { const message = await deps.stopPolling(); if (message) ctx.ui.notify(message, "info"); deps.updateStatus(ctx); }, }); } export type TelegramCommandAction = | { kind: "ignore"; executionMode: "ignored" } | { kind: "stop"; executionMode: "immediate" } | { kind: "compact"; executionMode: "immediate" } | { kind: "status"; executionMode: "immediate" } | { kind: "model"; executionMode: "immediate" } | { kind: "bind"; executionMode: "immediate" } | { kind: "help"; commandName: "help" | "start"; executionMode: "immediate"; }; export type TelegramCommandExecutionMode = "ignored" | "immediate"; export interface TelegramCommandActionDeps { handleStop: (message: TMessage, ctx: TContext) => Promise; handleCompact: (message: TMessage, ctx: TContext) => Promise; handleStatus: (message: TMessage, ctx: TContext) => Promise; handleModel: (message: TMessage, ctx: TContext) => Promise; handleBind?: (message: TMessage, ctx: TContext) => Promise; handleHelp: ( message: TMessage, commandName: "help" | "start", ctx: TContext, ) => Promise; } export interface TelegramStopCommandDeps { hasAbortHandler: () => boolean; clearPendingModelSwitch: () => void; clearQueuedTelegramItems: () => number; setPreserveQueuedTurnsAsHistory: (preserve: boolean) => void; abortCurrentTurn: () => void; updateStatus: () => void; sendTextReply: (text: string) => Promise; } export interface TelegramRuntimeEventRecorderPort { recordRuntimeEvent?: ( category: string, error: unknown, details?: Record, ) => void; } export interface TelegramCompactCommandDeps extends TelegramRuntimeEventRecorderPort { isIdle: () => boolean; hasPendingMessages: () => boolean; hasActiveTelegramTurn: () => boolean; hasDispatchPending: () => boolean; hasQueuedTelegramItems: () => boolean; isCompactionInProgress: () => boolean; setCompactionInProgress: (inProgress: boolean) => void; updateStatus: () => void; dispatchNextQueuedTelegramTurn: () => void; compact: (callbacks: { onComplete: () => void; onError: (error: unknown) => void; }) => void; sendTextReply: (text: string) => Promise; } export interface TelegramHelpCommandDeps { senderUserId?: number; getAllowedUserId: () => number | undefined; setAllowedUserId: (userId: number) => void; registerBotCommands: () => Promise; persistConfig: () => Promise; updateStatus: () => void; sendTextReply: (text: string) => Promise; } export type TelegramControlCommandType = PendingTelegramControlItem["controlType"]; export interface TelegramCommandRuntimeMessage { chat: { id: number; title?: string }; message_id: number; message_thread_id?: number; from?: { id?: number }; } export interface TelegramCommandMessageTarget { chatId: number; threadId?: number; replyToMessageId: number; } export interface TelegramCommandTargetRuntimeDeps { enqueueControlItem: ( target: TelegramCommandMessageTarget, ctx: TContext, controlType: TelegramControlCommandType, statusSummary: string, execute: (ctx: TContext) => Promise, ) => void; showStatus: ( chatId: number, replyToMessageId: number, ctx: TContext, options?: { threadId?: number }, ) => Promise; openModelMenu: ( chatId: number, replyToMessageId: number, ctx: TContext, options?: { threadId?: number }, ) => Promise; sendTextReply: ( chatId: number, replyToMessageId: number, text: string, options?: { threadId?: number }, ) => Promise; } export interface TelegramCommandTargetRuntime< TMessage extends TelegramCommandRuntimeMessage, TContext, > { enqueueControlItem: ( message: TMessage, ctx: TContext, controlType: TelegramControlCommandType, statusSummary: string, execute: (ctx: TContext) => Promise, ) => void; showStatus: (message: TMessage, ctx: TContext) => Promise; openModelMenu: (message: TMessage, ctx: TContext) => Promise; sendTextReply: (message: TMessage, text: string) => Promise; } export function getTelegramCommandMessageTarget( message: TelegramCommandRuntimeMessage, ): TelegramCommandMessageTarget { return { chatId: message.chat.id, threadId: message.message_thread_id, replyToMessageId: message.message_id, }; } export interface TelegramCommandControlQueueRuntimeDeps { createControlItem: (options: { chatId: number; threadId?: number; replyToMessageId: number; controlType: TelegramControlCommandType; statusSummary: string; execute: (ctx: TContext) => Promise; }) => PendingTelegramControlItem; appendControlItem: ( item: PendingTelegramControlItem, ctx: TContext, ) => void; dispatchNextQueuedTelegramTurn: (ctx: TContext) => void; } export function createTelegramCommandControlQueueRuntime( deps: TelegramCommandControlQueueRuntimeDeps, ): TelegramCommandTargetRuntimeDeps["enqueueControlItem"] { const controlQueueController = createTelegramControlQueueController({ appendControlItem: deps.appendControlItem, dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn, }); return createTelegramCommandControlEnqueueAdapter({ createControlItem: deps.createControlItem, enqueueControlItem: controlQueueController.enqueue, }); } export function createTelegramCommandControlEnqueueAdapter(deps: { createControlItem: (options: { chatId: number; threadId?: number; replyToMessageId: number; controlType: TelegramControlCommandType; statusSummary: string; execute: (ctx: TContext) => Promise; }) => PendingTelegramControlItem; enqueueControlItem: ( item: PendingTelegramControlItem, ctx: TContext, ) => void; }): TelegramCommandTargetRuntimeDeps["enqueueControlItem"] { return (target, ctx, controlType, statusSummary, execute) => { deps.enqueueControlItem( deps.createControlItem({ ...target, controlType, statusSummary, execute, }), ctx, ); }; } export type TelegramCommandTargetQueueRuntimeDeps = TelegramCommandControlQueueRuntimeDeps & Omit, "enqueueControlItem">; export function createTelegramCommandTargetQueueRuntime< TMessage extends TelegramCommandRuntimeMessage, TContext, >( deps: TelegramCommandTargetQueueRuntimeDeps, ): TelegramCommandTargetRuntime { return createTelegramCommandTargetRuntime({ enqueueControlItem: createTelegramCommandControlQueueRuntime({ createControlItem: deps.createControlItem, appendControlItem: deps.appendControlItem, dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn, }), showStatus: deps.showStatus, openModelMenu: deps.openModelMenu, sendTextReply: deps.sendTextReply, }); } export function createTelegramCommandTargetRuntime< TMessage extends TelegramCommandRuntimeMessage, TContext, >( deps: TelegramCommandTargetRuntimeDeps, ): TelegramCommandTargetRuntime { return { enqueueControlItem: (message, ctx, controlType, statusSummary, execute) => { deps.enqueueControlItem( getTelegramCommandMessageTarget(message), ctx, controlType, statusSummary, execute, ); }, showStatus: (message, ctx) => { const target = getTelegramCommandMessageTarget(message); return deps.showStatus(target.chatId, target.replyToMessageId, ctx, { threadId: target.threadId, }); }, openModelMenu: (message, ctx) => { const target = getTelegramCommandMessageTarget(message); return deps.openModelMenu(target.chatId, target.replyToMessageId, ctx, { threadId: target.threadId, }); }, sendTextReply: async (message, text) => { const target = getTelegramCommandMessageTarget(message); await deps.sendTextReply(target.chatId, target.replyToMessageId, text, { threadId: target.threadId, }); }, }; } export interface TelegramCommandOrPromptRuntimeDeps { extractRawText: (messages: TMessage[]) => string; handleCommand: ( commandName: string | undefined, message: TMessage, ctx: TContext, ) => Promise; enqueueTurn: (messages: TMessage[], ctx: TContext) => Promise; } export interface TelegramCommandRuntimeDeps< TMessage extends TelegramCommandRuntimeMessage, TContext, > extends TelegramRuntimeEventRecorderPort { hasAbortHandler: () => boolean; clearPendingModelSwitch: () => void; hasQueuedTelegramItems: () => boolean; clearQueuedTelegramItems: (ctx: TContext) => number; setPreserveQueuedTurnsAsHistory: (preserve: boolean) => void; abortCurrentTurn: () => void; isIdle: (ctx: TContext) => boolean; hasPendingMessages: (ctx: TContext) => boolean; hasActiveTelegramTurn: () => boolean; hasDispatchPending: () => boolean; isCompactionInProgress: () => boolean; setCompactionInProgress: (inProgress: boolean) => void; updateStatus: (ctx: TContext) => void; dispatchNextQueuedTelegramTurn: (ctx: TContext) => void; compact: ( ctx: TContext, callbacks: { onComplete: () => void; onError: (error: unknown) => void }, ) => void; enqueueControlItem: ( message: TMessage, ctx: TContext, controlType: TelegramControlCommandType, statusSummary: string, execute: (ctx: TContext) => Promise, ) => void; showStatus: (message: TMessage, ctx: TContext) => Promise; openModelMenu: (message: TMessage, ctx: TContext) => Promise; getAllowedUserId: () => number | undefined; setAllowedUserId: (userId: number) => void; registerBotCommands: () => Promise; persistConfig: () => Promise; bindCurrentTopic?: (binding: TelegramTopicBinding) => Promise; sendTextReply: (message: TMessage, text: string) => Promise; } export const TELEGRAM_HELP_TEXT = "Send me a message and I will forward it to pi. Commands: /status, /model, /compact, /stop, /bind . In groups with topics, generate a code in pi with /tg-bind-code and send /bind in the target topic."; function getTelegramCommandErrorMessage(error: unknown): string { return error instanceof Error ? error.message : String(error); } export function parseTelegramCommand( text: string, ): ParsedTelegramCommand | undefined { const trimmed = text.trim(); if (!trimmed.startsWith("/")) return undefined; const [head, ...tail] = trimmed.split(/\s+/); const name = head.slice(1).split("@")[0]?.toLowerCase(); if (!name) return undefined; return { name, args: tail.join(" ").trim() }; } export function buildTelegramCommandAction( commandName: string | undefined, ): TelegramCommandAction { switch (commandName) { case "stop": return { kind: "stop", executionMode: "immediate" }; case "compact": return { kind: "compact", executionMode: "immediate" }; case "status": return { kind: "status", executionMode: "immediate" }; case "model": return { kind: "model", executionMode: "immediate" }; case "bind": return { kind: "bind", executionMode: "immediate" }; case "help": case "start": return { kind: "help", commandName, executionMode: "immediate" }; default: return { kind: "ignore", executionMode: "ignored" }; } } export function getTelegramCommandExecutionMode( action: TelegramCommandAction, ): TelegramCommandExecutionMode { return action.executionMode; } function formatTelegramQueuedTurnCount(count: number): string { return count === 1 ? "1 queued turn" : `${count} queued turns`; } export async function handleTelegramStopCommand( deps: TelegramStopCommandDeps, ): Promise { deps.clearPendingModelSwitch(); const clearedCount = deps.clearQueuedTelegramItems(); deps.setPreserveQueuedTurnsAsHistory(false); if (!deps.hasAbortHandler()) { const clearedSuffix = clearedCount > 0 ? ` Cleared ${formatTelegramQueuedTurnCount(clearedCount)}.` : ""; if (clearedCount > 0) deps.updateStatus(); await deps.sendTextReply(`No active turn.${clearedSuffix}`); return; } deps.abortCurrentTurn(); deps.updateStatus(); const clearedSuffix = clearedCount > 0 ? ` Cleared ${formatTelegramQueuedTurnCount(clearedCount)}.` : ""; await deps.sendTextReply(`Aborted current turn.${clearedSuffix}`); } export async function handleTelegramCompactCommand( deps: TelegramCompactCommandDeps, ): Promise { if ( !deps.isIdle() || deps.hasPendingMessages() || deps.hasActiveTelegramTurn() || deps.hasDispatchPending() || deps.hasQueuedTelegramItems() || deps.isCompactionInProgress() ) { await deps.sendTextReply( "Cannot compact while pi or the Telegram queue is busy. Wait for queued turns to finish or send /stop first.", ); return; } deps.setCompactionInProgress(true); deps.updateStatus(); try { deps.compact({ onComplete: () => { deps.setCompactionInProgress(false); deps.updateStatus(); deps.dispatchNextQueuedTelegramTurn(); void deps.sendTextReply("Compaction completed."); }, onError: (error) => { deps.setCompactionInProgress(false); deps.updateStatus(); deps.dispatchNextQueuedTelegramTurn(); deps.recordRuntimeEvent?.("compact", error); const errorMessage = getTelegramCommandErrorMessage(error); void deps.sendTextReply(`Compaction failed: ${errorMessage}`); }, }); } catch (error) { deps.setCompactionInProgress(false); deps.updateStatus(); deps.recordRuntimeEvent?.("compact", error); const errorMessage = getTelegramCommandErrorMessage(error); await deps.sendTextReply(`Compaction failed: ${errorMessage}`); return; } await deps.sendTextReply("Compaction started."); } export async function handleTelegramHelpCommand( commandName: "help" | "start", deps: TelegramHelpCommandDeps, ): Promise { let helpText = TELEGRAM_HELP_TEXT; if (commandName === "start") { try { await deps.registerBotCommands(); } catch (error) { const errorMessage = getTelegramCommandErrorMessage(error); helpText += `\n\nWarning: failed to register bot commands menu: ${errorMessage}`; } } await deps.sendTextReply(helpText); if (deps.senderUserId === undefined) return; await pairTelegramUserIfNeeded(deps.senderUserId, { allowedUserId: deps.getAllowedUserId(), ctx: undefined, setAllowedUserId: deps.setAllowedUserId, persistConfig: deps.persistConfig, updateStatus: deps.updateStatus, }); } export async function handleTelegramStatusCommand(deps: { ctx: TContext; showStatus: (ctx: TContext) => Promise; }): Promise { await deps.showStatus(deps.ctx); } export async function handleTelegramModelCommand(deps: { ctx: TContext; openModelMenu: (ctx: TContext) => Promise; }): Promise { await deps.openModelMenu(deps.ctx); } export async function executeTelegramCommandAction( action: TelegramCommandAction, message: TMessage, ctx: TContext, deps: TelegramCommandActionDeps, ): Promise { switch (action.kind) { case "ignore": return false; case "stop": await deps.handleStop(message, ctx); return true; case "compact": await deps.handleCompact(message, ctx); return true; case "status": await deps.handleStatus(message, ctx); return true; case "model": await deps.handleModel(message, ctx); return true; case "bind": await deps.handleBind?.(message, ctx); return true; case "help": await deps.handleHelp(message, action.commandName, ctx); return true; } } export interface TelegramCommandHandlerTargetRuntimeDeps< TMessage extends TelegramCommandRuntimeMessage, TContext, > extends Omit< TelegramCommandRuntimeDeps, | "enqueueControlItem" | "showStatus" | "openModelMenu" | "sendTextReply" | "registerBotCommands" >, Omit, "createControlItem">, TelegramBotCommandRegistrationDeps { allocateItemOrder: () => number; allocateControlOrder: () => number; } export function createTelegramCommandHandlerTargetRuntime< TMessage extends TelegramCommandRuntimeMessage, TContext, >( deps: TelegramCommandHandlerTargetRuntimeDeps, ): ( commandName: string | undefined, message: TMessage, ctx: TContext, ) => Promise { const commandTargetRuntime = createTelegramCommandTargetQueueRuntime< TMessage, TContext >({ createControlItem: createTelegramControlItemBuilder({ allocateItemOrder: deps.allocateItemOrder, allocateControlOrder: deps.allocateControlOrder, }), appendControlItem: deps.appendControlItem, dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn, showStatus: deps.showStatus, openModelMenu: deps.openModelMenu, sendTextReply: deps.sendTextReply, }); return createTelegramCommandHandler({ hasAbortHandler: deps.hasAbortHandler, clearPendingModelSwitch: deps.clearPendingModelSwitch, hasQueuedTelegramItems: deps.hasQueuedTelegramItems, clearQueuedTelegramItems: deps.clearQueuedTelegramItems, setPreserveQueuedTurnsAsHistory: deps.setPreserveQueuedTurnsAsHistory, abortCurrentTurn: deps.abortCurrentTurn, isIdle: deps.isIdle, hasPendingMessages: deps.hasPendingMessages, hasActiveTelegramTurn: deps.hasActiveTelegramTurn, hasDispatchPending: deps.hasDispatchPending, isCompactionInProgress: deps.isCompactionInProgress, setCompactionInProgress: deps.setCompactionInProgress, updateStatus: deps.updateStatus, dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn, compact: deps.compact, enqueueControlItem: commandTargetRuntime.enqueueControlItem, showStatus: commandTargetRuntime.showStatus, openModelMenu: commandTargetRuntime.openModelMenu, getAllowedUserId: deps.getAllowedUserId, setAllowedUserId: deps.setAllowedUserId, registerBotCommands: createTelegramBotCommandRegistrar({ setMyCommands: deps.setMyCommands, }), persistConfig: deps.persistConfig, bindCurrentTopic: deps.bindCurrentTopic, sendTextReply: commandTargetRuntime.sendTextReply, recordRuntimeEvent: deps.recordRuntimeEvent, }); } export function createTelegramCommandHandler< TMessage extends TelegramCommandRuntimeMessage, TContext, >(deps: TelegramCommandRuntimeDeps) { return async function handleTelegramCommand( commandName: string | undefined, message: TMessage, ctx: TContext, ): Promise { return handleTelegramCommandRuntime(commandName, message, ctx, deps); }; } export function createTelegramCommandOrPromptRuntime( deps: TelegramCommandOrPromptRuntimeDeps, ) { return { dispatchMessages: async ( messages: TMessage[], ctx: TContext, ): Promise => { const firstMessage = messages[0]; if (!firstMessage) return; const commandName = parseTelegramCommand( deps.extractRawText(messages), )?.name; const handled = await deps.handleCommand(commandName, firstMessage, ctx); if (handled) return; await deps.enqueueTurn(messages, ctx); }, }; } async function handleTelegramCommandRuntime< TMessage extends TelegramCommandRuntimeMessage, TContext, >( commandName: string | undefined, message: TMessage, ctx: TContext, deps: TelegramCommandRuntimeDeps, ): Promise { const sendReplyFor = (nextMessage: TMessage) => (text: string) => deps.sendTextReply(nextMessage, text); const updateStatusFor = (commandCtx: TContext) => () => deps.updateStatus(commandCtx); return executeTelegramCommandAction( buildTelegramCommandAction(commandName), message, ctx, { handleStop: async (nextMessage, commandCtx) => { await handleTelegramStopCommand({ hasAbortHandler: deps.hasAbortHandler, clearPendingModelSwitch: deps.clearPendingModelSwitch, clearQueuedTelegramItems: () => deps.clearQueuedTelegramItems(commandCtx), setPreserveQueuedTurnsAsHistory: deps.setPreserveQueuedTurnsAsHistory, abortCurrentTurn: deps.abortCurrentTurn, updateStatus: updateStatusFor(commandCtx), sendTextReply: sendReplyFor(nextMessage), }); }, handleCompact: async (nextMessage, commandCtx) => { await handleTelegramCompactCommand({ isIdle: () => deps.isIdle(commandCtx), hasPendingMessages: () => deps.hasPendingMessages(commandCtx), hasActiveTelegramTurn: deps.hasActiveTelegramTurn, hasDispatchPending: deps.hasDispatchPending, hasQueuedTelegramItems: deps.hasQueuedTelegramItems, isCompactionInProgress: deps.isCompactionInProgress, setCompactionInProgress: deps.setCompactionInProgress, updateStatus: updateStatusFor(commandCtx), dispatchNextQueuedTelegramTurn: () => deps.dispatchNextQueuedTelegramTurn(commandCtx), compact: (callbacks) => deps.compact(commandCtx, callbacks), sendTextReply: sendReplyFor(nextMessage), recordRuntimeEvent: deps.recordRuntimeEvent, }); }, handleStatus: async (nextMessage, commandCtx) => { await handleTelegramStatusCommand({ ctx: commandCtx, showStatus: (controlCtx) => deps.showStatus(nextMessage, controlCtx), }); }, handleModel: async (nextMessage, commandCtx) => { await handleTelegramModelCommand({ ctx: commandCtx, openModelMenu: (controlCtx) => deps.openModelMenu(nextMessage, controlCtx), }); }, handleBind: async (nextMessage) => { const chatId = nextMessage.chat?.id; if (typeof chatId !== "number") { await deps.sendTextReply(nextMessage, "Cannot bind: message has no chat id."); return; } await deps.bindCurrentTopic?.({ chatId, threadId: nextMessage.message_thread_id, title: nextMessage.chat?.title, updatedAt: Date.now(), }); await deps.sendTextReply( nextMessage, `Bound this pi session to chat ${chatId}${nextMessage.message_thread_id !== undefined ? ` topic ${nextMessage.message_thread_id}` : ""}.`, ); }, handleHelp: async (nextMessage, nextCommandName, commandCtx) => { await handleTelegramHelpCommand(nextCommandName, { senderUserId: nextMessage.from?.id, getAllowedUserId: deps.getAllowedUserId, setAllowedUserId: deps.setAllowedUserId, registerBotCommands: deps.registerBotCommands, persistConfig: deps.persistConfig, updateStatus: updateStatusFor(commandCtx), sendTextReply: sendReplyFor(nextMessage), }); }, }, ); }