Files

97 lines
3.1 KiB
TypeScript

/**
* 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<string, number> = 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);
});
}