From 657fbf72bc1d9f0b833e469e03122283a0204801 Mon Sep 17 00:00:00 2001 From: trilegit Date: Thu, 23 Apr 2026 15:36:04 +0700 Subject: [PATCH 1/2] fix(channel): strip system tags from agent messages before sending to Telegram Claude Code injects structured XML tags (command-name, command-message, local-command-stdout, system-reminder, etc.) into the conversation that are intended for the UI layer, not for end users. Without filtering, these tags appear verbatim in Telegram messages. Add stripSystemTags() to remove known system tags from agent output before forwarding to Telegram. --- packages/cli/src/commands/channel.ts | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/commands/channel.ts b/packages/cli/src/commands/channel.ts index 2b1fdc0..4a18bb8 100644 --- a/packages/cli/src/commands/channel.ts +++ b/packages/cli/src/commands/channel.ts @@ -105,6 +105,23 @@ function setupInputHandler( }); } +const SYSTEM_TAGS = [ + 'command-name', + 'command-message', + 'command-args', + 'local-command-stdout', + 'system-reminder', + 'user-prompt-submit-hook', +]; + +function stripSystemTags(content: string): string { + let result = content; + for (const tag of SYSTEM_TAGS) { + result = result.replace(new RegExp(`<${tag}[^>]*>[\\s\\S]*?<\\/${tag}>`, 'g'), ''); + } + return result.trim(); +} + function startOutputPolling( telegram: TelegramAdapter, agentAdapter: AgentAdapter, @@ -137,8 +154,10 @@ function startOutputPolling( for (const msg of newMessages) { if (msg.role !== 'user' && msg.content) { - await telegram.sendMessage(chatIdRef.value, msg.content); - debug(`Sent agent response to Telegram (role: ${msg.role}, length: ${msg.content.length})`); + const cleaned = stripSystemTags(msg.content); + if (!cleaned) continue; + await telegram.sendMessage(chatIdRef.value, cleaned); + debug(`Sent agent response to Telegram (role: ${msg.role}, length: ${cleaned.length})`); } } } catch { From 8255be4172ed85c9828ad991258b15624624ba90 Mon Sep 17 00:00:00 2001 From: trilegit Date: Thu, 23 Apr 2026 15:43:09 +0700 Subject: [PATCH 2/2] feat(channel): render Markdown as HTML in Telegram messages Claude Code responses use Markdown formatting (bold, italic, code blocks, inline code) which Telegram displays as raw text by default. Add markdownToHtml() converter in TelegramAdapter that transforms standard Markdown to Telegram HTML, and send messages with parse_mode: 'HTML'. Code blocks are extracted first to prevent formatting replacements inside them. --- .../src/adapters/TelegramAdapter.ts | 44 ++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/packages/channel-connector/src/adapters/TelegramAdapter.ts b/packages/channel-connector/src/adapters/TelegramAdapter.ts index 9ad3e39..14a0e37 100644 --- a/packages/channel-connector/src/adapters/TelegramAdapter.ts +++ b/packages/channel-connector/src/adapters/TelegramAdapter.ts @@ -59,7 +59,8 @@ export class TelegramAdapter implements ChannelAdapter { async sendMessage(chatId: string, text: string): Promise { const chunks = chunkMessage(text, TELEGRAM_MAX_MESSAGE_LENGTH); for (const chunk of chunks) { - await this.bot.telegram.sendMessage(chatId, chunk); + const html = markdownToHtml(chunk); + await this.bot.telegram.sendMessage(chatId, html, { parse_mode: 'HTML' }); } } @@ -108,3 +109,44 @@ function chunkMessage(text: string, maxLen: number): string[] { return chunks; } + +function escapeHtml(text: string): string { + return text.replace(/&/g, '&').replace(//g, '>'); +} + +/** + * Convert standard Markdown to Telegram HTML. + * Handles code blocks first to prevent formatting inside them. + */ +function markdownToHtml(text: string): string { + const codeBlocks: string[] = []; + const inlineCodes: string[] = []; + + let result = text.replace(/```(\w*)\n?([\s\S]*?)```/g, (_, lang, code) => { + const escaped = escapeHtml(code.trimEnd()); + const block = lang + ? `
${escaped}
` + : `
${escaped}
`; + codeBlocks.push(block); + return `\x00CODE${codeBlocks.length - 1}\x00`; + }); + + result = result.replace(/`([^`]+)`/g, (_, code) => { + inlineCodes.push(`${escapeHtml(code)}`); + return `\x00INLINE${inlineCodes.length - 1}\x00`; + }); + + result = escapeHtml(result); + + result = result + .replace(/\*\*(.+?)\*\*/g, '$1') + .replace(/__(.+?)__/g, '$1') + .replace(/\*(.+?)\*/g, '$1') + .replace(/_(.+?)_/g, '$1') + .replace(/~~(.+?)~~/g, '$1'); + + result = result.replace(/\x00CODE(\d+)\x00/g, (_, i) => codeBlocks[parseInt(i)]); + result = result.replace(/\x00INLINE(\d+)\x00/g, (_, i) => inlineCodes[parseInt(i)]); + + return result; +}