From d91b4a30454b9fc0fea989e7caa63003e8c9ec9d Mon Sep 17 00:00:00 2001 From: hougangdev Date: Tue, 27 Jan 2026 09:37:22 +0800 Subject: [PATCH 1/8] feat: improve /help and /commands formatting with categories and pagination - Add CommandCategory type to organize commands into groups (session, options, status, management, media, tools, docks) - Refactor /help to show grouped sections for better discoverability - Add pagination support for /commands on Telegram (8 commands per page with nav buttons) - Show grouped list without pagination on other channels - Handle commands_page_N callback queries for Telegram pagination navigation --- src/auto-reply/commands-registry.data.ts | 38 ++- src/auto-reply/commands-registry.types.ts | 10 + src/auto-reply/reply/commands-info.ts | 57 ++++- src/auto-reply/status.ts | 282 ++++++++++++++++++---- src/telegram/bot-handlers.ts | 45 ++++ 5 files changed, 384 insertions(+), 48 deletions(-) diff --git a/src/auto-reply/commands-registry.data.ts b/src/auto-reply/commands-registry.data.ts index 5ba6826fe..1e2ebeb57 100644 --- a/src/auto-reply/commands-registry.data.ts +++ b/src/auto-reply/commands-registry.data.ts @@ -2,7 +2,11 @@ import { listChannelDocks } from "../channels/dock.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"; +import type { + ChatCommandDefinition, + CommandCategory, + CommandScope, +} from "./commands-registry.types.js"; type DefineChatCommandInput = { key: string; @@ -16,6 +20,7 @@ type DefineChatCommandInput = { textAlias?: string; textAliases?: string[]; scope?: CommandScope; + category?: CommandCategory; }; function defineChatCommand(command: DefineChatCommandInput): ChatCommandDefinition { @@ -37,6 +42,7 @@ function defineChatCommand(command: DefineChatCommandInput): ChatCommandDefiniti argsMenu: command.argsMenu, textAliases: aliases, scope, + category: command.category, }; } @@ -48,6 +54,7 @@ function defineDockCommand(dock: ChannelDock): ChatCommandDefinition { nativeName: `dock_${dock.id}`, description: `Switch to ${dock.id} for replies.`, textAliases: [`/dock-${dock.id}`, `/dock_${dock.id}`], + category: "docks", }); } @@ -124,18 +131,21 @@ function buildChatCommands(): ChatCommandDefinition[] { nativeName: "help", description: "Show available commands.", textAlias: "/help", + category: "status", }), defineChatCommand({ key: "commands", nativeName: "commands", description: "List all slash commands.", textAlias: "/commands", + category: "status", }), defineChatCommand({ key: "skill", nativeName: "skill", description: "Run a skill by name.", textAlias: "/skill", + category: "tools", args: [ { name: "name", @@ -156,6 +166,7 @@ function buildChatCommands(): ChatCommandDefinition[] { nativeName: "status", description: "Show current status.", textAlias: "/status", + category: "status", }), defineChatCommand({ key: "allowlist", @@ -163,6 +174,7 @@ function buildChatCommands(): ChatCommandDefinition[] { textAlias: "/allowlist", acceptsArgs: true, scope: "text", + category: "management", }), defineChatCommand({ key: "approve", @@ -170,6 +182,7 @@ function buildChatCommands(): ChatCommandDefinition[] { description: "Approve or deny exec requests.", textAlias: "/approve", acceptsArgs: true, + category: "management", }), defineChatCommand({ key: "context", @@ -177,12 +190,14 @@ function buildChatCommands(): ChatCommandDefinition[] { description: "Explain how context is built and used.", textAlias: "/context", acceptsArgs: true, + category: "status", }), defineChatCommand({ key: "tts", nativeName: "tts", description: "Control text-to-speech (TTS).", textAlias: "/tts", + category: "media", args: [ { name: "action", @@ -225,12 +240,14 @@ function buildChatCommands(): ChatCommandDefinition[] { nativeName: "whoami", description: "Show your sender id.", textAlias: "/whoami", + category: "status", }), defineChatCommand({ key: "subagents", nativeName: "subagents", description: "List/stop/log/info subagent runs for this session.", textAlias: "/subagents", + category: "management", args: [ { name: "action", @@ -257,6 +274,7 @@ function buildChatCommands(): ChatCommandDefinition[] { nativeName: "config", description: "Show or set config values.", textAlias: "/config", + category: "management", args: [ { name: "action", @@ -284,6 +302,7 @@ function buildChatCommands(): ChatCommandDefinition[] { nativeName: "debug", description: "Set runtime debug overrides.", textAlias: "/debug", + category: "management", args: [ { name: "action", @@ -311,6 +330,7 @@ function buildChatCommands(): ChatCommandDefinition[] { nativeName: "usage", description: "Usage footer or cost summary.", textAlias: "/usage", + category: "options", args: [ { name: "mode", @@ -326,18 +346,21 @@ function buildChatCommands(): ChatCommandDefinition[] { nativeName: "stop", description: "Stop the current run.", textAlias: "/stop", + category: "session", }), defineChatCommand({ key: "restart", nativeName: "restart", description: "Restart Clawdbot.", textAlias: "/restart", + category: "tools", }), defineChatCommand({ key: "activation", nativeName: "activation", description: "Set group activation mode.", textAlias: "/activation", + category: "management", args: [ { name: "mode", @@ -353,6 +376,7 @@ function buildChatCommands(): ChatCommandDefinition[] { nativeName: "send", description: "Set send policy.", textAlias: "/send", + category: "management", args: [ { name: "mode", @@ -369,6 +393,7 @@ function buildChatCommands(): ChatCommandDefinition[] { description: "Reset the current session.", textAlias: "/reset", acceptsArgs: true, + category: "session", }), defineChatCommand({ key: "new", @@ -376,12 +401,14 @@ function buildChatCommands(): ChatCommandDefinition[] { description: "Start a new session.", textAlias: "/new", acceptsArgs: true, + category: "session", }), defineChatCommand({ key: "compact", description: "Compact the session context.", textAlias: "/compact", scope: "text", + category: "session", args: [ { name: "instructions", @@ -396,6 +423,7 @@ function buildChatCommands(): ChatCommandDefinition[] { nativeName: "think", description: "Set thinking level.", textAlias: "/think", + category: "options", args: [ { name: "level", @@ -411,6 +439,7 @@ function buildChatCommands(): ChatCommandDefinition[] { nativeName: "verbose", description: "Toggle verbose mode.", textAlias: "/verbose", + category: "options", args: [ { name: "mode", @@ -426,6 +455,7 @@ function buildChatCommands(): ChatCommandDefinition[] { nativeName: "reasoning", description: "Toggle reasoning visibility.", textAlias: "/reasoning", + category: "options", args: [ { name: "mode", @@ -441,6 +471,7 @@ function buildChatCommands(): ChatCommandDefinition[] { nativeName: "elevated", description: "Toggle elevated mode.", textAlias: "/elevated", + category: "options", args: [ { name: "mode", @@ -456,6 +487,7 @@ function buildChatCommands(): ChatCommandDefinition[] { nativeName: "exec", description: "Set exec defaults for this session.", textAlias: "/exec", + category: "options", args: [ { name: "options", @@ -470,6 +502,7 @@ function buildChatCommands(): ChatCommandDefinition[] { nativeName: "model", description: "Show or set the model.", textAlias: "/model", + category: "options", args: [ { name: "model", @@ -485,12 +518,14 @@ function buildChatCommands(): ChatCommandDefinition[] { textAlias: "/models", argsParsing: "none", acceptsArgs: true, + category: "options", }), defineChatCommand({ key: "queue", nativeName: "queue", description: "Adjust queue settings.", textAlias: "/queue", + category: "options", args: [ { name: "mode", @@ -523,6 +558,7 @@ function buildChatCommands(): ChatCommandDefinition[] { description: "Run host shell commands (host-only).", textAlias: "/bash", scope: "text", + category: "tools", args: [ { name: "command", diff --git a/src/auto-reply/commands-registry.types.ts b/src/auto-reply/commands-registry.types.ts index 5e5bdd8cb..6b9371604 100644 --- a/src/auto-reply/commands-registry.types.ts +++ b/src/auto-reply/commands-registry.types.ts @@ -2,6 +2,15 @@ import type { ClawdbotConfig } from "../config/types.js"; export type CommandScope = "text" | "native" | "both"; +export type CommandCategory = + | "session" + | "options" + | "status" + | "management" + | "media" + | "tools" + | "docks"; + export type CommandArgType = "string" | "number" | "boolean"; export type CommandArgChoiceContext = { @@ -51,6 +60,7 @@ export type ChatCommandDefinition = { formatArgs?: (values: CommandArgValues) => string | undefined; argsMenu?: CommandArgMenuSpec | "auto"; scope: CommandScope; + category?: CommandCategory; }; export type NativeCommandSpec = { diff --git a/src/auto-reply/reply/commands-info.ts b/src/auto-reply/reply/commands-info.ts index 1a525150c..eec7053a7 100644 --- a/src/auto-reply/reply/commands-info.ts +++ b/src/auto-reply/reply/commands-info.ts @@ -1,6 +1,10 @@ import { logVerbose } from "../../globals.js"; import { listSkillCommandsForWorkspace } from "../skill-commands.js"; -import { buildCommandsMessage, buildHelpMessage } from "../status.js"; +import { + buildCommandsMessage, + buildCommandsMessagePaginated, + buildHelpMessage, +} from "../status.js"; import { buildStatusReply } from "./commands-status.js"; import { buildContextReply } from "./commands-context-report.js"; import type { CommandHandler } from "./commands-types.js"; @@ -35,12 +39,61 @@ export const handleCommandsListCommand: CommandHandler = async (params, allowTex workspaceDir: params.workspaceDir, cfg: params.cfg, }); + const surface = params.ctx.Surface; + + // For Telegram, return paginated result with inline buttons + if (surface === "telegram") { + const result = buildCommandsMessagePaginated(params.cfg, skillCommands, { + page: 1, + surface, + }); + + // Build inline keyboard for pagination if there are multiple pages + if (result.totalPages > 1) { + return { + shouldContinue: false, + reply: { + text: result.text, + channelData: { + telegram: { + buttons: buildCommandsPaginationKeyboard(result.currentPage, result.totalPages), + }, + }, + }, + }; + } + + return { + shouldContinue: false, + reply: { text: result.text }, + }; + } + return { shouldContinue: false, - reply: { text: buildCommandsMessage(params.cfg, skillCommands) }, + reply: { text: buildCommandsMessage(params.cfg, skillCommands, { surface }) }, }; }; +export function buildCommandsPaginationKeyboard( + currentPage: number, + totalPages: number, +): Array> { + const buttons: Array<{ text: string; callback_data: string }> = []; + + if (currentPage > 1) { + buttons.push({ text: "◀ Prev", callback_data: `commands_page_${currentPage - 1}` }); + } + + buttons.push({ text: `${currentPage}/${totalPages}`, callback_data: "commands_page_noop" }); + + if (currentPage < totalPages) { + buttons.push({ text: "Next ▶", callback_data: `commands_page_${currentPage + 1}` }); + } + + return [buttons]; +} + export const handleStatusCommand: CommandHandler = async (params, allowTextCommands) => { if (!allowTextCommands) return null; const statusRequested = diff --git a/src/auto-reply/status.ts b/src/auto-reply/status.ts index 733205c8c..713815f4f 100644 --- a/src/auto-reply/status.ts +++ b/src/auto-reply/status.ts @@ -29,7 +29,12 @@ import { resolveModelCostConfig, } from "../utils/usage-format.js"; import { VERSION } from "../version.js"; -import { listChatCommands, listChatCommandsForConfig } from "./commands-registry.js"; +import { + listChatCommands, + listChatCommandsForConfig, + type ChatCommandDefinition, +} from "./commands-registry.js"; +import type { CommandCategory } from "./commands-registry.types.js"; import { listPluginCommands } from "../plugins/commands.js"; import type { SkillCommandSpec } from "../agents/skills.js"; import type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel } from "./thinking.js"; @@ -427,61 +432,248 @@ export function buildStatusMessage(args: StatusArgs): string { .join("\n"); } +const CATEGORY_LABELS: Record = { + session: "Session", + options: "Options", + status: "Status", + management: "Management", + media: "Media", + tools: "Tools", + docks: "Docks", +}; + +const CATEGORY_ORDER: CommandCategory[] = [ + "session", + "options", + "status", + "management", + "media", + "tools", + "docks", +]; + +function groupCommandsByCategory( + commands: ChatCommandDefinition[], +): Map { + const grouped = new Map(); + for (const category of CATEGORY_ORDER) { + grouped.set(category, []); + } + for (const command of commands) { + const category = command.category ?? "tools"; + const list = grouped.get(category) ?? []; + list.push(command); + grouped.set(category, list); + } + return grouped; +} + export function buildHelpMessage(cfg?: ClawdbotConfig): string { - const options = [ - "/think ", - "/verbose on|full|off", - "/reasoning on|off", - "/elevated on|off|ask|full", - "/model ", - "/usage off|tokens|full", - ]; - if (cfg?.commands?.config === true) options.push("/config show"); - if (cfg?.commands?.debug === true) options.push("/debug show"); - return [ - "ℹ️ Help", - "Shortcuts: /new reset | /compact [instructions] | /restart relink (if enabled)", - `Options: ${options.join(" | ")}`, - "Skills: /skill [input]", - "More: /commands for all slash commands", - ].join("\n"); + const lines = ["ℹ️ Help", ""]; + + // Session commands - quick shortcuts + lines.push("Session"); + lines.push(" /new | /reset | /compact [instructions] | /stop"); + lines.push(""); + + // Options - most commonly used + const optionParts = ["/think ", "/model ", "/verbose on|off"]; + if (cfg?.commands?.config === true) optionParts.push("/config"); + if (cfg?.commands?.debug === true) optionParts.push("/debug"); + lines.push("Options"); + lines.push(` ${optionParts.join(" | ")}`); + lines.push(""); + + // Status commands + lines.push("Status"); + lines.push(" /status | /whoami | /context"); + lines.push(""); + + // Skills + lines.push("Skills"); + lines.push(" /skill [input]"); + + lines.push(""); + lines.push("More: /commands for full list"); + + return lines.join("\n"); +} + +const COMMANDS_PER_PAGE = 8; + +export type CommandsMessageOptions = { + page?: number; + surface?: string; +}; + +export type CommandsMessageResult = { + text: string; + totalPages: number; + currentPage: number; + hasNext: boolean; + hasPrev: boolean; +}; + +function formatCommandEntry(command: ChatCommandDefinition): string { + const primary = command.nativeName + ? `/${command.nativeName}` + : command.textAliases[0]?.trim() || `/${command.key}`; + const seen = new Set(); + const aliases = command.textAliases + .map((alias) => alias.trim()) + .filter(Boolean) + .filter((alias) => alias.toLowerCase() !== primary.toLowerCase()) + .filter((alias) => { + const key = alias.toLowerCase(); + if (seen.has(key)) return false; + seen.add(key); + return true; + }); + const aliasLabel = aliases.length ? ` (${aliases.join(", ")})` : ""; + const scopeLabel = command.scope === "text" ? " [text]" : ""; + return `${primary}${aliasLabel}${scopeLabel} - ${command.description}`; } export function buildCommandsMessage( cfg?: ClawdbotConfig, skillCommands?: SkillCommandSpec[], + options?: CommandsMessageOptions, ): string { - const lines = ["ℹ️ Slash commands"]; + const result = buildCommandsMessagePaginated(cfg, skillCommands, options); + return result.text; +} + +export function buildCommandsMessagePaginated( + cfg?: ClawdbotConfig, + skillCommands?: SkillCommandSpec[], + options?: CommandsMessageOptions, +): CommandsMessageResult { + const page = Math.max(1, options?.page ?? 1); + const surface = options?.surface?.toLowerCase(); + const isTelegram = surface === "telegram"; + const commands = cfg ? listChatCommandsForConfig(cfg, { skillCommands }) : listChatCommands({ skillCommands }); - for (const command of commands) { - const primary = command.nativeName - ? `/${command.nativeName}` - : command.textAliases[0]?.trim() || `/${command.key}`; - const seen = new Set(); - const aliases = command.textAliases - .map((alias) => alias.trim()) - .filter(Boolean) - .filter((alias) => alias.toLowerCase() !== primary.toLowerCase()) - .filter((alias) => { - const key = alias.toLowerCase(); - if (seen.has(key)) return false; - seen.add(key); - return true; - }); - const aliasLabel = aliases.length ? ` (aliases: ${aliases.join(", ")})` : ""; - const scopeLabel = command.scope === "text" ? " (text-only)" : ""; - lines.push(`${primary}${aliasLabel}${scopeLabel} - ${command.description}`); - } const pluginCommands = listPluginCommands(); - if (pluginCommands.length > 0) { - lines.push(""); - lines.push("Plugin commands:"); - for (const command of pluginCommands) { - const pluginLabel = command.pluginId ? ` (plugin: ${command.pluginId})` : ""; - lines.push(`/${command.name}${pluginLabel} - ${command.description}`); + + // For non-Telegram surfaces, show grouped list without pagination + if (!isTelegram) { + const grouped = groupCommandsByCategory(commands); + const lines = ["ℹ️ Slash commands", ""]; + + for (const category of CATEGORY_ORDER) { + const categoryCommands = grouped.get(category) ?? []; + if (categoryCommands.length === 0) continue; + + lines.push(`${CATEGORY_LABELS[category]}`); + for (const command of categoryCommands) { + lines.push(` ${formatCommandEntry(command)}`); + } + lines.push(""); + } + + if (pluginCommands.length > 0) { + lines.push("Plugins"); + for (const command of pluginCommands) { + const pluginLabel = command.pluginId ? ` (${command.pluginId})` : ""; + lines.push(` /${command.name}${pluginLabel} - ${command.description}`); + } + } + + return { + text: lines.join("\n").trim(), + totalPages: 1, + currentPage: 1, + hasNext: false, + hasPrev: false, + }; + } + + // For Telegram, use pagination + const grouped = groupCommandsByCategory(commands); + + // Flatten commands with category headers for pagination + type PageItem = + | { type: "header"; category: CommandCategory } + | { type: "command"; command: ChatCommandDefinition }; + const items: PageItem[] = []; + + for (const category of CATEGORY_ORDER) { + const categoryCommands = grouped.get(category) ?? []; + if (categoryCommands.length === 0) continue; + items.push({ type: "header", category }); + for (const command of categoryCommands) { + items.push({ type: "command", command }); } } - return lines.join("\n"); + + // Add plugin commands + if (pluginCommands.length > 0) { + items.push({ type: "header", category: "tools" }); // Reuse tools category for plugins header indicator + } + + // Calculate pages based on command count (headers don't count toward limit) + const commandItems = items.filter((item) => item.type === "command"); + const totalCommands = commandItems.length + pluginCommands.length; + const totalPages = Math.max(1, Math.ceil(totalCommands / COMMANDS_PER_PAGE)); + const currentPage = Math.min(page, totalPages); + const startIndex = (currentPage - 1) * COMMANDS_PER_PAGE; + const endIndex = startIndex + COMMANDS_PER_PAGE; + + // Build page content + const lines = [`ℹ️ Commands (${currentPage}/${totalPages})`, ""]; + + let commandIndex = 0; + let currentCategory: CommandCategory | null = null; + let pageCommandCount = 0; + + for (const item of items) { + if (pageCommandCount >= COMMANDS_PER_PAGE) break; + + if (item.type === "header") { + currentCategory = item.category; + continue; + } + + if (commandIndex >= startIndex && commandIndex < endIndex) { + // Add category header if this is the first command of a category on this page + if ( + (currentCategory && pageCommandCount === 0) || + items[items.indexOf(item) - 1]?.type === "header" + ) { + if (currentCategory) { + if (pageCommandCount > 0) lines.push(""); + lines.push(CATEGORY_LABELS[currentCategory]); + } + } + lines.push(` ${formatCommandEntry(item.command)}`); + pageCommandCount++; + } + commandIndex++; + } + + // Add plugin commands if they fall within this page range + const pluginStartIndex = commandItems.length; + for (let i = 0; i < pluginCommands.length && pageCommandCount < COMMANDS_PER_PAGE; i++) { + const pluginIndex = pluginStartIndex + i; + if (pluginIndex >= startIndex && pluginIndex < endIndex) { + if (i === 0 || pluginIndex === startIndex) { + if (pageCommandCount > 0) lines.push(""); + lines.push("Plugins"); + } + const command = pluginCommands[i]; + const pluginLabel = command.pluginId ? ` (${command.pluginId})` : ""; + lines.push(` /${command.name}${pluginLabel} - ${command.description}`); + pageCommandCount++; + } + } + + return { + text: lines.join("\n"), + totalPages, + currentPage, + hasNext: currentPage < totalPages, + hasPrev: currentPage > 1, + }; } diff --git a/src/telegram/bot-handlers.ts b/src/telegram/bot-handlers.ts index 7a5b88fd7..f4acafc19 100644 --- a/src/telegram/bot-handlers.ts +++ b/src/telegram/bot-handlers.ts @@ -4,6 +4,9 @@ import { createInboundDebouncer, resolveInboundDebounceMs, } from "../auto-reply/inbound-debounce.js"; +import { buildCommandsPaginationKeyboard } from "../auto-reply/reply/commands-info.js"; +import { buildCommandsMessagePaginated } from "../auto-reply/status.js"; +import { listSkillCommandsForAgents } from "../auto-reply/skill-commands.js"; import { loadConfig } from "../config/config.js"; import { writeConfigFile } from "../config/io.js"; import { danger, logVerbose, warn } from "../globals.js"; @@ -17,6 +20,7 @@ import { migrateTelegramGroupConfig } from "./group-migration.js"; import { resolveTelegramInlineButtonsScope } from "./inline-buttons.js"; import { readTelegramAllowFromStore } from "./pairing-store.js"; import { resolveChannelConfigWrites } from "../channels/plugins/config-writes.js"; +import { buildInlineKeyboard } from "./send.js"; export const registerTelegramHandlers = ({ cfg, @@ -199,6 +203,47 @@ export const registerTelegramHandlers = ({ const callbackMessage = callback.message; if (!data || !callbackMessage) return; + // Handle commands pagination callback + const paginationMatch = data.match(/^commands_page_(\d+|noop)$/); + if (paginationMatch) { + const pageValue = paginationMatch[1]; + if (pageValue === "noop") return; // Page number button - no action + + const page = parseInt(pageValue, 10); + if (isNaN(page) || page < 1) return; + + const skillCommands = listSkillCommandsForAgents({ cfg }); + const result = buildCommandsMessagePaginated(cfg, skillCommands, { + page, + surface: "telegram", + }); + + const messageId = callbackMessage.message_id; + const chatId = callbackMessage.chat.id; + const keyboard = + result.totalPages > 1 + ? buildInlineKeyboard( + buildCommandsPaginationKeyboard(result.currentPage, result.totalPages), + ) + : undefined; + + try { + await bot.api.editMessageText( + chatId, + messageId, + result.text, + keyboard ? { reply_markup: keyboard } : undefined, + ); + } catch (editErr) { + // Ignore "message is not modified" errors (user clicked same page) + const errStr = String(editErr); + if (!errStr.includes("message is not modified")) { + throw editErr; + } + } + return; + } + const inlineButtonsScope = resolveTelegramInlineButtonsScope({ cfg, accountId, From 97440eaf527d3b8443f729f630a4037fbf61ca41 Mon Sep 17 00:00:00 2001 From: hougangdev Date: Tue, 27 Jan 2026 10:18:53 +0800 Subject: [PATCH 2/8] test: update status tests for new help/commands format --- src/auto-reply/status.test.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/auto-reply/status.test.ts b/src/auto-reply/status.test.ts index 31b6b92ec..edefaf283 100644 --- a/src/auto-reply/status.test.ts +++ b/src/auto-reply/status.test.ts @@ -402,8 +402,8 @@ describe("buildCommandsMessage", () => { } as ClawdbotConfig); expect(text).toContain("/commands - List all slash commands."); expect(text).toContain("/skill - Run a skill by name."); - expect(text).toContain("/think (aliases: /thinking, /t) - Set thinking level."); - expect(text).toContain("/compact (text-only) - Compact the session context."); + expect(text).toContain("/think (/thinking, /t) - Set thinking level."); + expect(text).toContain("/compact [text] - Compact the session context."); expect(text).not.toContain("/config"); expect(text).not.toContain("/debug"); }); @@ -430,7 +430,8 @@ describe("buildHelpMessage", () => { const text = buildHelpMessage({ commands: { config: false, debug: false }, } as ClawdbotConfig); - expect(text).toContain("Skills: /skill [input]"); + expect(text).toContain("Skills"); + expect(text).toContain("/skill [input]"); expect(text).not.toContain("/config"); expect(text).not.toContain("/debug"); }); From cc1782b1055fda77ef89bcc023a23c1c1bad1c74 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Tue, 27 Jan 2026 02:35:09 -0500 Subject: [PATCH 3/8] fix: tighten commands output + telegram pagination (#2504) Co-authored-by: hougangdev --- src/auto-reply/reply/commands-info.test.ts | 13 ++ src/auto-reply/reply/commands-info.ts | 31 +++-- src/auto-reply/status.test.ts | 47 ++++++- src/auto-reply/status.ts | 155 ++++++++------------- src/telegram/bot-handlers.ts | 42 ++++++ src/telegram/bot.test.ts | 85 +++++++++++ 6 files changed, 263 insertions(+), 110 deletions(-) create mode 100644 src/auto-reply/reply/commands-info.test.ts diff --git a/src/auto-reply/reply/commands-info.test.ts b/src/auto-reply/reply/commands-info.test.ts new file mode 100644 index 000000000..9751c39cc --- /dev/null +++ b/src/auto-reply/reply/commands-info.test.ts @@ -0,0 +1,13 @@ +import { describe, expect, it } from "vitest"; +import { buildCommandsPaginationKeyboard } from "./commands-info.js"; + +describe("buildCommandsPaginationKeyboard", () => { + it("adds agent id to callback data when provided", () => { + const keyboard = buildCommandsPaginationKeyboard(2, 3, "agent-main"); + expect(keyboard[0]).toEqual([ + { text: "◀ Prev", callback_data: "commands_page_1:agent-main" }, + { text: "2/3", callback_data: "commands_page_noop:agent-main" }, + { text: "Next ▶", callback_data: "commands_page_3:agent-main" }, + ]); + }); +}); diff --git a/src/auto-reply/reply/commands-info.ts b/src/auto-reply/reply/commands-info.ts index eec7053a7..e7d8a8f6f 100644 --- a/src/auto-reply/reply/commands-info.ts +++ b/src/auto-reply/reply/commands-info.ts @@ -1,5 +1,5 @@ import { logVerbose } from "../../globals.js"; -import { listSkillCommandsForWorkspace } from "../skill-commands.js"; +import { listSkillCommandsForAgents } from "../skill-commands.js"; import { buildCommandsMessage, buildCommandsMessagePaginated, @@ -35,20 +35,18 @@ export const handleCommandsListCommand: CommandHandler = async (params, allowTex } const skillCommands = params.skillCommands ?? - listSkillCommandsForWorkspace({ - workspaceDir: params.workspaceDir, + listSkillCommandsForAgents({ cfg: params.cfg, + agentIds: params.agentId ? [params.agentId] : undefined, }); const surface = params.ctx.Surface; - // For Telegram, return paginated result with inline buttons if (surface === "telegram") { const result = buildCommandsMessagePaginated(params.cfg, skillCommands, { page: 1, surface, }); - // Build inline keyboard for pagination if there are multiple pages if (result.totalPages > 1) { return { shouldContinue: false, @@ -56,7 +54,11 @@ export const handleCommandsListCommand: CommandHandler = async (params, allowTex text: result.text, channelData: { telegram: { - buttons: buildCommandsPaginationKeyboard(result.currentPage, result.totalPages), + buttons: buildCommandsPaginationKeyboard( + result.currentPage, + result.totalPages, + params.agentId, + ), }, }, }, @@ -78,17 +80,28 @@ export const handleCommandsListCommand: CommandHandler = async (params, allowTex export function buildCommandsPaginationKeyboard( currentPage: number, totalPages: number, + agentId?: string, ): Array> { const buttons: Array<{ text: string; callback_data: string }> = []; + const suffix = agentId ? `:${agentId}` : ""; if (currentPage > 1) { - buttons.push({ text: "◀ Prev", callback_data: `commands_page_${currentPage - 1}` }); + buttons.push({ + text: "◀ Prev", + callback_data: `commands_page_${currentPage - 1}${suffix}`, + }); } - buttons.push({ text: `${currentPage}/${totalPages}`, callback_data: "commands_page_noop" }); + buttons.push({ + text: `${currentPage}/${totalPages}`, + callback_data: `commands_page_noop${suffix}`, + }); if (currentPage < totalPages) { - buttons.push({ text: "Next ▶", callback_data: `commands_page_${currentPage + 1}` }); + buttons.push({ + text: "Next ▶", + callback_data: `commands_page_${currentPage + 1}${suffix}`, + }); } return [buttons]; diff --git a/src/auto-reply/status.test.ts b/src/auto-reply/status.test.ts index edefaf283..465352538 100644 --- a/src/auto-reply/status.test.ts +++ b/src/auto-reply/status.test.ts @@ -4,7 +4,20 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { normalizeTestText } from "../../test/helpers/normalize-text.js"; import { withTempHome } from "../../test/helpers/temp-home.js"; import type { ClawdbotConfig } from "../config/config.js"; -import { buildCommandsMessage, buildHelpMessage, buildStatusMessage } from "./status.js"; +import { + buildCommandsMessage, + buildCommandsMessagePaginated, + buildHelpMessage, + buildStatusMessage, +} from "./status.js"; + +const { listPluginCommands } = vi.hoisted(() => ({ + listPluginCommands: vi.fn(() => []), +})); + +vi.mock("../plugins/commands.js", () => ({ + listPluginCommands, +})); afterEach(() => { vi.restoreAllMocks(); @@ -400,6 +413,8 @@ describe("buildCommandsMessage", () => { const text = buildCommandsMessage({ commands: { config: false, debug: false }, } as ClawdbotConfig); + expect(text).toContain("ℹ️ Slash commands"); + expect(text).toContain("Status"); expect(text).toContain("/commands - List all slash commands."); expect(text).toContain("/skill - Run a skill by name."); expect(text).toContain("/think (/thinking, /t) - Set thinking level."); @@ -436,3 +451,33 @@ describe("buildHelpMessage", () => { expect(text).not.toContain("/debug"); }); }); + +describe("buildCommandsMessagePaginated", () => { + it("formats telegram output with pages", () => { + const result = buildCommandsMessagePaginated( + { + commands: { config: false, debug: false }, + } as ClawdbotConfig, + undefined, + { surface: "telegram", page: 1 }, + ); + expect(result.text).toContain("ℹ️ Commands (1/"); + expect(result.text).toContain("Session"); + expect(result.text).toContain("/stop - Stop the current run."); + }); + + it("includes plugin commands in the paginated list", () => { + listPluginCommands.mockReturnValue([ + { name: "plugin_cmd", description: "Plugin command", pluginId: "demo-plugin" }, + ]); + const result = buildCommandsMessagePaginated( + { + commands: { config: false, debug: false }, + } as ClawdbotConfig, + undefined, + { surface: "telegram", page: 99 }, + ); + expect(result.text).toContain("Plugins"); + expect(result.text).toContain("/plugin_cmd (demo-plugin) - Plugin command"); + }); +}); diff --git a/src/auto-reply/status.ts b/src/auto-reply/status.ts index 713815f4f..7344b7502 100644 --- a/src/auto-reply/status.ts +++ b/src/auto-reply/status.ts @@ -34,9 +34,9 @@ import { listChatCommandsForConfig, type ChatCommandDefinition, } from "./commands-registry.js"; -import type { CommandCategory } from "./commands-registry.types.js"; import { listPluginCommands } from "../plugins/commands.js"; import type { SkillCommandSpec } from "../agents/skills.js"; +import type { CommandCategory } from "./commands-registry.types.js"; import type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel } from "./thinking.js"; import type { MediaUnderstandingDecision } from "../media-understanding/types.js"; @@ -471,12 +471,10 @@ function groupCommandsByCategory( export function buildHelpMessage(cfg?: ClawdbotConfig): string { const lines = ["ℹ️ Help", ""]; - // Session commands - quick shortcuts lines.push("Session"); lines.push(" /new | /reset | /compact [instructions] | /stop"); lines.push(""); - // Options - most commonly used const optionParts = ["/think ", "/model ", "/verbose on|off"]; if (cfg?.commands?.config === true) optionParts.push("/config"); if (cfg?.commands?.debug === true) optionParts.push("/debug"); @@ -484,12 +482,10 @@ export function buildHelpMessage(cfg?: ClawdbotConfig): string { lines.push(` ${optionParts.join(" | ")}`); lines.push(""); - // Status commands lines.push("Status"); lines.push(" /status | /whoami | /context"); lines.push(""); - // Skills lines.push("Skills"); lines.push(" /skill [input]"); @@ -534,6 +530,54 @@ function formatCommandEntry(command: ChatCommandDefinition): string { return `${primary}${aliasLabel}${scopeLabel} - ${command.description}`; } +type CommandsListItem = { + label: string; + text: string; +}; + +function buildCommandItems( + commands: ChatCommandDefinition[], + pluginCommands: ReturnType, +): CommandsListItem[] { + const grouped = groupCommandsByCategory(commands); + const items: CommandsListItem[] = []; + + for (const category of CATEGORY_ORDER) { + const categoryCommands = grouped.get(category) ?? []; + if (categoryCommands.length === 0) continue; + const label = CATEGORY_LABELS[category]; + for (const command of categoryCommands) { + items.push({ label, text: formatCommandEntry(command) }); + } + } + + for (const command of pluginCommands) { + const pluginLabel = command.pluginId ? ` (${command.pluginId})` : ""; + items.push({ + label: "Plugins", + text: `/${command.name}${pluginLabel} - ${command.description}`, + }); + } + + return items; +} + +function formatCommandList(items: CommandsListItem[]): string { + const lines: string[] = []; + let currentLabel: string | null = null; + + for (const item of items) { + if (item.label !== currentLabel) { + if (lines.length > 0) lines.push(""); + lines.push(item.label); + currentLabel = item.label; + } + lines.push(` ${item.text}`); + } + + return lines.join("\n"); +} + export function buildCommandsMessage( cfg?: ClawdbotConfig, skillCommands?: SkillCommandSpec[], @@ -556,31 +600,11 @@ export function buildCommandsMessagePaginated( ? listChatCommandsForConfig(cfg, { skillCommands }) : listChatCommands({ skillCommands }); const pluginCommands = listPluginCommands(); + const items = buildCommandItems(commands, pluginCommands); - // For non-Telegram surfaces, show grouped list without pagination if (!isTelegram) { - const grouped = groupCommandsByCategory(commands); const lines = ["ℹ️ Slash commands", ""]; - - for (const category of CATEGORY_ORDER) { - const categoryCommands = grouped.get(category) ?? []; - if (categoryCommands.length === 0) continue; - - lines.push(`${CATEGORY_LABELS[category]}`); - for (const command of categoryCommands) { - lines.push(` ${formatCommandEntry(command)}`); - } - lines.push(""); - } - - if (pluginCommands.length > 0) { - lines.push("Plugins"); - for (const command of pluginCommands) { - const pluginLabel = command.pluginId ? ` (${command.pluginId})` : ""; - lines.push(` /${command.name}${pluginLabel} - ${command.description}`); - } - } - + lines.push(formatCommandList(items)); return { text: lines.join("\n").trim(), totalPages: 1, @@ -590,87 +614,18 @@ export function buildCommandsMessagePaginated( }; } - // For Telegram, use pagination - const grouped = groupCommandsByCategory(commands); - - // Flatten commands with category headers for pagination - type PageItem = - | { type: "header"; category: CommandCategory } - | { type: "command"; command: ChatCommandDefinition }; - const items: PageItem[] = []; - - for (const category of CATEGORY_ORDER) { - const categoryCommands = grouped.get(category) ?? []; - if (categoryCommands.length === 0) continue; - items.push({ type: "header", category }); - for (const command of categoryCommands) { - items.push({ type: "command", command }); - } - } - - // Add plugin commands - if (pluginCommands.length > 0) { - items.push({ type: "header", category: "tools" }); // Reuse tools category for plugins header indicator - } - - // Calculate pages based on command count (headers don't count toward limit) - const commandItems = items.filter((item) => item.type === "command"); - const totalCommands = commandItems.length + pluginCommands.length; + const totalCommands = items.length; const totalPages = Math.max(1, Math.ceil(totalCommands / COMMANDS_PER_PAGE)); const currentPage = Math.min(page, totalPages); const startIndex = (currentPage - 1) * COMMANDS_PER_PAGE; const endIndex = startIndex + COMMANDS_PER_PAGE; + const pageItems = items.slice(startIndex, endIndex); - // Build page content const lines = [`ℹ️ Commands (${currentPage}/${totalPages})`, ""]; - - let commandIndex = 0; - let currentCategory: CommandCategory | null = null; - let pageCommandCount = 0; - - for (const item of items) { - if (pageCommandCount >= COMMANDS_PER_PAGE) break; - - if (item.type === "header") { - currentCategory = item.category; - continue; - } - - if (commandIndex >= startIndex && commandIndex < endIndex) { - // Add category header if this is the first command of a category on this page - if ( - (currentCategory && pageCommandCount === 0) || - items[items.indexOf(item) - 1]?.type === "header" - ) { - if (currentCategory) { - if (pageCommandCount > 0) lines.push(""); - lines.push(CATEGORY_LABELS[currentCategory]); - } - } - lines.push(` ${formatCommandEntry(item.command)}`); - pageCommandCount++; - } - commandIndex++; - } - - // Add plugin commands if they fall within this page range - const pluginStartIndex = commandItems.length; - for (let i = 0; i < pluginCommands.length && pageCommandCount < COMMANDS_PER_PAGE; i++) { - const pluginIndex = pluginStartIndex + i; - if (pluginIndex >= startIndex && pluginIndex < endIndex) { - if (i === 0 || pluginIndex === startIndex) { - if (pageCommandCount > 0) lines.push(""); - lines.push("Plugins"); - } - const command = pluginCommands[i]; - const pluginLabel = command.pluginId ? ` (${command.pluginId})` : ""; - lines.push(` /${command.name}${pluginLabel} - ${command.description}`); - pageCommandCount++; - } - } + lines.push(formatCommandList(pageItems)); return { - text: lines.join("\n"), + text: lines.join("\n").trim(), totalPages, currentPage, hasNext: currentPage < totalPages, diff --git a/src/telegram/bot-handlers.ts b/src/telegram/bot-handlers.ts index f4acafc19..de012f19c 100644 --- a/src/telegram/bot-handlers.ts +++ b/src/telegram/bot-handlers.ts @@ -7,6 +7,7 @@ import { import { buildCommandsPaginationKeyboard } from "../auto-reply/reply/commands-info.js"; import { buildCommandsMessagePaginated } from "../auto-reply/status.js"; import { listSkillCommandsForAgents } from "../auto-reply/skill-commands.js"; +import { resolveDefaultAgentId } from "../agents/agent-scope.js"; import { loadConfig } from "../config/config.js"; import { writeConfigFile } from "../config/io.js"; import { danger, logVerbose, warn } from "../globals.js"; @@ -368,6 +369,47 @@ export const registerTelegramHandlers = ({ } } + const paginationMatch = data.match(/^commands_page_(\d+|noop)(?::(.+))?$/); + if (paginationMatch) { + const pageValue = paginationMatch[1]; + if (pageValue === "noop") return; + + const page = Number.parseInt(pageValue, 10); + if (Number.isNaN(page) || page < 1) return; + + const agentId = paginationMatch[2]?.trim() || resolveDefaultAgentId(cfg) || undefined; + const skillCommands = listSkillCommandsForAgents({ + cfg, + agentIds: agentId ? [agentId] : undefined, + }); + const result = buildCommandsMessagePaginated(cfg, skillCommands, { + page, + surface: "telegram", + }); + + const keyboard = + result.totalPages > 1 + ? buildInlineKeyboard( + buildCommandsPaginationKeyboard(result.currentPage, result.totalPages, agentId), + ) + : undefined; + + try { + await bot.api.editMessageText( + callbackMessage.chat.id, + callbackMessage.message_id, + result.text, + keyboard ? { reply_markup: keyboard } : undefined, + ); + } catch (editErr) { + const errStr = String(editErr); + if (!errStr.includes("message is not modified")) { + throw editErr; + } + } + return; + } + const syntheticMessage: TelegramMessage = { ...callbackMessage, from: callback.from, diff --git a/src/telegram/bot.test.ts b/src/telegram/bot.test.ts index 274f7c6a9..c2de155b0 100644 --- a/src/telegram/bot.test.ts +++ b/src/telegram/bot.test.ts @@ -93,6 +93,7 @@ const commandSpy = vi.fn(); const botCtorSpy = vi.fn(); const answerCallbackQuerySpy = vi.fn(async () => undefined); const sendChatActionSpy = vi.fn(); +const editMessageTextSpy = vi.fn(async () => ({ message_id: 88 })); const setMessageReactionSpy = vi.fn(async () => undefined); const setMyCommandsSpy = vi.fn(async () => undefined); const sendMessageSpy = vi.fn(async () => ({ message_id: 77 })); @@ -102,6 +103,7 @@ type ApiStub = { config: { use: (arg: unknown) => void }; answerCallbackQuery: typeof answerCallbackQuerySpy; sendChatAction: typeof sendChatActionSpy; + editMessageText: typeof editMessageTextSpy; setMessageReaction: typeof setMessageReactionSpy; setMyCommands: typeof setMyCommandsSpy; sendMessage: typeof sendMessageSpy; @@ -112,6 +114,7 @@ const apiStub: ApiStub = { config: { use: useSpy }, answerCallbackQuery: answerCallbackQuerySpy, sendChatAction: sendChatActionSpy, + editMessageText: editMessageTextSpy, setMessageReaction: setMessageReactionSpy, setMyCommands: setMyCommandsSpy, sendMessage: sendMessageSpy, @@ -192,6 +195,7 @@ describe("createTelegramBot", () => { sendPhotoSpy.mockReset(); setMessageReactionSpy.mockReset(); answerCallbackQuerySpy.mockReset(); + editMessageTextSpy.mockReset(); setMyCommandsSpy.mockReset(); wasSentByBot.mockReset(); middlewareUseSpy.mockReset(); @@ -424,6 +428,87 @@ describe("createTelegramBot", () => { expect(answerCallbackQuerySpy).toHaveBeenCalledWith("cbq-2"); }); + it("edits commands list for pagination callbacks", async () => { + onSpy.mockReset(); + listSkillCommandsForAgents.mockReset(); + + createTelegramBot({ token: "tok" }); + const callbackHandler = onSpy.mock.calls.find((call) => call[0] === "callback_query")?.[1] as ( + ctx: Record, + ) => Promise; + expect(callbackHandler).toBeDefined(); + + await callbackHandler({ + callbackQuery: { + id: "cbq-3", + data: "commands_page_2:main", + from: { id: 9, first_name: "Ada", username: "ada_bot" }, + message: { + chat: { id: 1234, type: "private" }, + date: 1736380800, + message_id: 12, + }, + }, + me: { username: "clawdbot_bot" }, + getFile: async () => ({ download: async () => new Uint8Array() }), + }); + + expect(listSkillCommandsForAgents).toHaveBeenCalledWith({ + cfg: expect.any(Object), + agentIds: ["main"], + }); + expect(editMessageTextSpy).toHaveBeenCalledTimes(1); + const [chatId, messageId, text, params] = editMessageTextSpy.mock.calls[0] ?? []; + expect(chatId).toBe(1234); + expect(messageId).toBe(12); + expect(String(text)).toContain("ℹ️ Commands"); + expect(params).toEqual( + expect.objectContaining({ + reply_markup: expect.any(Object), + }), + ); + }); + + it("blocks pagination callbacks when allowlist rejects sender", async () => { + onSpy.mockReset(); + editMessageTextSpy.mockReset(); + + createTelegramBot({ + token: "tok", + config: { + channels: { + telegram: { + dmPolicy: "pairing", + capabilities: { inlineButtons: "allowlist" }, + allowFrom: [], + }, + }, + }, + }); + const callbackHandler = onSpy.mock.calls.find((call) => call[0] === "callback_query")?.[1] as ( + ctx: Record, + ) => Promise; + expect(callbackHandler).toBeDefined(); + + await callbackHandler({ + callbackQuery: { + id: "cbq-4", + data: "commands_page_2", + from: { id: 9, first_name: "Ada", username: "ada_bot" }, + message: { + chat: { id: 1234, type: "private" }, + date: 1736380800, + message_id: 13, + }, + }, + me: { username: "clawdbot_bot" }, + getFile: async () => ({ download: async () => new Uint8Array() }), + }); + + expect(editMessageTextSpy).not.toHaveBeenCalled(); + expect(answerCallbackQuerySpy).toHaveBeenCalledWith("cbq-4"); + }); + it("wraps inbound message with Telegram envelope", async () => { const originalTz = process.env.TZ; process.env.TZ = "Europe/Vienna"; From 2ad550abe8d0e25d377c7a9f752c2a494b2e2e95 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Tue, 27 Jan 2026 02:42:54 -0500 Subject: [PATCH 4/8] fix: land /help + /commands formatting (#2504) (thanks @hougangdev) --- CHANGELOG.md | 1 + src/telegram/bot-handlers.ts | 41 ------------------------------------ 2 files changed, 1 insertion(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33dd3dafc..749a6c660 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Docs: https://docs.clawd.bot Status: unreleased. ### Changes +- Commands: group /help and /commands output with Telegram paging. (#2504) Thanks @hougangdev. - macOS: limit project-local `node_modules/.bin` PATH preference to debug builds (reduce PATH hijacking risk). - Tools: add per-sender group tool policies and fix precedence. (#1757) Thanks @adam91holt. - Agents: summarize dropped messages during compaction safeguard pruning. (#2509) Thanks @jogi47. diff --git a/src/telegram/bot-handlers.ts b/src/telegram/bot-handlers.ts index de012f19c..477b98280 100644 --- a/src/telegram/bot-handlers.ts +++ b/src/telegram/bot-handlers.ts @@ -204,47 +204,6 @@ export const registerTelegramHandlers = ({ const callbackMessage = callback.message; if (!data || !callbackMessage) return; - // Handle commands pagination callback - const paginationMatch = data.match(/^commands_page_(\d+|noop)$/); - if (paginationMatch) { - const pageValue = paginationMatch[1]; - if (pageValue === "noop") return; // Page number button - no action - - const page = parseInt(pageValue, 10); - if (isNaN(page) || page < 1) return; - - const skillCommands = listSkillCommandsForAgents({ cfg }); - const result = buildCommandsMessagePaginated(cfg, skillCommands, { - page, - surface: "telegram", - }); - - const messageId = callbackMessage.message_id; - const chatId = callbackMessage.chat.id; - const keyboard = - result.totalPages > 1 - ? buildInlineKeyboard( - buildCommandsPaginationKeyboard(result.currentPage, result.totalPages), - ) - : undefined; - - try { - await bot.api.editMessageText( - chatId, - messageId, - result.text, - keyboard ? { reply_markup: keyboard } : undefined, - ); - } catch (editErr) { - // Ignore "message is not modified" errors (user clicked same page) - const errStr = String(editErr); - if (!errStr.includes("message is not modified")) { - throw editErr; - } - } - return; - } - const inlineButtonsScope = resolveTelegramInlineButtonsScope({ cfg, accountId, From cc80495baadcd8ced64b49d7728074b0451da365 Mon Sep 17 00:00:00 2001 From: Ayaan Zaidi Date: Tue, 27 Jan 2026 13:33:04 +0530 Subject: [PATCH 5/8] fix(telegram): send sticker pixels to vision models --- src/telegram/bot-message-context.ts | 29 ++++++++++- src/telegram/bot-message-dispatch.ts | 58 +++++++++++++++------- src/telegram/sticker-cache.ts | 74 +++++++++++++++++++++++----- 3 files changed, 132 insertions(+), 29 deletions(-) diff --git a/src/telegram/bot-message-context.ts b/src/telegram/bot-message-context.ts index 71ac8a011..3f2c4af57 100644 --- a/src/telegram/bot-message-context.ts +++ b/src/telegram/bot-message-context.ts @@ -1,6 +1,12 @@ import type { Bot } from "grammy"; import { resolveAckReaction } from "../agents/identity.js"; +import { + findModelInCatalog, + loadModelCatalog, + modelSupportsVision, +} from "../agents/model-catalog.js"; +import { resolveDefaultModelForAgent } from "../agents/model-selection.js"; import { hasControlCommand } from "../auto-reply/command-detection.js"; import { normalizeCommandBody } from "../auto-reply/commands-registry.js"; import { formatInboundEnvelope, resolveEnvelopeFormatOptions } from "../auto-reply/envelope.js"; @@ -104,6 +110,24 @@ type BuildTelegramMessageContextParams = { resolveTelegramGroupConfig: ResolveTelegramGroupConfig; }; +async function resolveStickerVisionSupport(params: { + cfg: ClawdbotConfig; + agentId?: string; +}): Promise { + try { + const catalog = await loadModelCatalog({ config: params.cfg }); + const defaultModel = resolveDefaultModelForAgent({ + cfg: params.cfg, + agentId: params.agentId, + }); + const entry = findModelInCatalog(catalog, defaultModel.provider, defaultModel.model); + if (!entry) return false; + return entry.input ? modelSupportsVision(entry) : true; + } catch { + return false; + } +} + export const buildTelegramMessageContext = async ({ primaryCtx, allMedia, @@ -316,7 +340,10 @@ export const buildTelegramMessageContext = async ({ // Check if sticker has a cached description - if so, use it instead of sending the image const cachedStickerDescription = allMedia[0]?.stickerMetadata?.cachedDescription; - const stickerCacheHit = Boolean(cachedStickerDescription); + const stickerSupportsVision = msg.sticker + ? await resolveStickerVisionSupport({ cfg, agentId: route.agentId }) + : false; + const stickerCacheHit = Boolean(cachedStickerDescription) && !stickerSupportsVision; if (stickerCacheHit) { // Format cached description with sticker context const emoji = allMedia[0]?.stickerMetadata?.emoji; diff --git a/src/telegram/bot-message-dispatch.ts b/src/telegram/bot-message-dispatch.ts index a3e9c3faa..7c5929e5a 100644 --- a/src/telegram/bot-message-dispatch.ts +++ b/src/telegram/bot-message-dispatch.ts @@ -1,5 +1,11 @@ // @ts-nocheck import { EmbeddedBlockChunker } from "../agents/pi-embedded-block-chunker.js"; +import { + findModelInCatalog, + loadModelCatalog, + modelSupportsVision, +} from "../agents/model-catalog.js"; +import { resolveDefaultModelForAgent } from "../agents/model-selection.js"; import { resolveChunkMode } from "../auto-reply/chunk.js"; import { clearHistoryEntriesIfEnabled } from "../auto-reply/reply/history.js"; import { dispatchReplyWithBufferedBlockDispatcher } from "../auto-reply/reply/provider-dispatcher.js"; @@ -15,6 +21,18 @@ import { createTelegramDraftStream } from "./draft-stream.js"; import { cacheSticker, describeStickerImage } from "./sticker-cache.js"; import { resolveAgentDir } from "../agents/agent-scope.js"; +async function resolveStickerVisionSupport(cfg, agentId) { + try { + const catalog = await loadModelCatalog({ config: cfg }); + const defaultModel = resolveDefaultModelForAgent({ cfg, agentId }); + const entry = findModelInCatalog(catalog, defaultModel.provider, defaultModel.model); + if (!entry) return false; + return entry.input ? modelSupportsVision(entry) : true; + } catch { + return false; + } +} + export const dispatchTelegramMessage = async ({ context, bot, @@ -133,14 +151,18 @@ export const dispatchTelegramMessage = async ({ // Handle uncached stickers: get a dedicated vision description before dispatch // This ensures we cache a raw description rather than a conversational response const sticker = ctxPayload.Sticker; - if (sticker?.fileUniqueId && !sticker.cachedDescription && ctxPayload.MediaPath) { + if (sticker?.fileUniqueId && ctxPayload.MediaPath) { const agentDir = resolveAgentDir(cfg, route.agentId); - const description = await describeStickerImage({ - imagePath: ctxPayload.MediaPath, - cfg, - agentDir, - agentId: route.agentId, - }); + const stickerSupportsVision = await resolveStickerVisionSupport(cfg, route.agentId); + let description = sticker.cachedDescription ?? null; + if (!description) { + description = await describeStickerImage({ + imagePath: ctxPayload.MediaPath, + cfg, + agentDir, + agentId: route.agentId, + }); + } if (description) { // Format the description with sticker context const stickerContext = [sticker.emoji, sticker.setName ? `from "${sticker.setName}"` : null] @@ -148,17 +170,19 @@ export const dispatchTelegramMessage = async ({ .join(" "); const formattedDesc = `[Sticker${stickerContext ? ` ${stickerContext}` : ""}] ${description}`; - // Update context to use description instead of image sticker.cachedDescription = description; - ctxPayload.Body = formattedDesc; - ctxPayload.BodyForAgent = formattedDesc; - // Clear media paths so native vision doesn't process the image again - ctxPayload.MediaPath = undefined; - ctxPayload.MediaType = undefined; - ctxPayload.MediaUrl = undefined; - ctxPayload.MediaPaths = undefined; - ctxPayload.MediaUrls = undefined; - ctxPayload.MediaTypes = undefined; + if (!stickerSupportsVision) { + // Update context to use description instead of image + ctxPayload.Body = formattedDesc; + ctxPayload.BodyForAgent = formattedDesc; + // Clear media paths so native vision doesn't process the image again + ctxPayload.MediaPath = undefined; + ctxPayload.MediaType = undefined; + ctxPayload.MediaUrl = undefined; + ctxPayload.MediaPaths = undefined; + ctxPayload.MediaUrls = undefined; + ctxPayload.MediaTypes = undefined; + } // Cache the description for future encounters cacheSticker({ diff --git a/src/telegram/sticker-cache.ts b/src/telegram/sticker-cache.ts index 38f421851..5c517ac12 100644 --- a/src/telegram/sticker-cache.ts +++ b/src/telegram/sticker-cache.ts @@ -4,11 +4,13 @@ import type { ClawdbotConfig } from "../config/config.js"; import { STATE_DIR_CLAWDBOT } from "../config/paths.js"; import { loadJsonFile, saveJsonFile } from "../infra/json-file.js"; import { logVerbose } from "../globals.js"; +import type { ModelCatalogEntry } from "../agents/model-catalog.js"; import { findModelInCatalog, loadModelCatalog, modelSupportsVision, } from "../agents/model-catalog.js"; +import { resolveApiKeyForProvider } from "../agents/model-auth.js"; import { resolveDefaultModelForAgent } from "../agents/model-selection.js"; import { resolveAutoImageModel } from "../media-understanding/runner.js"; @@ -140,6 +142,7 @@ export function getCacheStats(): { count: number; oldestAt?: string; newestAt?: const STICKER_DESCRIPTION_PROMPT = "Describe this sticker image in 1-2 sentences. Focus on what the sticker depicts (character, object, action, emotion). Be concise and objective."; +const VISION_PROVIDERS = ["openai", "anthropic", "google", "minimax"] as const; export interface DescribeStickerParams { imagePath: string; @@ -158,31 +161,80 @@ export async function describeStickerImage(params: DescribeStickerParams): Promi const defaultModel = resolveDefaultModelForAgent({ cfg, agentId }); let activeModel = undefined as { provider: string; model: string } | undefined; + let catalog: ModelCatalogEntry[] = []; try { - const catalog = await loadModelCatalog({ config: cfg }); + catalog = await loadModelCatalog({ config: cfg }); const entry = findModelInCatalog(catalog, defaultModel.provider, defaultModel.model); - if (modelSupportsVision(entry)) { + const supportsVision = entry?.input ? modelSupportsVision(entry) : Boolean(entry); + if (supportsVision) { activeModel = { provider: defaultModel.provider, model: defaultModel.model }; } } catch { // Ignore catalog failures; fall back to auto selection. } - const resolved = await resolveAutoImageModel({ - cfg, - agentDir, - activeModel, - }); + const hasProviderKey = async (provider: string) => { + try { + await resolveApiKeyForProvider({ provider, cfg, agentDir }); + return true; + } catch { + return false; + } + }; + + const selectCatalogModel = (provider: string) => { + const entries = catalog.filter( + (entry) => + entry.provider.toLowerCase() === provider.toLowerCase() && + (entry.input ? modelSupportsVision(entry) : true), + ); + if (entries.length === 0) return undefined; + const defaultId = + provider === "openai" + ? "gpt-5-mini" + : provider === "anthropic" + ? "claude-opus-4-5" + : provider === "google" + ? "gemini-3-flash-preview" + : "MiniMax-VL-01"; + const preferred = entries.find((entry) => entry.id === defaultId); + return preferred ?? entries[0]; + }; + + let resolved = null as { provider: string; model?: string } | null; + if ( + activeModel && + VISION_PROVIDERS.includes(activeModel.provider as (typeof VISION_PROVIDERS)[number]) && + (await hasProviderKey(activeModel.provider)) + ) { + resolved = activeModel; + } + if (!resolved) { + for (const provider of VISION_PROVIDERS) { + if (!(await hasProviderKey(provider))) continue; + const entry = selectCatalogModel(provider); + if (entry) { + resolved = { provider, model: entry.id }; + break; + } + } + } + + if (!resolved) { + resolved = await resolveAutoImageModel({ + cfg, + agentDir, + activeModel, + }); + } + + if (!resolved?.model) { logVerbose("telegram: no vision provider available for sticker description"); return null; } const { provider, model } = resolved; - if (!model) { - logVerbose(`telegram: no vision model available for ${provider}`); - return null; - } logVerbose(`telegram: describing sticker with ${provider}/${model}`); try { From a49250fffc3f1e44c3d0de381cdc5f42dece36a6 Mon Sep 17 00:00:00 2001 From: Ayaan Zaidi Date: Tue, 27 Jan 2026 13:33:45 +0530 Subject: [PATCH 6/8] docs: add changelog for #2650 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 749a6c660..d7eb7f4c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,7 @@ Status: unreleased. - Telegram: add optional silent send flag (disable notifications). (#2382) Thanks @Suksham-sharma. - Telegram: support editing sent messages via message(action="edit"). (#2394) Thanks @marcelomar21. - Telegram: add sticker receive/send with vision caching. (#2629) Thanks @longjos. +- Telegram: send sticker pixels to vision models. (#2650) - Config: apply config.env before ${VAR} substitution. (#1813) Thanks @spanishflu-est1918. - Slack: clear ack reaction after streamed replies. (#2044) Thanks @fancyboi999. - macOS: keep custom SSH usernames in remote target. (#2046) Thanks @algal. From d7a00dc823d83e1ce3314492e7df9605602de524 Mon Sep 17 00:00:00 2001 From: Ayaan Zaidi Date: Tue, 27 Jan 2026 13:42:40 +0530 Subject: [PATCH 7/8] fix: gate sticker vision on image input --- src/telegram/bot-message-context.ts | 2 +- src/telegram/bot-message-dispatch.ts | 2 +- src/telegram/sticker-cache.ts | 5 ++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/telegram/bot-message-context.ts b/src/telegram/bot-message-context.ts index 3f2c4af57..f978be7c2 100644 --- a/src/telegram/bot-message-context.ts +++ b/src/telegram/bot-message-context.ts @@ -122,7 +122,7 @@ async function resolveStickerVisionSupport(params: { }); const entry = findModelInCatalog(catalog, defaultModel.provider, defaultModel.model); if (!entry) return false; - return entry.input ? modelSupportsVision(entry) : true; + return modelSupportsVision(entry); } catch { return false; } diff --git a/src/telegram/bot-message-dispatch.ts b/src/telegram/bot-message-dispatch.ts index 7c5929e5a..27c6a3bfa 100644 --- a/src/telegram/bot-message-dispatch.ts +++ b/src/telegram/bot-message-dispatch.ts @@ -27,7 +27,7 @@ async function resolveStickerVisionSupport(cfg, agentId) { const defaultModel = resolveDefaultModelForAgent({ cfg, agentId }); const entry = findModelInCatalog(catalog, defaultModel.provider, defaultModel.model); if (!entry) return false; - return entry.input ? modelSupportsVision(entry) : true; + return modelSupportsVision(entry); } catch { return false; } diff --git a/src/telegram/sticker-cache.ts b/src/telegram/sticker-cache.ts index 5c517ac12..ab322e59e 100644 --- a/src/telegram/sticker-cache.ts +++ b/src/telegram/sticker-cache.ts @@ -165,7 +165,7 @@ export async function describeStickerImage(params: DescribeStickerParams): Promi try { catalog = await loadModelCatalog({ config: cfg }); const entry = findModelInCatalog(catalog, defaultModel.provider, defaultModel.model); - const supportsVision = entry?.input ? modelSupportsVision(entry) : Boolean(entry); + const supportsVision = modelSupportsVision(entry); if (supportsVision) { activeModel = { provider: defaultModel.provider, model: defaultModel.model }; } @@ -185,8 +185,7 @@ export async function describeStickerImage(params: DescribeStickerParams): Promi const selectCatalogModel = (provider: string) => { const entries = catalog.filter( (entry) => - entry.provider.toLowerCase() === provider.toLowerCase() && - (entry.input ? modelSupportsVision(entry) : true), + entry.provider.toLowerCase() === provider.toLowerCase() && modelSupportsVision(entry), ); if (entries.length === 0) return undefined; const defaultId = From 72fea5e305bedd46e908473c1a8c5e050f1f28a1 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 27 Jan 2026 09:10:21 +0000 Subject: [PATCH 8/8] chore: bump version to 2026.1.26 --- CHANGELOG.md | 3 ++- apps/android/app/build.gradle.kts | 4 ++-- apps/ios/Sources/Info.plist | 4 ++-- apps/ios/Tests/Info.plist | 4 ++-- apps/ios/project.yml | 8 ++++---- apps/macos/Sources/Clawdbot/Resources/Info.plist | 4 ++-- docs/platforms/fly.md | 2 +- docs/platforms/mac/release.md | 14 +++++++------- docs/reference/RELEASING.md | 2 +- extensions/bluebubbles/package.json | 6 +++--- extensions/copilot-proxy/package.json | 4 ++-- extensions/diagnostics-otel/package.json | 4 ++-- extensions/discord/package.json | 4 ++-- extensions/google-antigravity-auth/package.json | 4 ++-- extensions/google-gemini-cli-auth/package.json | 4 ++-- extensions/googlechat/package.json | 8 ++++---- extensions/imessage/package.json | 4 ++-- extensions/line/package.json | 6 +++--- extensions/llm-task/package.json | 4 ++-- extensions/lobster/package.json | 4 ++-- extensions/matrix/package.json | 6 +++--- extensions/mattermost/package.json | 6 +++--- extensions/memory-core/package.json | 6 +++--- extensions/memory-lancedb/package.json | 4 ++-- extensions/msteams/package.json | 6 +++--- extensions/nextcloud-talk/package.json | 6 +++--- extensions/nostr/package.json | 6 +++--- extensions/open-prose/package.json | 4 ++-- extensions/signal/package.json | 4 ++-- extensions/slack/package.json | 4 ++-- extensions/telegram/package.json | 4 ++-- extensions/tlon/package.json | 6 +++--- extensions/twitch/package.json | 4 ++-- extensions/voice-call/CHANGELOG.md | 2 +- extensions/voice-call/package.json | 4 ++-- extensions/whatsapp/package.json | 4 ++-- extensions/zalo/package.json | 6 +++--- extensions/zalouser/package.json | 6 +++--- package.json | 9 ++++++--- packages/clawdbot/package.json | 16 ++++++++++++++++ 40 files changed, 115 insertions(+), 95 deletions(-) create mode 100644 packages/clawdbot/package.json diff --git a/CHANGELOG.md b/CHANGELOG.md index d7eb7f4c4..447e77846 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,11 @@ Docs: https://docs.clawd.bot -## 2026.1.25 +## 2026.1.26 Status: unreleased. ### Changes +- Rebrand: rename the npm package/CLI to `moltbot`, add a `clawdbot` compatibility shim, and move extensions to the `@moltbot/*` scope. - Commands: group /help and /commands output with Telegram paging. (#2504) Thanks @hougangdev. - macOS: limit project-local `node_modules/.bin` PATH preference to debug builds (reduce PATH hijacking risk). - Tools: add per-sender group tool policies and fix precedence. (#1757) Thanks @adam91holt. diff --git a/apps/android/app/build.gradle.kts b/apps/android/app/build.gradle.kts index a015c0e36..85dc9c566 100644 --- a/apps/android/app/build.gradle.kts +++ b/apps/android/app/build.gradle.kts @@ -21,8 +21,8 @@ android { applicationId = "com.clawdbot.android" minSdk = 31 targetSdk = 36 - versionCode = 202601250 - versionName = "2026.1.25" + versionCode = 202601260 + versionName = "2026.1.26" } buildTypes { diff --git a/apps/ios/Sources/Info.plist b/apps/ios/Sources/Info.plist index e1cf2b71d..fb5212e59 100644 --- a/apps/ios/Sources/Info.plist +++ b/apps/ios/Sources/Info.plist @@ -19,9 +19,9 @@ CFBundlePackageType APPL CFBundleShortVersionString - 2026.1.25 + 2026.1.26 CFBundleVersion - 20260125 + 20260126 NSAppTransportSecurity NSAllowsArbitraryLoadsInWebContent diff --git a/apps/ios/Tests/Info.plist b/apps/ios/Tests/Info.plist index 6ff977b05..7a6bc5cec 100644 --- a/apps/ios/Tests/Info.plist +++ b/apps/ios/Tests/Info.plist @@ -17,8 +17,8 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 2026.1.25 + 2026.1.26 CFBundleVersion - 20260125 + 20260126 diff --git a/apps/ios/project.yml b/apps/ios/project.yml index 0073b4ef9..c955bce24 100644 --- a/apps/ios/project.yml +++ b/apps/ios/project.yml @@ -81,8 +81,8 @@ targets: properties: CFBundleDisplayName: Clawdbot CFBundleIconName: AppIcon - CFBundleShortVersionString: "2026.1.25" - CFBundleVersion: "20260125" + CFBundleShortVersionString: "2026.1.26" + CFBundleVersion: "20260126" UILaunchScreen: {} UIApplicationSceneManifest: UIApplicationSupportsMultipleScenes: false @@ -130,5 +130,5 @@ targets: path: Tests/Info.plist properties: CFBundleDisplayName: ClawdbotTests - CFBundleShortVersionString: "2026.1.25" - CFBundleVersion: "20260125" + CFBundleShortVersionString: "2026.1.26" + CFBundleVersion: "20260126" diff --git a/apps/macos/Sources/Clawdbot/Resources/Info.plist b/apps/macos/Sources/Clawdbot/Resources/Info.plist index ee9e3113d..c3031805e 100644 --- a/apps/macos/Sources/Clawdbot/Resources/Info.plist +++ b/apps/macos/Sources/Clawdbot/Resources/Info.plist @@ -15,9 +15,9 @@ CFBundlePackageType APPL CFBundleShortVersionString - 2026.1.25 + 2026.1.26 CFBundleVersion - 202601250 + 202601260 CFBundleIconFile Clawdbot CFBundleURLTypes diff --git a/docs/platforms/fly.md b/docs/platforms/fly.md index dee731ea7..a9c8bafb4 100644 --- a/docs/platforms/fly.md +++ b/docs/platforms/fly.md @@ -185,7 +185,7 @@ cat > /data/clawdbot.json << 'EOF' "bind": "auto" }, "meta": { - "lastTouchedVersion": "2026.1.25" + "lastTouchedVersion": "2026.1.26" } } EOF diff --git a/docs/platforms/mac/release.md b/docs/platforms/mac/release.md index d3bfd02c3..b1226a5ad 100644 --- a/docs/platforms/mac/release.md +++ b/docs/platforms/mac/release.md @@ -30,17 +30,17 @@ Notes: # From repo root; set release IDs so Sparkle feed is enabled. # APP_BUILD must be numeric + monotonic for Sparkle compare. BUNDLE_ID=com.clawdbot.mac \ -APP_VERSION=2026.1.25 \ +APP_VERSION=2026.1.26 \ APP_BUILD="$(git rev-list --count HEAD)" \ BUILD_CONFIG=release \ SIGN_IDENTITY="Developer ID Application: ()" \ scripts/package-mac-app.sh # Zip for distribution (includes resource forks for Sparkle delta support) -ditto -c -k --sequesterRsrc --keepParent dist/Clawdbot.app dist/Clawdbot-2026.1.25.zip +ditto -c -k --sequesterRsrc --keepParent dist/Clawdbot.app dist/Clawdbot-2026.1.26.zip # Optional: also build a styled DMG for humans (drag to /Applications) -scripts/create-dmg.sh dist/Clawdbot.app dist/Clawdbot-2026.1.25.dmg +scripts/create-dmg.sh dist/Clawdbot.app dist/Clawdbot-2026.1.26.dmg # Recommended: build + notarize/staple zip + DMG # First, create a keychain profile once: @@ -48,26 +48,26 @@ scripts/create-dmg.sh dist/Clawdbot.app dist/Clawdbot-2026.1.25.dmg # --apple-id "" --team-id "" --password "" NOTARIZE=1 NOTARYTOOL_PROFILE=clawdbot-notary \ BUNDLE_ID=com.clawdbot.mac \ -APP_VERSION=2026.1.25 \ +APP_VERSION=2026.1.26 \ APP_BUILD="$(git rev-list --count HEAD)" \ BUILD_CONFIG=release \ SIGN_IDENTITY="Developer ID Application: ()" \ scripts/package-mac-dist.sh # Optional: ship dSYM alongside the release -ditto -c -k --keepParent apps/macos/.build/release/Clawdbot.app.dSYM dist/Clawdbot-2026.1.25.dSYM.zip +ditto -c -k --keepParent apps/macos/.build/release/Clawdbot.app.dSYM dist/Clawdbot-2026.1.26.dSYM.zip ``` ## Appcast entry Use the release note generator so Sparkle renders formatted HTML notes: ```bash -SPARKLE_PRIVATE_KEY_FILE=/path/to/ed25519-private-key scripts/make_appcast.sh dist/Clawdbot-2026.1.25.zip https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml +SPARKLE_PRIVATE_KEY_FILE=/path/to/ed25519-private-key scripts/make_appcast.sh dist/Clawdbot-2026.1.26.zip https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml ``` Generates HTML release notes from `CHANGELOG.md` (via [`scripts/changelog-to-html.sh`](https://github.com/clawdbot/clawdbot/blob/main/scripts/changelog-to-html.sh)) and embeds them in the appcast entry. Commit the updated `appcast.xml` alongside the release assets (zip + dSYM) when publishing. ## Publish & verify -- Upload `Clawdbot-2026.1.25.zip` (and `Clawdbot-2026.1.25.dSYM.zip`) to the GitHub release for tag `v2026.1.25`. +- Upload `Clawdbot-2026.1.26.zip` (and `Clawdbot-2026.1.26.dSYM.zip`) to the GitHub release for tag `v2026.1.26`. - Ensure the raw appcast URL matches the baked feed: `https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml`. - Sanity checks: - `curl -I https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml` returns 200. diff --git a/docs/reference/RELEASING.md b/docs/reference/RELEASING.md index 244757a48..a4c68e3e9 100644 --- a/docs/reference/RELEASING.md +++ b/docs/reference/RELEASING.md @@ -17,7 +17,7 @@ When the operator says “release”, immediately do this preflight (no extra qu - Use Sparkle keys from `~/Library/CloudStorage/Dropbox/Backup/Sparkle` if needed. 1) **Version & metadata** -- [ ] Bump `package.json` version (e.g., `2026.1.25`). +- [ ] Bump `package.json` version (e.g., `2026.1.26`). - [ ] Run `pnpm plugins:sync` to align extension package versions + changelogs. - [ ] Update CLI/version strings: [`src/cli/program.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/cli/program.ts) and the Baileys user agent in [`src/provider-web.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/provider-web.ts). - [ ] Confirm package metadata (name, description, repository, keywords, license) and `bin` map points to [`dist/entry.js`](https://github.com/clawdbot/clawdbot/blob/main/dist/entry.js) for `clawdbot`. diff --git a/extensions/bluebubbles/package.json b/extensions/bluebubbles/package.json index 7d82036a0..3f9b5995e 100644 --- a/extensions/bluebubbles/package.json +++ b/extensions/bluebubbles/package.json @@ -1,6 +1,6 @@ { - "name": "@clawdbot/bluebubbles", - "version": "2026.1.25", + "name": "@moltbot/bluebubbles", + "version": "2026.1.26", "type": "module", "description": "Clawdbot BlueBubbles channel plugin", "clawdbot": { @@ -25,7 +25,7 @@ "order": 75 }, "install": { - "npmSpec": "@clawdbot/bluebubbles", + "npmSpec": "@moltbot/bluebubbles", "localPath": "extensions/bluebubbles", "defaultChoice": "npm" } diff --git a/extensions/copilot-proxy/package.json b/extensions/copilot-proxy/package.json index 2a9a63c71..16e76ba6a 100644 --- a/extensions/copilot-proxy/package.json +++ b/extensions/copilot-proxy/package.json @@ -1,6 +1,6 @@ { - "name": "@clawdbot/copilot-proxy", - "version": "2026.1.25", + "name": "@moltbot/copilot-proxy", + "version": "2026.1.26", "type": "module", "description": "Clawdbot Copilot Proxy provider plugin", "clawdbot": { diff --git a/extensions/diagnostics-otel/package.json b/extensions/diagnostics-otel/package.json index 65a6bf0cd..f6fb4aea6 100644 --- a/extensions/diagnostics-otel/package.json +++ b/extensions/diagnostics-otel/package.json @@ -1,6 +1,6 @@ { - "name": "@clawdbot/diagnostics-otel", - "version": "2026.1.25", + "name": "@moltbot/diagnostics-otel", + "version": "2026.1.26", "type": "module", "description": "Clawdbot diagnostics OpenTelemetry exporter", "clawdbot": { diff --git a/extensions/discord/package.json b/extensions/discord/package.json index 90a99d4d3..b5b1c9a30 100644 --- a/extensions/discord/package.json +++ b/extensions/discord/package.json @@ -1,6 +1,6 @@ { - "name": "@clawdbot/discord", - "version": "2026.1.25", + "name": "@moltbot/discord", + "version": "2026.1.26", "type": "module", "description": "Clawdbot Discord channel plugin", "clawdbot": { diff --git a/extensions/google-antigravity-auth/package.json b/extensions/google-antigravity-auth/package.json index f1d8f86bd..6ccd2157b 100644 --- a/extensions/google-antigravity-auth/package.json +++ b/extensions/google-antigravity-auth/package.json @@ -1,6 +1,6 @@ { - "name": "@clawdbot/google-antigravity-auth", - "version": "2026.1.25", + "name": "@moltbot/google-antigravity-auth", + "version": "2026.1.26", "type": "module", "description": "Clawdbot Google Antigravity OAuth provider plugin", "clawdbot": { diff --git a/extensions/google-gemini-cli-auth/package.json b/extensions/google-gemini-cli-auth/package.json index 7e3fef15b..1aa094a0a 100644 --- a/extensions/google-gemini-cli-auth/package.json +++ b/extensions/google-gemini-cli-auth/package.json @@ -1,6 +1,6 @@ { - "name": "@clawdbot/google-gemini-cli-auth", - "version": "2026.1.25", + "name": "@moltbot/google-gemini-cli-auth", + "version": "2026.1.26", "type": "module", "description": "Clawdbot Gemini CLI OAuth provider plugin", "clawdbot": { diff --git a/extensions/googlechat/package.json b/extensions/googlechat/package.json index af1ccf8e1..fd77bc189 100644 --- a/extensions/googlechat/package.json +++ b/extensions/googlechat/package.json @@ -1,6 +1,6 @@ { - "name": "@clawdbot/googlechat", - "version": "2026.1.25", + "name": "@moltbot/googlechat", + "version": "2026.1.26", "type": "module", "description": "Clawdbot Google Chat channel plugin", "clawdbot": { @@ -22,7 +22,7 @@ "order": 55 }, "install": { - "npmSpec": "@clawdbot/googlechat", + "npmSpec": "@moltbot/googlechat", "localPath": "extensions/googlechat", "defaultChoice": "npm" } @@ -34,6 +34,6 @@ "clawdbot": "workspace:*" }, "peerDependencies": { - "clawdbot": ">=2026.1.25" + "clawdbot": ">=2026.1.26" } } diff --git a/extensions/imessage/package.json b/extensions/imessage/package.json index 944ad06bf..4efab972f 100644 --- a/extensions/imessage/package.json +++ b/extensions/imessage/package.json @@ -1,6 +1,6 @@ { - "name": "@clawdbot/imessage", - "version": "2026.1.25", + "name": "@moltbot/imessage", + "version": "2026.1.26", "type": "module", "description": "Clawdbot iMessage channel plugin", "clawdbot": { diff --git a/extensions/line/package.json b/extensions/line/package.json index 346d66415..b08c56b69 100644 --- a/extensions/line/package.json +++ b/extensions/line/package.json @@ -1,6 +1,6 @@ { - "name": "@clawdbot/line", - "version": "2026.1.25", + "name": "@moltbot/line", + "version": "2026.1.26", "type": "module", "description": "Clawdbot LINE channel plugin", "clawdbot": { @@ -18,7 +18,7 @@ "quickstartAllowFrom": true }, "install": { - "npmSpec": "@clawdbot/line", + "npmSpec": "@moltbot/line", "localPath": "extensions/line", "defaultChoice": "npm" } diff --git a/extensions/llm-task/package.json b/extensions/llm-task/package.json index d6bfbb31d..f8f65df37 100644 --- a/extensions/llm-task/package.json +++ b/extensions/llm-task/package.json @@ -1,6 +1,6 @@ { - "name": "@clawdbot/llm-task", - "version": "2026.1.25", + "name": "@moltbot/llm-task", + "version": "2026.1.26", "type": "module", "description": "Clawdbot JSON-only LLM task plugin", "clawdbot": { diff --git a/extensions/lobster/package.json b/extensions/lobster/package.json index b73dbac69..2640f0135 100644 --- a/extensions/lobster/package.json +++ b/extensions/lobster/package.json @@ -1,6 +1,6 @@ { - "name": "@clawdbot/lobster", - "version": "2026.1.25", + "name": "@moltbot/lobster", + "version": "2026.1.26", "type": "module", "description": "Lobster workflow tool plugin (typed pipelines + resumable approvals)", "clawdbot": { diff --git a/extensions/matrix/package.json b/extensions/matrix/package.json index 625c92df0..9c17962fe 100644 --- a/extensions/matrix/package.json +++ b/extensions/matrix/package.json @@ -1,6 +1,6 @@ { - "name": "@clawdbot/matrix", - "version": "2026.1.25", + "name": "@moltbot/matrix", + "version": "2026.1.26", "type": "module", "description": "Clawdbot Matrix channel plugin", "clawdbot": { @@ -18,7 +18,7 @@ "quickstartAllowFrom": true }, "install": { - "npmSpec": "@clawdbot/matrix", + "npmSpec": "@moltbot/matrix", "localPath": "extensions/matrix", "defaultChoice": "npm" } diff --git a/extensions/mattermost/package.json b/extensions/mattermost/package.json index 60c02d50f..5244710bc 100644 --- a/extensions/mattermost/package.json +++ b/extensions/mattermost/package.json @@ -1,6 +1,6 @@ { - "name": "@clawdbot/mattermost", - "version": "2026.1.25", + "name": "@moltbot/mattermost", + "version": "2026.1.26", "type": "module", "description": "Clawdbot Mattermost channel plugin", "clawdbot": { @@ -17,7 +17,7 @@ "order": 65 }, "install": { - "npmSpec": "@clawdbot/mattermost", + "npmSpec": "@moltbot/mattermost", "localPath": "extensions/mattermost", "defaultChoice": "npm" } diff --git a/extensions/memory-core/package.json b/extensions/memory-core/package.json index af6a3f9cd..307d5b208 100644 --- a/extensions/memory-core/package.json +++ b/extensions/memory-core/package.json @@ -1,6 +1,6 @@ { - "name": "@clawdbot/memory-core", - "version": "2026.1.25", + "name": "@moltbot/memory-core", + "version": "2026.1.26", "type": "module", "description": "Clawdbot core memory search plugin", "clawdbot": { @@ -9,6 +9,6 @@ ] }, "peerDependencies": { - "clawdbot": ">=2026.1.24-3" + "clawdbot": ">=2026.1.26" } } diff --git a/extensions/memory-lancedb/package.json b/extensions/memory-lancedb/package.json index e003f5890..a443687dc 100644 --- a/extensions/memory-lancedb/package.json +++ b/extensions/memory-lancedb/package.json @@ -1,6 +1,6 @@ { - "name": "@clawdbot/memory-lancedb", - "version": "2026.1.25", + "name": "@moltbot/memory-lancedb", + "version": "2026.1.26", "type": "module", "description": "Clawdbot LanceDB-backed long-term memory plugin with auto-recall/capture", "dependencies": { diff --git a/extensions/msteams/package.json b/extensions/msteams/package.json index b94f8e76a..29a6cdcdd 100644 --- a/extensions/msteams/package.json +++ b/extensions/msteams/package.json @@ -1,6 +1,6 @@ { - "name": "@clawdbot/msteams", - "version": "2026.1.25", + "name": "@moltbot/msteams", + "version": "2026.1.26", "type": "module", "description": "Clawdbot Microsoft Teams channel plugin", "clawdbot": { @@ -20,7 +20,7 @@ "order": 60 }, "install": { - "npmSpec": "@clawdbot/msteams", + "npmSpec": "@moltbot/msteams", "localPath": "extensions/msteams", "defaultChoice": "npm" } diff --git a/extensions/nextcloud-talk/package.json b/extensions/nextcloud-talk/package.json index 2da3f3b2a..aa96bad9a 100644 --- a/extensions/nextcloud-talk/package.json +++ b/extensions/nextcloud-talk/package.json @@ -1,6 +1,6 @@ { - "name": "@clawdbot/nextcloud-talk", - "version": "2026.1.25", + "name": "@moltbot/nextcloud-talk", + "version": "2026.1.26", "type": "module", "description": "Clawdbot Nextcloud Talk channel plugin", "clawdbot": { @@ -22,7 +22,7 @@ "quickstartAllowFrom": true }, "install": { - "npmSpec": "@clawdbot/nextcloud-talk", + "npmSpec": "@moltbot/nextcloud-talk", "localPath": "extensions/nextcloud-talk", "defaultChoice": "npm" } diff --git a/extensions/nostr/package.json b/extensions/nostr/package.json index b2fb4b799..bde398392 100644 --- a/extensions/nostr/package.json +++ b/extensions/nostr/package.json @@ -1,6 +1,6 @@ { - "name": "@clawdbot/nostr", - "version": "2026.1.25", + "name": "@moltbot/nostr", + "version": "2026.1.26", "type": "module", "description": "Clawdbot Nostr channel plugin for NIP-04 encrypted DMs", "clawdbot": { @@ -18,7 +18,7 @@ "quickstartAllowFrom": true }, "install": { - "npmSpec": "@clawdbot/nostr", + "npmSpec": "@moltbot/nostr", "localPath": "extensions/nostr", "defaultChoice": "npm" } diff --git a/extensions/open-prose/package.json b/extensions/open-prose/package.json index 052201205..2637a55f7 100644 --- a/extensions/open-prose/package.json +++ b/extensions/open-prose/package.json @@ -1,6 +1,6 @@ { - "name": "@clawdbot/open-prose", - "version": "2026.1.25", + "name": "@moltbot/open-prose", + "version": "2026.1.26", "type": "module", "description": "OpenProse VM skill pack plugin (slash command + telemetry).", "clawdbot": { diff --git a/extensions/signal/package.json b/extensions/signal/package.json index 65948eb7b..b598bf996 100644 --- a/extensions/signal/package.json +++ b/extensions/signal/package.json @@ -1,6 +1,6 @@ { - "name": "@clawdbot/signal", - "version": "2026.1.25", + "name": "@moltbot/signal", + "version": "2026.1.26", "type": "module", "description": "Clawdbot Signal channel plugin", "clawdbot": { diff --git a/extensions/slack/package.json b/extensions/slack/package.json index 5bd452d2e..8f0c7531c 100644 --- a/extensions/slack/package.json +++ b/extensions/slack/package.json @@ -1,6 +1,6 @@ { - "name": "@clawdbot/slack", - "version": "2026.1.25", + "name": "@moltbot/slack", + "version": "2026.1.26", "type": "module", "description": "Clawdbot Slack channel plugin", "clawdbot": { diff --git a/extensions/telegram/package.json b/extensions/telegram/package.json index 64d3d7dea..149517e46 100644 --- a/extensions/telegram/package.json +++ b/extensions/telegram/package.json @@ -1,6 +1,6 @@ { - "name": "@clawdbot/telegram", - "version": "2026.1.25", + "name": "@moltbot/telegram", + "version": "2026.1.26", "type": "module", "description": "Clawdbot Telegram channel plugin", "clawdbot": { diff --git a/extensions/tlon/package.json b/extensions/tlon/package.json index 06750126d..e1382e96b 100644 --- a/extensions/tlon/package.json +++ b/extensions/tlon/package.json @@ -1,6 +1,6 @@ { - "name": "@clawdbot/tlon", - "version": "2026.1.25", + "name": "@moltbot/tlon", + "version": "2026.1.26", "type": "module", "description": "Clawdbot Tlon/Urbit channel plugin", "clawdbot": { @@ -18,7 +18,7 @@ "quickstartAllowFrom": true }, "install": { - "npmSpec": "@clawdbot/tlon", + "npmSpec": "@moltbot/tlon", "localPath": "extensions/tlon", "defaultChoice": "npm" } diff --git a/extensions/twitch/package.json b/extensions/twitch/package.json index 2c9dd2683..1931a2979 100644 --- a/extensions/twitch/package.json +++ b/extensions/twitch/package.json @@ -1,6 +1,6 @@ { - "name": "@clawdbot/twitch", - "version": "2026.1.23", + "name": "@moltbot/twitch", + "version": "2026.1.26", "description": "Clawdbot Twitch channel plugin", "type": "module", "dependencies": { diff --git a/extensions/voice-call/CHANGELOG.md b/extensions/voice-call/CHANGELOG.md index 588817858..a757abe64 100644 --- a/extensions/voice-call/CHANGELOG.md +++ b/extensions/voice-call/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## 2026.1.25 +## 2026.1.26 ### Changes - Breaking: voice-call TTS now uses core `messages.tts` (plugin TTS config deep‑merges with core). diff --git a/extensions/voice-call/package.json b/extensions/voice-call/package.json index 31b171f76..4d2fee875 100644 --- a/extensions/voice-call/package.json +++ b/extensions/voice-call/package.json @@ -1,6 +1,6 @@ { - "name": "@clawdbot/voice-call", - "version": "2026.1.25", + "name": "@moltbot/voice-call", + "version": "2026.1.26", "type": "module", "description": "Clawdbot voice-call plugin", "dependencies": { diff --git a/extensions/whatsapp/package.json b/extensions/whatsapp/package.json index b7b57eb51..c8aef82ff 100644 --- a/extensions/whatsapp/package.json +++ b/extensions/whatsapp/package.json @@ -1,6 +1,6 @@ { - "name": "@clawdbot/whatsapp", - "version": "2026.1.25", + "name": "@moltbot/whatsapp", + "version": "2026.1.26", "type": "module", "description": "Clawdbot WhatsApp channel plugin", "clawdbot": { diff --git a/extensions/zalo/package.json b/extensions/zalo/package.json index 8f077a6b3..b011c9e89 100644 --- a/extensions/zalo/package.json +++ b/extensions/zalo/package.json @@ -1,6 +1,6 @@ { - "name": "@clawdbot/zalo", - "version": "2026.1.25", + "name": "@moltbot/zalo", + "version": "2026.1.26", "type": "module", "description": "Clawdbot Zalo channel plugin", "clawdbot": { @@ -21,7 +21,7 @@ "quickstartAllowFrom": true }, "install": { - "npmSpec": "@clawdbot/zalo", + "npmSpec": "@moltbot/zalo", "localPath": "extensions/zalo", "defaultChoice": "npm" } diff --git a/extensions/zalouser/package.json b/extensions/zalouser/package.json index 0ab93d1ce..90077bafd 100644 --- a/extensions/zalouser/package.json +++ b/extensions/zalouser/package.json @@ -1,6 +1,6 @@ { - "name": "@clawdbot/zalouser", - "version": "2026.1.25", + "name": "@moltbot/zalouser", + "version": "2026.1.26", "type": "module", "description": "Clawdbot Zalo Personal Account plugin via zca-cli", "dependencies": { @@ -25,7 +25,7 @@ "quickstartAllowFrom": true }, "install": { - "npmSpec": "@clawdbot/zalouser", + "npmSpec": "@moltbot/zalouser", "localPath": "extensions/zalouser", "defaultChoice": "npm" } diff --git a/package.json b/package.json index 1a6d65178..6a88df982 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,17 @@ { - "name": "clawdbot", - "version": "2026.1.25", + "name": "moltbot", + "version": "2026.1.26", "description": "WhatsApp gateway CLI (Baileys web) with Pi RPC agent", "type": "module", "main": "dist/index.js", "exports": { ".": "./dist/index.js", "./plugin-sdk": "./dist/plugin-sdk/index.js", - "./plugin-sdk/*": "./dist/plugin-sdk/*" + "./plugin-sdk/*": "./dist/plugin-sdk/*", + "./cli-entry": "./dist/entry.js" }, "bin": { + "moltbot": "dist/entry.js", "clawdbot": "dist/entry.js" }, "files": [ @@ -90,6 +92,7 @@ "ui:build": "node scripts/ui.js build", "start": "node scripts/run-node.mjs", "clawdbot": "node scripts/run-node.mjs", + "moltbot": "node scripts/run-node.mjs", "gateway:watch": "node scripts/watch-node.mjs gateway --force", "gateway:dev": "CLAWDBOT_SKIP_CHANNELS=1 node scripts/run-node.mjs --dev gateway", "gateway:dev:reset": "CLAWDBOT_SKIP_CHANNELS=1 node scripts/run-node.mjs --dev gateway --reset", diff --git a/packages/clawdbot/package.json b/packages/clawdbot/package.json new file mode 100644 index 000000000..dad75eda4 --- /dev/null +++ b/packages/clawdbot/package.json @@ -0,0 +1,16 @@ +{ + "name": "clawdbot", + "version": "2026.1.26", + "type": "module", + "description": "Compatibility shim that forwards to moltbot", + "exports": { + ".": "./index.js", + "./plugin-sdk": "./plugin-sdk/index.js" + }, + "bin": { + "clawdbot": "./bin/clawdbot.js" + }, + "dependencies": { + "moltbot": "workspace:*" + } +}