97 lines
3.1 KiB
TypeScript
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);
|
|
});
|
|
}
|