/** * Telegram Stream Extension * * Sends live updates to Telegram as Pi runs tool calls. * Edits a single message in-place so it feels like streaming. * * Reads TELEGRAM_BOT_TOKEN, TELEGRAM_ALLOWED_USERS, TELEGRAM_STREAM_MSG_ID * from env — the bot sets TELEGRAM_STREAM_MSG_ID before spawning Pi. * * Usage: pi -e extensions/telegram-stream.ts */ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; import { isToolCallEventType } from "@mariozechner/pi-coding-agent"; const BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN ?? ""; const CHAT_ID = process.env.TELEGRAM_ALLOWED_USERS ?? ""; const MSG_ID = process.env.TELEGRAM_STREAM_MSG_ID ?? ""; const MAX_LEN = 3800; // Telegram max is 4096, leave room function truncate(s: string): string { return s.length > MAX_LEN ? s.slice(0, MAX_LEN) + "\n…" : s; } async function editMessage(text: string) { if (!BOT_TOKEN || !CHAT_ID || !MSG_ID) return; try { await fetch(`https://api.telegram.org/bot${BOT_TOKEN}/editMessageText`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ chat_id: CHAT_ID, message_id: parseInt(MSG_ID), text: truncate(text), parse_mode: "Markdown", }), }); } catch (_) {} } export default function (pi: ExtensionAPI) { const steps: string[] = []; const startTimes: Map = new Map(); function render(currentStep?: string): string { const lines = ["⚙️ *Pi is working...*\n"]; for (const s of steps) lines.push(s); if (currentStep) lines.push(currentStep); return lines.join("\n"); } pi.on("tool_call", async (event, _ctx) => { let label = ""; if (isToolCallEventType("bash", event)) { const cmd = event.input.command ?? ""; const short = cmd.length > 80 ? cmd.slice(0, 80) + "…" : cmd; label = `🔄 \`${short}\``; } else { label = `🔄 \`${event.toolName}\``; } startTimes.set(event.toolName + event.toolCallId, Date.now()); await editMessage(render(label)); }); pi.on("tool_execution_end", async (event) => { const key = event.toolName + (event as any).toolCallId; const elapsed = startTimes.has(key) ? ((Date.now() - startTimes.get(key)!) / 1000).toFixed(1) : "?"; startTimes.delete(key); let label = ""; if (event.toolName === "bash") { const result = (event.result ?? "").toString().trim(); const preview = result.split("\n").slice(0, 3).join("\n"); const short = preview.length > 120 ? preview.slice(0, 120) + "…" : preview; label = `✅ \`bash\` _(${elapsed}s)_${short ? "\n```\n" + short + "\n```" : ""}`; } else { label = `✅ \`${event.toolName}\` _(${elapsed}s)_`; } steps.push(label); // Keep last 10 steps to avoid message getting too long if (steps.length > 10) steps.shift(); await editMessage(render()); }); pi.on("agent_end", async (_event) => { const summary = steps.length > 0 ? `✅ *Done!* _(${steps.length} steps)_` : `✅ *Done!*`; await editMessage(render() + "\n\n" + summary); }); }