diff --git a/apps/broker/src/telegram-ai.ts b/apps/broker/src/telegram-ai.ts index 8ce7e10..a2e76fa 100644 --- a/apps/broker/src/telegram-ai.ts +++ b/apps/broker/src/telegram-ai.ts @@ -225,24 +225,24 @@ export function formatConfirmation(toolCall: AiToolCall): string { switch (name) { case "send_message": - return `📤 *Send message to ${escMd(String(input.to))}:*\n\n"${escMd(String(input.message))}"\n\nPriority: ${input.priority ?? "next"}`; + return `📤 Send message to ${escHtml(String(input.to))}:\n\n"${escHtml(String(input.message))}"\n\nPriority: ${input.priority ?? "next"}`; case "create_mesh": - return `🔧 *Create mesh:*\n\nName: ${escMd(String(input.name))}`; + return `🔧 Create mesh:\n\nName: ${escHtml(String(input.name))}`; case "share_mesh": return input.email - ? `📧 *Send invite to:*\n\n${escMd(String(input.email))}` - : `🔗 *Generate invite link*`; + ? `📧 Send invite to:\n\n${escHtml(String(input.email))}` + : `🔗 Generate invite link`; case "set_state": - return `📝 *Set state:*\n\n\`${escMd(String(input.key))}\` = \`${escMd(String(input.value))}\``; + return `📝 Set state:\n\n${escHtml(String(input.key))} = ${escHtml(String(input.value))}`; case "remember": - return `💾 *Remember:*\n\n"${escMd(String(input.content))}"${input.tags ? `\nTags: ${(input.tags as string[]).join(", ")}` : ""}`; + return `💾 Remember:\n\n"${escHtml(String(input.content))}"${input.tags ? `\nTags: ${(input.tags as string[]).join(", ")}` : ""}`; default: - return `⚙️ *${name}:*\n\n${JSON.stringify(input, null, 2)}`; + return `⚙️ ${escHtml(name)}:\n\n
${escHtml(JSON.stringify(input, null, 2))}
`; } } @@ -256,33 +256,33 @@ export function formatResult(toolName: string, result: unknown): string { case "list_peers": { const peers = result as Array<{ displayName: string; status: string; summary?: string }>; - if (!peers || peers.length === 0) return "No peers online\\."; - return "👥 *Online peers:*\n\n" + peers.map(p => { + if (!peers || peers.length === 0) return "No peers online."; + return "👥 Online peers:\n\n" + peers.map(p => { const icon = p.status === "idle" ? "🟢" : p.status === "working" ? "🟡" : p.status === "dnd" ? "🔴" : "⚪"; - return `${icon} *${escMd(p.displayName)}*${p.summary ? ` — ${escMd(p.summary)}` : ""}`; + return `${icon} ${escHtml(p.displayName)}${p.summary ? ` — ${escHtml(p.summary)}` : ""}`; }).join("\n"); } case "list_meshes": { const meshes = result as Array<{ slug: string; peers: number }>; - if (!meshes || meshes.length === 0) return "No meshes connected\\. Use /connect to add one\\."; - return "🔗 *Connected meshes:*\n\n" + meshes.map(m => - `• *${escMd(m.slug)}* — ${m.peers} peer${m.peers !== 1 ? "s" : ""} online` + if (!meshes || meshes.length === 0) return "No meshes connected. Use /connect to add one."; + return "🔗 Connected meshes:\n\n" + meshes.map(m => + `• ${escHtml(m.slug)} — ${m.peers} peer${m.peers !== 1 ? "s" : ""} online` ).join("\n"); } case "recall": { const memories = result as Array<{ content: string; tags: string[] }>; if (!memories || memories.length === 0) return "No memories found."; - return "🧠 *Memories:*\n\n" + memories.map(m => - `• ${escMd(m.content)}${m.tags.length ? ` _[${m.tags.join(", ")}]_` : ""}` + return "🧠 Memories:\n\n" + memories.map(m => + `• ${escHtml(m.content)}${m.tags.length ? ` [${m.tags.join(", ")}]` : ""}` ).join("\n"); } case "get_state": { const state = result as { key: string; value: unknown } | null; if (!state) return "Key not found."; - return `📊 \`${escMd(state.key)}\` = \`${escMd(String(state.value))}\``; + return `📊 ${escHtml(state.key)} = ${escHtml(String(state.value))}`; } case "remember": @@ -302,8 +302,8 @@ export function formatResult(toolName: string, result: unknown): string { } } -function escMd(s: string): string { - return s.replace(/[_*[\]()~`>#+\-=|{}.!\\]/g, "\\$&"); +function escHtml(s: string): string { + return s.replace(/&/g, "&").replace(//g, ">"); } export { CONFIRM_ACTIONS }; diff --git a/apps/broker/src/telegram-bridge.ts b/apps/broker/src/telegram-bridge.ts index ba73d70..6930423 100644 --- a/apps/broker/src/telegram-bridge.ts +++ b/apps/broker/src/telegram-bridge.ts @@ -686,6 +686,17 @@ function escapeMarkdown(s: string): string { return s.replace(/([_*\[\]()~`>#+\-=|{}.!\\])/g, "\\$1"); } +/** Strip markdown formatting from AI text responses for plain Telegram display. */ +function stripMarkdown(s: string): string { + return s + .replace(/\*\*(.*?)\*\*/g, "$1") // **bold** → bold + .replace(/\*(.*?)\*/g, "$1") // *italic* → italic + .replace(/__(.*?)__/g, "$1") // __underline__ → underline + .replace(/~~(.*?)~~/g, "$1") // ~~strike~~ → strike + .replace(/`(.*?)`/g, "$1") // `code` → code + .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1"); // [text](url) → text +} + // --------------------------------------------------------------------------- // Bot command handlers // --------------------------------------------------------------------------- @@ -1197,7 +1208,7 @@ function setupBotCommands( const result = await executeAiToolCall(pending.toolCall, pending.meshIds); await ctx.editMessageText( formatResult(pending.toolCall.name, result), - { parse_mode: "MarkdownV2" }, + { parse_mode: "HTML" }, ); } catch (err) { await ctx.editMessageText(`❌ Failed: ${err instanceof Error ? err.message : String(err)}`); @@ -1608,12 +1619,12 @@ function setupBotCommands( }); if (result.type === "error") { - await ctx.reply(result.text ?? "Something went wrong."); + await ctx.reply(stripMarkdown(result.text ?? "Something went wrong.")); return; } if (result.type === "text") { - await ctx.reply(result.text ?? ""); + await ctx.reply(stripMarkdown(result.text ?? "")); return; } @@ -1630,7 +1641,7 @@ function setupBotCommands( const confirmText = formatConfirmation(result.toolCall); await ctx.reply(confirmText, { - parse_mode: "MarkdownV2", + parse_mode: "HTML", reply_markup: { inline_keyboard: [ [ @@ -1645,7 +1656,7 @@ function setupBotCommands( // Read-only action — execute immediately const execResult = await executeAiToolCall(result.toolCall, meshIds); await ctx.reply(formatResult(result.toolCall.name, execResult), { - parse_mode: "MarkdownV2", + parse_mode: "HTML", }); } }