diff --git a/src/channels/telegram/telegram.js b/src/channels/telegram/telegram.js index fc1cc4e..4573112 100644 --- a/src/channels/telegram/telegram.js +++ b/src/channels/telegram/telegram.js @@ -78,6 +78,105 @@ class TelegramChannel extends NotificationChannel { } } + /** + * Escape special characters for Telegram Markdown + * @param {string} text - Text to escape + * @returns {string} - Escaped text + */ + _escapeMarkdown(text) { + if (!text) return ''; + // Minimal escaping to avoid message rejection + // Over-escaping causes Telegram to reject the message + return text + .replace(/\*/g, '\\*') // Escape asterisks + .replace(/_/g, '\\_') // Escape underscores + .replace(/\[/g, '\\[') // Escape square brackets + .replace(/\]/g, '\\]') // Escape square brackets + .replace(/`/g, '\\`'); // Escape backticks + } + + /** + * Create a safe plain text version without markdown formatting + * @param {string} text - Text to make safe + * @returns {string} - Safe text + */ + _createSafeText(text) { + if (!text) return ''; + // Remove problematic characters entirely to ensure message sends + return text + .replace(/[_*\[\]()~`>#+=|{}.!\\-]/g, '') // Remove special chars + .replace(/\s+/g, ' ') // Collapse multiple spaces + .trim(); + } + + /** + * Calculate message length including formatting + * @param {string} message - Message to calculate + * @returns {number} - Message length + */ + _calculateMessageLength(message) { + return message.length; + } + + /** + * Split long text into chunks that fit Telegram limits + * @param {string} text - Text to split + * @param {number} maxLength - Maximum length per chunk + * @returns {string[]} - Array of text chunks + */ + _splitTextIntoChunks(text, maxLength = 3000) { + if (text.length <= maxLength) { + return [text]; + } + + const chunks = []; + let currentChunk = ''; + const lines = text.split('\n'); + + for (const line of lines) { + // If adding this line would exceed the limit + if (currentChunk.length + line.length + 1 > maxLength) { + if (currentChunk) { + chunks.push(currentChunk.trim()); + currentChunk = ''; + } + + // If single line is too long, split it by words + if (line.length > maxLength) { + const words = line.split(' '); + let wordChunk = ''; + + for (const word of words) { + if (wordChunk.length + word.length + 1 > maxLength) { + if (wordChunk) { + chunks.push(wordChunk.trim()); + wordChunk = word; + } else { + // Single word is too long, truncate it + chunks.push(word.substring(0, maxLength - 3) + '...'); + } + } else { + wordChunk += (wordChunk ? ' ' : '') + word; + } + } + if (wordChunk) { + currentChunk = wordChunk; + } + } else { + currentChunk = line; + } + } else { + currentChunk += (currentChunk ? '\n' : '') + line; + } + } + + if (currentChunk) { + chunks.push(currentChunk.trim()); + } + + return chunks; + } + async _getBotUsername() { if (this.botUsername) { return this.botUsername; @@ -156,16 +255,100 @@ class TelegramChannel extends NotificationChannel { }; try { + // Log the message details for debugging + console.log(`[DEBUG] =====================================================`); + console.log(`[DEBUG] Sending Telegram message, length: ${messageText.length}`); + console.log(`[DEBUG] Chat ID: ${chatId}`); + console.log(`[DEBUG] Session ID: ${sessionId}`); + console.log(`[DEBUG] Message preview:`, messageText.substring(0, 200) + '...'); + console.log(`[DEBUG] =====================================================`); + const response = await axios.post( `${this.apiBaseUrl}/bot${this.config.botToken}/sendMessage`, requestData, - this._getNetworkOptions() + { + ...this._getNetworkOptions(), + timeout: 15000 // 15 second timeout + } ); + console.log(`[DEBUG] ✅ Telegram message sent successfully!`); this.logger.info(`Telegram message sent successfully, Session: ${sessionId}`); return true; } catch (error) { - this.logger.error('Failed to send Telegram message:', error.response?.data || error.message); + // Enhanced error logging + const errorData = error.response?.data; + const errorMessage = errorData?.description || error.message; + const errorCode = errorData?.error_code; + + console.log(`[DEBUG] ❌ Telegram send error occurred:`); + console.log(`[DEBUG] Error Code: ${errorCode}`); + console.log(`[DEBUG] Error Message: ${errorMessage}`); + console.log(`[DEBUG] Full error response:`, JSON.stringify(errorData, null, 2)); + console.log(`[DEBUG] Original message length: ${messageText.length}`); + + this.logger.error(`Failed to send Telegram message (${errorCode}): ${errorMessage}`); + + // Try multiple fallback strategies + console.log(`[DEBUG] Attempting fallback strategies...`); + + // Strategy 1: Try without parse_mode (plain text) + try { + console.log(`[DEBUG] Trying Strategy 1: Plain text without markdown`); + const plainTextMessage = this._generateMinimalMessage(notification, token, + notification.type === 'completed' ? '✅' : '⏳', + notification.type === 'completed' ? 'Completed' : 'Waiting'); + + await axios.post( + `${this.apiBaseUrl}/bot${this.config.botToken}/sendMessage`, + { + chat_id: chatId, + text: plainTextMessage, + reply_markup: { + inline_keyboard: buttons + } + }, + { + ...this._getNetworkOptions(), + timeout: 15000 + } + ); + + console.log(`[DEBUG] ✅ Strategy 1 succeeded: Plain text message sent`); + this.logger.info(`Telegram plain text fallback message sent successfully, Session: ${sessionId}`); + return true; + + } catch (fallbackError1) { + console.log(`[DEBUG] ❌ Strategy 1 failed:`, fallbackError1.response?.data?.description || fallbackError1.message); + + // Strategy 2: Try absolute minimal message without buttons + try { + console.log(`[DEBUG] Trying Strategy 2: Minimal message without buttons`); + const minimalMessage = `Claude Task Ready\\nToken: ${token}`; + + await axios.post( + `${this.apiBaseUrl}/bot${this.config.botToken}/sendMessage`, + { + chat_id: chatId, + text: minimalMessage + }, + { + ...this._getNetworkOptions(), + timeout: 15000 + } + ); + + console.log(`[DEBUG] ✅ Strategy 2 succeeded: Minimal message sent`); + this.logger.info(`Telegram minimal fallback message sent successfully, Session: ${sessionId}`); + return true; + + } catch (fallbackError2) { + console.log(`[DEBUG] ❌ Strategy 2 failed:`, fallbackError2.response?.data?.description || fallbackError2.message); + console.log(`[DEBUG] ❌ All fallback strategies failed`); + this.logger.error('All Telegram fallback strategies failed'); + } + } + // Clean up failed session await this._removeSession(sessionId); return false; @@ -177,35 +360,92 @@ class TelegramChannel extends NotificationChannel { const emoji = type === 'completed' ? '✅' : '⏳'; const status = type === 'completed' ? 'Completed' : 'Waiting for Input'; - let messageText = `${emoji} *Claude Task ${status}*\n`; - messageText += `*Project:* ${notification.project}\n`; - messageText += `*Session Token:* \`${token}\`\n\n`; - - if (notification.metadata) { - if (notification.metadata.userQuestion) { - messageText += `📝 *Your Question:*\n${notification.metadata.userQuestion.substring(0, 200)}`; - if (notification.metadata.userQuestion.length > 200) { - messageText += '...'; - } - messageText += '\n\n'; + try { + // Method 1: Try with minimal markdown formatting + let messageText = this._generateFormattedMessage(notification, token, emoji, status); + + if (messageText.length <= 4000) { + console.log(`[DEBUG] Generated formatted message length: ${messageText.length}`); + return messageText; } - if (notification.metadata.claudeResponse) { - messageText += `🤖 *Claude Response:*\n${notification.metadata.claudeResponse.substring(0, 300)}`; - if (notification.metadata.claudeResponse.length > 300) { - messageText += '...'; - } - messageText += '\n\n'; + // Method 2: If too long, try plain text version + console.log(`[DEBUG] Formatted message too long (${messageText.length}), trying plain text`); + messageText = this._generatePlainTextMessage(notification, token, emoji, status); + + if (messageText.length <= 4000) { + console.log(`[DEBUG] Generated plain text message length: ${messageText.length}`); + return messageText; } + + // Method 3: If still too long, use minimal fallback + console.log(`[DEBUG] Plain text still too long (${messageText.length}), using minimal fallback`); + return this._generateMinimalMessage(notification, token, emoji, status); + + } catch (error) { + console.log(`[DEBUG] Error generating message: ${error.message}, using safe fallback`); + return this._generateMinimalMessage(notification, token, emoji, status); + } + } + + _generateFormattedMessage(notification, token, emoji, status) { + let messageText = `${emoji} *Claude Task ${status}*\n`; + messageText += `*Project:* ${notification.project || 'Unknown'}\n`; + messageText += `*Token:* \`${token}\`\n\n`; + + // Add user question if available (limited length) + if (notification.metadata?.userQuestion) { + const question = notification.metadata.userQuestion.substring(0, 150); + messageText += `📝 *Question:* ${this._escapeMarkdown(question)}${question.length < notification.metadata.userQuestion.length ? '...' : ''}\n\n`; } - messageText += `💬 *To send a new command:*\n`; - messageText += `Reply with: \`/cmd ${token} \`\n`; - messageText += `Example: \`/cmd ${token} Please analyze this code\``; - + // Add Claude response if available (limited length) + if (notification.metadata?.claudeResponse) { + const maxResponseLength = 3000 - messageText.length - 200; // Reserve space for instructions + let response = notification.metadata.claudeResponse; + + if (response.length > maxResponseLength) { + response = response.substring(0, maxResponseLength - 10) + '...'; + } + + messageText += `🤖 *Response:*\n${this._escapeMarkdown(response)}\n\n`; + } + + messageText += `💬 Use: \`/cmd ${token} \``; return messageText; } + _generatePlainTextMessage(notification, token, emoji, status) { + let messageText = `${emoji} Claude Task ${status}\n`; + messageText += `Project: ${notification.project || 'Unknown'}\n`; + messageText += `Token: ${token}\n\n`; + + // Add user question (plain text, limited) + if (notification.metadata?.userQuestion) { + const question = this._createSafeText(notification.metadata.userQuestion.substring(0, 150)); + messageText += `Question: ${question}${question.length < notification.metadata.userQuestion.length ? '...' : ''}\n\n`; + } + + // Add Claude response (plain text, limited) + if (notification.metadata?.claudeResponse) { + const maxResponseLength = 3000 - messageText.length - 100; + let response = this._createSafeText(notification.metadata.claudeResponse); + + if (response.length > maxResponseLength) { + response = response.substring(0, maxResponseLength - 10) + '...'; + } + + messageText += `Response: ${response}\n\n`; + } + + messageText += `Use: /cmd ${token} `; + return messageText; + } + + _generateMinimalMessage(notification, token, emoji, status) { + return `${emoji} Claude Task ${status}\nToken: ${token}\nUse: /cmd ${token} `; + } + async _createSession(sessionId, notification, token) { const session = { id: sessionId, diff --git a/src/data/session-map.json b/src/data/session-map.json index f224839..7494634 100644 --- a/src/data/session-map.json +++ b/src/data/session-map.json @@ -817,5 +817,95 @@ "sessionId": "dbd1f477-0550-463b-b147-fcdf78720177", "tmuxSession": "claude-session", "description": "completed - ecllipse" + }, + "IS1F9FN9": { + "type": "pty", + "createdAt": 1756991169, + "expiresAt": 1757077569, + "cwd": "/home/lsamc/.local/src/Claude-Code-Remote", + "sessionId": "2fa9255d-eb9a-4b37-8852-8204d5730d42", + "tmuxSession": "claude-session", + "description": "completed - Claude-Code-Remote" + }, + "CJRIHOH9": { + "type": "pty", + "createdAt": 1756991395, + "expiresAt": 1757077795, + "cwd": "/home/lsamc/develop/ecllipse", + "sessionId": "c5d96613-00de-459c-a8f0-52f020d135fc", + "tmuxSession": "claude-session", + "description": "completed - ecllipse" + }, + "19318YCF": { + "type": "pty", + "createdAt": 1756991584, + "expiresAt": 1757077984, + "cwd": "/home/lsamc/develop/ecllipse", + "sessionId": "ffba11b1-0aaf-4639-ba4f-6db27249ae22", + "tmuxSession": "claude-session", + "description": "completed - ecllipse" + }, + "KTP2NLKA": { + "type": "pty", + "createdAt": 1756991689, + "expiresAt": 1757078089, + "cwd": "/home/lsamc/develop/ecllipse", + "sessionId": "c4b49e6a-c4ca-446a-b28c-09863478c9fc", + "tmuxSession": "claude-session", + "description": "completed - ecllipse" + }, + "3HB36PXA": { + "type": "pty", + "createdAt": 1756991822, + "expiresAt": 1757078222, + "cwd": "/home/lsamc/develop/ecllipse", + "sessionId": "9a14ee0e-cf2d-44f5-bec8-548853fc97e1", + "tmuxSession": "claude-session", + "description": "completed - ecllipse" + }, + "WLR6CVMN": { + "type": "pty", + "createdAt": 1756991860, + "expiresAt": 1757078260, + "cwd": "/home/lsamc/.local/src/Claude-Code-Remote", + "sessionId": "22cd07f1-34c2-4f58-b470-d7e7d235a92b", + "tmuxSession": "claude-session", + "description": "completed - Claude-Code-Remote" + }, + "634ZIJ55": { + "type": "pty", + "createdAt": 1756991894, + "expiresAt": 1757078294, + "cwd": "/home/lsamc/develop/ecllipse", + "sessionId": "7016be36-709a-48c5-8f64-2589aae4e5e9", + "tmuxSession": "claude-session", + "description": "completed - ecllipse" + }, + "HG4Z82VG": { + "type": "pty", + "createdAt": 1756991984, + "expiresAt": 1757078384, + "cwd": "/home/lsamc/develop/ecllipse", + "sessionId": "c42a911c-2b47-4b41-8280-c62889ab2384", + "tmuxSession": "claude-session", + "description": "completed - ecllipse" + }, + "OIO9DPTN": { + "type": "pty", + "createdAt": 1756992035, + "expiresAt": 1757078435, + "cwd": "/home/lsamc/develop/ecllipse", + "sessionId": "e29d64f2-19f5-4e5b-92c8-6bd1b766bc0d", + "tmuxSession": "claude-session", + "description": "completed - ecllipse" + }, + "PWB2DEYR": { + "type": "pty", + "createdAt": 1756992167, + "expiresAt": 1757078567, + "cwd": "/home/lsamc/develop/ecllipse", + "sessionId": "197cd53a-4a64-42a9-8262-8e3b429f08b8", + "tmuxSession": "claude-session", + "description": "completed - ecllipse" } } \ No newline at end of file diff --git a/test-extraction-fix.js b/test-extraction-fix.js deleted file mode 100644 index b881b93..0000000 --- a/test-extraction-fix.js +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env node - -/** - * Test script for extractConversation function fix - * Tests the improved response detection logic - */ - -const TmuxMonitor = require('./src/utils/tmux-monitor'); - -// Create test tmux buffer content that mimics actual Claude Code output -const testBuffer1 = ` -Welcome to Claude Code -? for shortcuts -─────────────────────────── - -> what does this project do? - -I'll help you understand what this project does. Let me analyze the codebase structure first. - - - -/path/to/file.js \ No newline at end of file