@bytesbrains/pi-telegram-bridge 1.1.6 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,45 +1,153 @@
1
1
  # pi-telegram-bridge
2
2
 
3
- Telegram bot bridge for pi agents — send messages, ask questions, and listen for human replies via Telegram.
3
+ Telegram bot bridge for pi agents — send messages, ask questions, receive photos, and get a live project dashboard on "hi".
4
4
 
5
5
  ## Install
6
6
 
7
7
  ```bash
8
- pi install npm:@bytesbrains/pi-telegram-bridge
8
+ pi install npm:@bytesbrains/pi-telegram-bridge@1.1.6
9
9
  ```
10
10
 
11
- ## Configuration
11
+ ## Quick Setup
12
12
 
13
- Set the following environment variables:
13
+ ### 1. Create a bot with @BotFather
14
+
15
+ Open Telegram and chat with [@BotFather](https://t.me/BotFather):
16
+
17
+ ```
18
+ /newbot
19
+ ```
20
+
21
+ Follow the prompts. You'll get a token like:
22
+
23
+ ```
24
+ 123456:ABC-DEF1234ghikl-zyx57W2v1u123ew11
25
+ ```
26
+
27
+ ### 2. Get your Chat ID
28
+
29
+ Send ANY message to your new bot on Telegram, then visit this URL in your browser:
30
+
31
+ ```
32
+ https://api.telegram.org/bot<YOUR_TOKEN>/getUpdates
33
+ ```
34
+
35
+ Look for `"chat":{"id":123456789}` in the response. That's your chat ID.
36
+
37
+ > **For group chats:** Add the bot to a group, send a message mentioning the bot, then check the same URL. Use the group chat ID.
38
+
39
+ ### 3. Set environment variables
14
40
 
15
41
  ```bash
16
42
  export TELEGRAM_BOT_TOKEN="123456:ABC-DEF1234ghikl-zyx57W2v1u123ew11"
17
43
  export TELEGRAM_CHAT_ID="123456789"
18
44
  ```
19
45
 
20
- | Variable | Required | Description |
21
- |----------|----------|-------------|
22
- | `TELEGRAM_BOT_TOKEN` | ✅ | Bot token from [@BotFather](https://t.me/BotFather) |
23
- | `TELEGRAM_CHAT_ID` | ✅ | Target chat ID (your personal chat or a group) |
46
+ Add them to your shell profile (`~/.zshrc`, `~/.bashrc`) to persist across sessions.
47
+
48
+ ### 4. Verify
49
+
50
+ Start pi and run `telegram_status()`. You should see:
51
+
52
+ ```
53
+ Active. Bot: @your_bot_name (listener: 🟢 running)
54
+ ```
55
+
56
+ ## Environment Variables
57
+
58
+ | Variable | Required | Default | Description |
59
+ | -------------------- | -------- | ----------------------- | --------------------------------------------------------------- |
60
+ | `TELEGRAM_BOT_TOKEN` | ✅ | — | Bot token from @BotFather |
61
+ | `TELEGRAM_CHAT_ID` | ✅ | — | Chat ID to send/receive messages |
62
+ | `TELEGRAM_MENTION` | ❌ | `@pi` (or bot username) | Only respond to messages containing this mention in group chats |
63
+
64
+ ### TELEGRAM_MENTION
65
+
66
+ Controls which messages the agent responds to in **group chats**:
67
+
68
+ ```bash
69
+ # Use a custom mention trigger
70
+ export TELEGRAM_MENTION="@mybot"
71
+
72
+ # Or let it auto-detect from the bot username
73
+ export TELEGRAM_MENTION="" # defaults to @bot_username
74
+ ```
75
+
76
+ - **Private chat** (direct message to bot): All messages are treated as directed — no mention needed
77
+ - **Group chat**: Only messages containing `@botname` are forwarded to the agent. Everything else is ignored.
78
+
79
+ ## Features
80
+
81
+ ### Status Dashboard
82
+
83
+ Send `hi`, `/status`, or `status` to your bot on Telegram. It replies with a live project summary:
84
+
85
+ ```
86
+ 📊 pi-ext Status
87
+
88
+ 📋 Open Issues: 3
89
+ #1 telegram-bridge HTML parse crash
90
+ #5 status dashboard on hi message
91
+
92
+ 🔀 Open PRs: 2
93
+ ✅ #2 fix(telegram-bridge): escape HTML entities
94
+
95
+ 🔧 Last CI: ⏳ queued (5 recent runs)
96
+ ```
97
+
98
+ ### Photo Support
99
+
100
+ **Send photos to the agent:** Send any photo to the bot — the agent downloads it and can use `read()` to view it. Add a caption for context:
101
+
102
+ > 📸 Photo: `login page broken, 404 in console`
103
+
104
+ **Send photos from the agent:** Use `telegram_send_photo()` to share screenshots or diagrams:
105
+
106
+ ```
107
+ telegram_send_photo(photoPath="/tmp/screenshot.png", caption="Build output")
108
+ ```
109
+
110
+ ### @Mention Support
111
+
112
+ In group chats, the bot only responds to messages that mention it. This lets humans talk freely without triggering the agent.
113
+
114
+ | Chat type | Message | Bot responds? |
115
+ | --------- | ------------------------ | ---------------------------------------- |
116
+ | Private | `hi` | ✅ |
117
+ | Private | `@pi what's the status?` | ✅ (mention stripped) |
118
+ | Group | `hi` | ❌ |
119
+ | Group | `@pi what's the status?` | ✅ (agent receives `what's the status?`) |
24
120
 
25
121
  ## Tools
26
122
 
27
123
  ### telegram_listen
28
124
 
29
- Check for new inbound messages from the human.
125
+ Check for new inbound messages (text and photos) from the human.
30
126
 
31
127
  ```
32
128
  telegram_listen()
33
129
  ```
34
130
 
131
+ Returns photo paths and captions if a photo was received.
132
+
35
133
  ### telegram_send
36
134
 
37
- Send a one-way message. Use for status updates and progress reports.
135
+ Send a one-way text message.
38
136
 
39
137
  ```
40
138
  telegram_send(message="Build completed successfully ✅")
41
139
  ```
42
140
 
141
+ ### telegram_send_photo
142
+
143
+ Send a photo — supports local file paths and remote URLs.
144
+
145
+ ```
146
+ telegram_send_photo(photoPath="/tmp/screenshot.png")
147
+ telegram_send_photo(photoPath="/tmp/screenshot.png", caption="Error in console")
148
+ telegram_send_photo(photoPath="https://example.com/image.jpg")
149
+ ```
150
+
43
151
  ### telegram_ask
44
152
 
45
153
  Ask a question with inline keyboard options and **wait** for a human reply. Blocks until answered or timeout (default 30 min).
@@ -87,6 +195,7 @@ telegram_override(
87
195
  | `timedOut` | `boolean` | Whether the request timed out |
88
196
 
89
197
  **Agent workflow:**
198
+
90
199
  1. Agent gets blocked by supervisor
91
200
  2. Calls `telegram_override(command, reason, context)`
92
201
  3. Checks `details.action`:
@@ -103,33 +212,11 @@ Check if the bridge is configured and running.
103
212
  telegram_status()
104
213
  ```
105
214
 
106
- ## Background Listener
107
-
108
- The extension starts a background listener on session start that polls for incoming Telegram messages. Any non-bot text message in the configured chat is forwarded to the agent as a user message. The agent responds with a confirmation.
109
-
110
- The listener runs automatically — no manual setup needed.
111
-
112
- ## How It Works
113
-
114
- 1. **Session start**: The bridge restores the last processed update ID from session state and starts polling
115
- 2. **Inbound messages**: Non-bot text messages in the configured chat are forwarded to the agent
116
- 3. **Session end**: The listener stops cleanly on session shutdown
117
- 4. **Update ID is persisted** across sessions to avoid processing duplicate messages
118
-
119
- ## Get a Bot Token
120
-
121
- 1. Open Telegram and chat with [@BotFather](https://t.me/BotFather)
122
- 2. Send `/newbot` and follow the prompts
123
- 3. Copy the token and set `TELEGRAM_BOT_TOKEN`
124
- 4. Send a message to your bot, then visit:
125
- `https://api.telegram.org/bot<YOUR_TOKEN>/getUpdates`
126
- 5. Copy the `chat.id` from the response and set `TELEGRAM_CHAT_ID`
127
-
128
215
  ## Requirements
129
216
 
130
217
  - Node.js >= 18
131
- - A Telegram bot token
132
- - A chat ID to send/receive messages
218
+ - A Telegram bot token (from @BotFather)
219
+ - A chat ID
133
220
 
134
221
  ## License
135
222
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bytesbrains/pi-telegram-bridge",
3
- "version": "1.1.6",
4
- "description": "Telegram bot bridge for pi agents \u2014 send messages, ask questions, and listen for human replies via Telegram.",
3
+ "version": "1.2.0",
4
+ "description": "Telegram bot bridge for pi agents send messages, ask questions, and listen for human replies via Telegram.",
5
5
  "keywords": [
6
6
  "pi-package",
7
7
  "pi-extension",
@@ -440,7 +440,7 @@ describe("pollUpdates", () => {
440
440
  controller.signal, onMessage, vi.fn(), onUpdateId,
441
441
  );
442
442
 
443
- expect(onMessage).toHaveBeenCalledWith("Hello agent!");
443
+ expect(onMessage).toHaveBeenCalledWith('Hello agent!', '987654321');
444
444
  expect(onUpdateId).toHaveBeenCalledWith(100);
445
445
  }, 15000);
446
446
 
@@ -618,7 +618,7 @@ describe("pollUpdates", () => {
618
618
  controller.signal, onMessage, vi.fn(), onUpdateId,
619
619
  );
620
620
 
621
- expect(onMessage).toHaveBeenCalledWith("recovered");
621
+ expect(onMessage).toHaveBeenCalledWith('recovered', '987654321');
622
622
  }, 20000);
623
623
 
624
624
  it("handles network errors with retry", async () => {
@@ -663,7 +663,7 @@ describe("pollUpdates", () => {
663
663
  controller.signal, onMessage, vi.fn(), onUpdateId,
664
664
  );
665
665
 
666
- expect(onMessage).toHaveBeenCalledWith("eventually");
666
+ expect(onMessage).toHaveBeenCalledWith('eventually', '987654321');
667
667
  }, 25000);
668
668
 
669
669
  it("stops gracefully when aborted immediately", async () => {
@@ -734,7 +734,7 @@ describe("pollUpdates", () => {
734
734
  controller.signal, onMessage, vi.fn(), onUpdateId,
735
735
  );
736
736
 
737
- expect(onMessage).toHaveBeenCalledWith("after not-ok");
737
+ expect(onMessage).toHaveBeenCalledWith('after not-ok', '987654321');
738
738
  }, 20000);
739
739
 
740
740
  it("ignores messages without text field", async () => {
package/src/helpers.ts CHANGED
@@ -57,7 +57,11 @@ export async function sendMsg(
57
57
  } catch (e: unknown) {
58
58
  // Retry without parse_mode if formatting caused a 400 parse error
59
59
  const msg = e instanceof Error ? e.message : String(e);
60
- if (parseMode && msg.includes("400") && (msg.includes("parse") || msg.includes("entity"))) {
60
+ if (
61
+ parseMode &&
62
+ msg.includes("400") &&
63
+ (msg.includes("parse") || msg.includes("entity"))
64
+ ) {
61
65
  delete body.parse_mode;
62
66
  const r2 = (await telegramApi("sendMessage", body)) as {
63
67
  ok: boolean;
@@ -66,7 +70,8 @@ export async function sendMsg(
66
70
  return r2.result.message_id;
67
71
  }
68
72
  throw e;
69
- }}
73
+ }
74
+ }
70
75
 
71
76
  // ── Photo / File Support ────────────────────────────────────────────
72
77
 
@@ -77,14 +82,11 @@ export async function sendMsg(
77
82
  export async function getFileUrl(fileId: string): Promise<string | null> {
78
83
  const token = getToken();
79
84
  try {
80
- const res = await fetch(
81
- `${BASE_URL}${token}/getFile`,
82
- {
83
- method: "POST",
84
- headers: { "Content-Type": "application/json" },
85
- body: JSON.stringify({ file_id: fileId }),
86
- },
87
- );
85
+ const res = await fetch(`${BASE_URL}${token}/getFile`, {
86
+ method: "POST",
87
+ headers: { "Content-Type": "application/json" },
88
+ body: JSON.stringify({ file_id: fileId }),
89
+ });
88
90
  if (!res.ok) return null;
89
91
  const data = (await res.json()) as {
90
92
  ok: boolean;
@@ -102,7 +104,10 @@ export async function getFileUrl(fileId: string): Promise<string | null> {
102
104
  * Returns the file path on success, null on failure.
103
105
  * Creates parent directories automatically.
104
106
  */
105
- export async function downloadFile(fileId: string, destPath: string): Promise<string | null> {
107
+ export async function downloadFile(
108
+ fileId: string,
109
+ destPath: string,
110
+ ): Promise<string | null> {
106
111
  try {
107
112
  const fileUrl = await getFileUrl(fileId);
108
113
  if (!fileUrl) return null;
@@ -162,7 +167,11 @@ export async function sendPhoto(
162
167
  if (!res.ok) {
163
168
  const errText = await res.text();
164
169
  // Retry without caption if parse_mode caused a 400
165
- if (caption && res.status === 400 && (errText.includes("parse") || errText.includes("entity"))) {
170
+ if (
171
+ caption &&
172
+ res.status === 400 &&
173
+ (errText.includes("parse") || errText.includes("entity"))
174
+ ) {
166
175
  return sendPhoto(photoPath);
167
176
  }
168
177
  throw new Error(`sendPhoto failed: ${res.status} ${errText}`);
@@ -308,7 +317,7 @@ export async function pollUpdates(
308
317
  botId: number | null,
309
318
  lastUpdateId: number,
310
319
  signal: AbortSignal,
311
- onMessage: (text: string) => void,
320
+ onMessage: (text: string, msgChatId: string) => void,
312
321
  onPhoto: (photoId: string, caption?: string) => void,
313
322
  onUpdateId: (id: number) => void,
314
323
  ): Promise<void> {
@@ -333,7 +342,12 @@ export async function pollUpdates(
333
342
  chat: { id: number };
334
343
  from?: { id: number; is_bot?: boolean };
335
344
  text?: string;
336
- photo?: Array<{ file_id: string; file_unique_id: string; width: number; height: number }>;
345
+ photo?: Array<{
346
+ file_id: string;
347
+ file_unique_id: string;
348
+ width: number;
349
+ height: number;
350
+ }>;
337
351
  caption?: string;
338
352
  };
339
353
  callback_query?: unknown;
@@ -349,10 +363,7 @@ export async function pollUpdates(
349
363
  // Skip callback queries (handled by telegram_ask)
350
364
  if (u.callback_query) continue;
351
365
  // Process messages from the configured chat, not from our bot
352
- if (
353
- u.message &&
354
- String(u.message.chat.id) === chatId
355
- ) {
366
+ if (u.message && String(u.message.chat.id) === chatId) {
356
367
  if (u.message.from?.is_bot || u.message.from?.id === botId) {
357
368
  continue;
358
369
  }
@@ -364,7 +375,7 @@ export async function pollUpdates(
364
375
  }
365
376
  // Text message
366
377
  if (u.message.text) {
367
- onMessage(u.message.text);
378
+ onMessage(u.message.text, String(u.message.chat.id));
368
379
  }
369
380
  }
370
381
  }
package/src/index.ts CHANGED
@@ -37,6 +37,144 @@ export default function telegramBridge(pi: ExtensionAPI) {
37
37
  let botId: number | null = null;
38
38
  let listenerAbort: AbortController | null = null;
39
39
  let lastUpdateId = 0;
40
+ let mentionTrigger = "@pi"; // configurable via TELEGRAM_MENTION env var
41
+ const sessionStartTime = Date.now();
42
+ let messageCount = 0;
43
+
44
+ // ── Mention support ────────────────────────────────────────────
45
+
46
+ async function resolveMentionTrigger(): Promise<string> {
47
+ // 1. Explicit env var overrides everything
48
+ const envMention = process.env.TELEGRAM_MENTION;
49
+ if (envMention)
50
+ return envMention.startsWith("@") ? envMention : "@" + envMention;
51
+ // 2. Try bot username from getMe
52
+ try {
53
+ const r = (await telegramApi("getMe", {})) as {
54
+ result: { username: string };
55
+ };
56
+ if (r.result?.username) return "@" + r.result.username;
57
+ } catch {
58
+ /* fall through */
59
+ }
60
+ return "@pi";
61
+ }
62
+
63
+ /** Strip mention from message text and return cleaned text + whether it was mentioned. */
64
+ function checkMention(
65
+ text: string,
66
+ isPrivateChat: boolean,
67
+ ): { mentioned: boolean; cleanedText: string } {
68
+ if (isPrivateChat) return { mentioned: true, cleanedText: text };
69
+ const trigger = mentionTrigger.slice(1).toLowerCase(); // strip @
70
+ const patterns = [
71
+ new RegExp(`@${trigger}\\b`, "i"), // @pi at start or middle
72
+ new RegExp(`^${trigger}\\b`, "i"), // pi at start (no @)
73
+ ];
74
+ for (const re of patterns) {
75
+ if (re.test(text)) {
76
+ const cleaned = text.replace(re, "").trim();
77
+ return { mentioned: true, cleanedText: cleaned || text };
78
+ }
79
+ }
80
+ return { mentioned: false, cleanedText: text };
81
+ }
82
+
83
+ // ── Dashboard helper ────────────────────────────────────────────
84
+
85
+ async function sendDashboard() {
86
+ const token = getToken();
87
+ const giteaToken = process.env.GITEA_TOKEN || process.env.GIT_TOKEN || "";
88
+ const apiBase = "http://127.0.0.1:3001/api/v1/repos/factory/pi-ext";
89
+ const headers: Record<string, string> = {
90
+ "Content-Type": "application/json",
91
+ };
92
+ if (giteaToken) headers["Authorization"] = `token ${giteaToken}`;
93
+
94
+ try {
95
+ const [issuesR, prsR, runsR] = await Promise.all([
96
+ fetch(`${apiBase}/issues?state=open&limit=10`, { headers })
97
+ .then((r) => r.json())
98
+ .catch(() => []),
99
+ fetch(`${apiBase}/pulls?state=open&limit=10`, { headers })
100
+ .then((r) => r.json())
101
+ .catch(() => []),
102
+ fetch(`${apiBase}/actions/runs?limit=3`, { headers })
103
+ .then((r) => r.json())
104
+ .catch(() => ({ workflow_runs: [] })),
105
+ ]);
106
+
107
+ const issues = Array.isArray(issuesR) ? issuesR : [];
108
+ const prs = Array.isArray(prsR) ? prsR : [];
109
+ const runs = (runsR as any).workflow_runs ?? [];
110
+
111
+ const lines: string[] = [];
112
+ lines.push("📊 <b>pi-ext Status</b>");
113
+ lines.push("");
114
+
115
+ // Open issues
116
+ lines.push(`📋 <b>Open Issues:</b> ${issues.length}`);
117
+ if (issues.length > 0) {
118
+ for (const i of (issues as any[]).slice(0, 5)) {
119
+ const labels = i.labels?.length
120
+ ? ` [${i.labels.map((l: any) => l.name).join(", ")}]`
121
+ : "";
122
+ lines.push(` #${i.number} ${i.title.slice(0, 60)}${labels}`);
123
+ }
124
+ } else {
125
+ lines.push(" (none)");
126
+ }
127
+ lines.push("");
128
+
129
+ // Open PRs
130
+ lines.push(`🔀 <b>Open PRs:</b> ${prs.length}`);
131
+ if (prs.length > 0) {
132
+ for (const p of (prs as any[]).slice(0, 5)) {
133
+ const mergeIcon = p.mergeable ? "✅" : "❌";
134
+ lines.push(` ${mergeIcon} #${p.number} ${p.title.slice(0, 55)}`);
135
+ }
136
+ } else {
137
+ lines.push(" (none)");
138
+ }
139
+ lines.push("");
140
+
141
+ // CI status
142
+ if (runs.length > 0) {
143
+ const latest = runs[0];
144
+ const statusIcon =
145
+ latest.status === "success"
146
+ ? "✅"
147
+ : latest.status === "failure"
148
+ ? "❌"
149
+ : latest.status === "running"
150
+ ? "🔄"
151
+ : "⏳";
152
+ lines.push(
153
+ `🔧 <b>Last CI:</b> ${statusIcon} ${latest.status} (${runs.length} recent runs)`,
154
+ );
155
+ } else {
156
+ lines.push("🔧 <b>Last CI:</b> no runs");
157
+ }
158
+ lines.push("");
159
+
160
+ // Session metrics
161
+ const uptimeMin = Math.floor((Date.now() - sessionStartTime) / 60000);
162
+ const uptimeStr =
163
+ uptimeMin < 60
164
+ ? `${uptimeMin}m`
165
+ : `${Math.floor(uptimeMin / 60)}h ${uptimeMin % 60}m`;
166
+ lines.push("🤖 <b>Session</b>");
167
+ lines.push(` Uptime: ${uptimeStr}`);
168
+ lines.push(` Messages: ${messageCount}`);
169
+ lines.push(` Mention: ${mentionTrigger}`);
170
+
171
+ await sendMsg(lines.join("\n"), undefined, "HTML");
172
+ } catch {
173
+ await sendMsg(
174
+ "⚠️ Could not fetch project dashboard. Gitea may be unreachable.",
175
+ );
176
+ }
177
+ }
40
178
 
41
179
  // ── Session lifecycle ───────────────────────────────────────────
42
180
 
@@ -52,6 +190,7 @@ export default function telegramBridge(pi: ExtensionAPI) {
52
190
  }
53
191
  }
54
192
  // Start background listener
193
+ mentionTrigger = await resolveMentionTrigger();
55
194
  startListener();
56
195
  });
57
196
 
@@ -89,11 +228,21 @@ export default function telegramBridge(pi: ExtensionAPI) {
89
228
  botId,
90
229
  lastUpdateId,
91
230
  signal,
92
- // onMessage: forward text or photo to pi as user message
93
- async (text: string) => {
94
- pi.sendUserMessage(text);
231
+ // onMessage: forward to pi (with mention check in groups), or dashboard on "hi"
232
+ async (text: string, msgChatId: string) => {
233
+ const isPrivate = msgChatId === chatId;
234
+ const { mentioned, cleanedText } = checkMention(text, isPrivate);
235
+ if (!mentioned) return; // ignore unmentioned messages in groups
236
+
237
+ const trimmed = cleanedText.trim().toLowerCase();
238
+ if (trimmed === "hi" || trimmed === "/status" || trimmed === "status") {
239
+ await sendDashboard();
240
+ return;
241
+ }
242
+ messageCount++;
243
+ pi.sendUserMessage(cleanedText);
95
244
  await sendMsg(
96
- `👂 Got it! Working on: _${text.slice(0, 100)}_`,
245
+ `👂 Got it! Working on: _${cleanedText.slice(0, 100)}_`,
97
246
  undefined,
98
247
  "Markdown",
99
248
  );
@@ -111,8 +260,12 @@ export default function telegramBridge(pi: ExtensionAPI) {
111
260
  );
112
261
  await sendMsg("📸 Photo received! Processing...");
113
262
  } else {
114
- pi.sendUserMessage("📸 Photo received from Telegram but could not be downloaded.");
115
- await sendMsg("⚠️ Could not download your photo. Please check the bot configuration.");
263
+ pi.sendUserMessage(
264
+ "📸 Photo received from Telegram but could not be downloaded.",
265
+ );
266
+ await sendMsg(
267
+ "⚠️ Could not download your photo. Please check the bot configuration.",
268
+ );
116
269
  }
117
270
  },
118
271
  // onUpdateId: persist offset
@@ -177,38 +330,53 @@ export default function telegramBridge(pi: ExtensionAPI) {
177
330
  lastUpdateId = u.update_id;
178
331
  pi.appendEntry("tg-last-update", { update_id: lastUpdateId });
179
332
  if (u.callback_query) continue;
180
- if (
181
- u.message &&
182
- String(u.message.chat.id) === chatId
183
- ) {
333
+ if (u.message && String(u.message.chat.id) === chatId) {
184
334
  if (u.message.from?.is_bot || u.message.from?.id === botId)
185
335
  continue;
186
336
  // Photo message
187
337
  if (u.message.photo && u.message.photo.length > 0) {
188
- const photoId = u.message.photo[u.message.photo.length - 1].file_id;
338
+ const photoId =
339
+ u.message.photo[u.message.photo.length - 1].file_id;
189
340
  const caption = u.message.caption || "";
190
341
  const timestamp = Date.now();
191
342
  const fsPromises = await import("node:fs/promises");
192
343
  const photoDirLocal = path.join(os.tmpdir(), "telegram-photos");
193
- await fsPromises.mkdir(photoDirLocal, { recursive: true }).catch(() => {});
194
- const destFile = path.join(photoDirLocal, `photo_${timestamp}.jpg`);
344
+ await fsPromises
345
+ .mkdir(photoDirLocal, { recursive: true })
346
+ .catch(() => {});
347
+ const destFile = path.join(
348
+ photoDirLocal,
349
+ `photo_${timestamp}.jpg`,
350
+ );
195
351
  const result = await downloadFile(photoId, destFile);
196
352
  const captionInfo = caption ? `\nCaption: "${caption}"` : "";
197
353
  if (result) {
198
354
  return {
199
- content: [{
200
- type: "text",
201
- text: `📸 Photo received from Telegram\nPath: ${result}${captionInfo}\n\nUse read("${result}") to view the image.`,
202
- }],
203
- details: { message: caption, photoPath: result, hasPhoto: true },
355
+ content: [
356
+ {
357
+ type: "text",
358
+ text: `📸 Photo received from Telegram\nPath: ${result}${captionInfo}\n\nUse read("${result}") to view the image.`,
359
+ },
360
+ ],
361
+ details: {
362
+ message: caption,
363
+ photoPath: result,
364
+ hasPhoto: true,
365
+ },
204
366
  };
205
367
  }
206
368
  return {
207
- content: [{
208
- type: "text",
209
- text: `📸 Photo received from Telegram but could not be downloaded.${captionInfo}`,
210
- }],
211
- details: { message: caption, hasPhoto: true, downloadFailed: true },
369
+ content: [
370
+ {
371
+ type: "text",
372
+ text: `📸 Photo received from Telegram but could not be downloaded.${captionInfo}`,
373
+ },
374
+ ],
375
+ details: {
376
+ message: caption,
377
+ hasPhoto: true,
378
+ downloadFailed: true,
379
+ },
212
380
  };
213
381
  }
214
382
  // Text message
@@ -398,7 +566,11 @@ export default function telegramBridge(pi: ExtensionAPI) {
398
566
  action = "explain";
399
567
  }
400
568
 
401
- await sendMsg(`✅ Choice received: <i>${choice}</i>`, undefined, "HTML");
569
+ await sendMsg(
570
+ `✅ Choice received: <i>${choice}</i>`,
571
+ undefined,
572
+ "HTML",
573
+ );
402
574
 
403
575
  return {
404
576
  content: [
@@ -478,7 +650,10 @@ export default function telegramBridge(pi: ExtensionAPI) {
478
650
  description:
479
651
  "Send a photo to the Telegram chat. Supports local file paths and remote URLs.",
480
652
  parameters: sendPhotoSchema,
481
- async execute(_id: string, params: { photoPath: string; caption?: string }) {
653
+ async execute(
654
+ _id: string,
655
+ params: { photoPath: string; caption?: string },
656
+ ) {
482
657
  try {
483
658
  const mid = await sendPhoto(params.photoPath, params.caption);
484
659
  return {