#!/usr/bin/env node /** * lettabot-message - Send messages to channels * * Usage: * lettabot-message send --text "Hello!" [--channel telegram] [--chat 123456] * lettabot-message send -t "Hello!" * * The agent can use this CLI via Bash to send messages during silent mode * (heartbeats, cron jobs) or to send to different channels during conversations. */ import 'dotenv/config'; import { resolve } from 'node:path'; import { existsSync, readFileSync } from 'node:fs'; // Types interface LastTarget { channel: string; chatId: string; } interface AgentStore { agentId?: string; lastMessageTarget?: LastTarget; // Note: field is "lastMessageTarget" not "lastTarget" } // Store path (same location as bot uses) const STORE_PATH = resolve(process.cwd(), 'lettabot-agent.json'); function loadLastTarget(): LastTarget | null { try { if (existsSync(STORE_PATH)) { const store: AgentStore = JSON.parse(readFileSync(STORE_PATH, 'utf-8')); return store.lastMessageTarget || null; } } catch { // Ignore } return null; } // Channel senders async function sendTelegram(chatId: string, text: string): Promise { const token = process.env.TELEGRAM_BOT_TOKEN; if (!token) { throw new Error('TELEGRAM_BOT_TOKEN not set'); } const url = `https://api.telegram.org/bot${token}/sendMessage`; const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ chat_id: chatId, text: text, }), }); if (!response.ok) { const error = await response.text(); throw new Error(`Telegram API error: ${error}`); } const result = await response.json() as { ok: boolean; result?: { message_id: number } }; if (!result.ok) { throw new Error(`Telegram API returned ok=false`); } console.log(`✓ Sent to telegram:${chatId} (message_id: ${result.result?.message_id})`); } async function sendSlack(chatId: string, text: string): Promise { const token = process.env.SLACK_BOT_TOKEN; if (!token) { throw new Error('SLACK_BOT_TOKEN not set'); } const response = await fetch('https://slack.com/api/chat.postMessage', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}`, }, body: JSON.stringify({ channel: chatId, text: text, }), }); const result = await response.json() as { ok: boolean; ts?: string; error?: string }; if (!result.ok) { throw new Error(`Slack API error: ${result.error}`); } console.log(`✓ Sent to slack:${chatId} (ts: ${result.ts})`); } async function sendSignal(chatId: string, text: string): Promise { const apiUrl = process.env.SIGNAL_CLI_REST_API_URL || 'http://localhost:8080'; const phoneNumber = process.env.SIGNAL_PHONE_NUMBER; if (!phoneNumber) { throw new Error('SIGNAL_PHONE_NUMBER not set'); } const response = await fetch(`${apiUrl}/v2/send`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: text, number: phoneNumber, recipients: [chatId], }), }); if (!response.ok) { const error = await response.text(); throw new Error(`Signal API error: ${error}`); } console.log(`✓ Sent to signal:${chatId}`); } async function sendWhatsApp(chatId: string, text: string): Promise { // WhatsApp requires a running session, so we write to a queue file // that the bot process picks up. For now, error out. throw new Error('WhatsApp sending via CLI not yet supported (requires active session)'); } async function sendToChannel(channel: string, chatId: string, text: string): Promise { switch (channel.toLowerCase()) { case 'telegram': return sendTelegram(chatId, text); case 'slack': return sendSlack(chatId, text); case 'signal': return sendSignal(chatId, text); case 'whatsapp': return sendWhatsApp(chatId, text); default: throw new Error(`Unknown channel: ${channel}. Supported: telegram, slack, signal`); } } // Command handlers async function sendCommand(args: string[]): Promise { let text = ''; let channel = ''; let chatId = ''; // Parse args for (let i = 0; i < args.length; i++) { const arg = args[i]; const next = args[i + 1]; if ((arg === '--text' || arg === '-t') && next) { text = next; i++; } else if ((arg === '--channel' || arg === '-c') && next) { channel = next; i++; } else if ((arg === '--chat' || arg === '--to') && next) { chatId = next; i++; } } if (!text) { console.error('Error: --text is required'); console.error('Usage: lettabot-message send --text "Hello!" [--channel telegram] [--chat 123456]'); process.exit(1); } // Resolve defaults from last target if (!channel || !chatId) { const lastTarget = loadLastTarget(); if (lastTarget) { channel = channel || lastTarget.channel; chatId = chatId || lastTarget.chatId; } } if (!channel) { console.error('Error: --channel is required (no default available)'); console.error('Specify: --channel telegram|slack|signal'); process.exit(1); } if (!chatId) { console.error('Error: --chat is required (no default available)'); console.error('Specify: --chat '); process.exit(1); } try { await sendToChannel(channel, chatId, text); } catch (error) { console.error(`Error: ${error instanceof Error ? error.message : String(error)}`); process.exit(1); } } function showHelp(): void { console.log(` lettabot-message - Send messages to channels Commands: send [options] Send a message Send options: --text, -t Message text (required) --channel, -c Channel: telegram, slack, signal (default: last used) --chat, --to Chat/conversation ID (default: last messaged) Examples: # Send to last messaged user/channel lettabot-message send --text "Hello!" # Send to specific Telegram chat lettabot-message send --text "Hello!" --channel telegram --chat 123456789 # Short form lettabot-message send -t "Done!" -c telegram -to 123456789 Environment variables: TELEGRAM_BOT_TOKEN Required for Telegram SLACK_BOT_TOKEN Required for Slack SIGNAL_PHONE_NUMBER Required for Signal SIGNAL_CLI_REST_API_URL Signal API URL (default: http://localhost:8080) `); } // Main const args = process.argv.slice(2); const command = args[0]; switch (command) { case 'send': sendCommand(args.slice(1)); break; case 'help': case '--help': case '-h': showHelp(); break; default: if (command) { // Assume it's send with args starting with the command // e.g., `lettabot-message --text "Hi"` (no 'send' subcommand) if (command.startsWith('-')) { sendCommand(args); break; } console.error(`Unknown command: ${command}`); } showHelp(); break; }