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",
});
}
}