@bytesbrains/pi-telegram-bridge 1.1.3 → 1.1.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytesbrains/pi-telegram-bridge",
3
- "version": "1.1.3",
3
+ "version": "1.1.4",
4
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",
@@ -49,6 +49,7 @@
49
49
  "test:watch": "vitest"
50
50
  },
51
51
  "devDependencies": {
52
+ "@types/node": "^25.8.0",
52
53
  "typescript": "^6.0.3",
53
54
  "vitest": "^2.1.9"
54
55
  }
package/src/helpers.ts CHANGED
@@ -6,247 +6,241 @@ const BASE_URL = "https://api.telegram.org/bot";
6
6
  // ── Config ──────────────────────────────────────────────────────────
7
7
 
8
8
  export function getToken(): string {
9
- const tok = process.env.TELEGRAM_BOT_TOKEN;
10
- if (!tok) throw new Error("TELEGRAM_BOT_TOKEN not set");
11
- return tok;
9
+ const tok = process.env.TELEGRAM_BOT_TOKEN;
10
+ if (!tok) throw new Error("TELEGRAM_BOT_TOKEN not set");
11
+ return tok;
12
12
  }
13
13
 
14
14
  export function getChatId(): string {
15
- const id = process.env.TELEGRAM_CHAT_ID;
16
- if (!id) throw new Error("TELEGRAM_CHAT_ID not set");
17
- return id;
15
+ const id = process.env.TELEGRAM_CHAT_ID;
16
+ if (!id) throw new Error("TELEGRAM_CHAT_ID not set");
17
+ return id;
18
18
  }
19
19
 
20
20
  // ── API ─────────────────────────────────────────────────────────────
21
21
 
22
22
  export async function telegramApi(
23
- method: string,
24
- body: Record<string, unknown>,
23
+ method: string,
24
+ body: Record<string, unknown>,
25
25
  ): Promise<unknown> {
26
- const url = `${BASE_URL}${getToken()}/${method}`;
27
- const res = await fetch(url, {
28
- method: "POST",
29
- headers: { "Content-Type": "application/json" },
30
- body: JSON.stringify(body),
31
- });
32
- if (!res.ok) {
33
- throw new Error(`Telegram API ${res.status}: ${await res.text()}`);
34
- }
35
- return res.json();
26
+ const url = `${BASE_URL}${getToken()}/${method}`;
27
+ const res = await fetch(url, {
28
+ method: "POST",
29
+ headers: { "Content-Type": "application/json" },
30
+ body: JSON.stringify(body),
31
+ });
32
+ if (!res.ok) {
33
+ throw new Error(`Telegram API ${res.status}: ${await res.text()}`);
34
+ }
35
+ return res.json();
36
36
  }
37
37
 
38
38
  // ── Messaging ───────────────────────────────────────────────────────
39
39
 
40
40
  export async function sendMsg(
41
- text: string,
42
- replyMarkup?: object,
43
- parseMode: "Markdown" | "HTML" = "Markdown",
41
+ text: string,
42
+ replyMarkup?: object,
43
+ parseMode: "Markdown" | "HTML" | undefined = undefined,
44
44
  ): Promise<number> {
45
- const body: Record<string, unknown> = {
46
- chat_id: getChatId(),
47
- text,
48
- parse_mode: parseMode,
49
- };
50
- if (replyMarkup) body.reply_markup = JSON.stringify(replyMarkup);
51
- const r = (await telegramApi("sendMessage", body)) as {
52
- ok: boolean;
53
- result: { message_id: number };
54
- };
55
- return r.result.message_id;
45
+ const body: Record<string, unknown> = {
46
+ chat_id: getChatId(),
47
+ text,
48
+ parse_mode: parseMode,
49
+ };
50
+ if (replyMarkup) body.reply_markup = JSON.stringify(replyMarkup);
51
+ const r = (await telegramApi("sendMessage", body)) as {
52
+ ok: boolean;
53
+ result: { message_id: number };
54
+ };
55
+ return r.result.message_id;
56
56
  }
57
57
 
58
58
  // ── Polling ─────────────────────────────────────────────────────────
59
59
 
60
60
  export async function pollReply(
61
- sentId: number,
62
- chatId: string,
63
- timeoutMs: number,
61
+ sentId: number,
62
+ chatId: string,
63
+ timeoutMs: number,
64
64
  ): Promise<string | null> {
65
- const token = getToken();
66
- const deadline = Date.now() + timeoutMs;
65
+ const token = getToken();
66
+ const deadline = Date.now() + timeoutMs;
67
67
 
68
- // Drain pending updates to start fresh
69
- let lastId = 0;
70
- try {
71
- const drain = await fetch(`${BASE_URL}${token}/getUpdates`, {
72
- method: "POST",
73
- headers: { "Content-Type": "application/json" },
74
- body: JSON.stringify({ timeout: 0 }),
75
- });
76
- if (drain.ok) {
77
- const d = (await drain.json()) as {
78
- ok: boolean;
79
- result: Array<{ update_id: number }>;
80
- };
81
- if (d.ok && d.result.length > 0) {
82
- lastId = d.result[d.result.length - 1].update_id;
83
- }
84
- }
85
- } catch {
86
- /* ignore drain errors */
87
- }
68
+ // Drain pending updates to start fresh
69
+ let lastId = 0;
70
+ try {
71
+ const drain = await fetch(`${BASE_URL}${token}/getUpdates`, {
72
+ method: "POST",
73
+ headers: { "Content-Type": "application/json" },
74
+ body: JSON.stringify({ timeout: 0 }),
75
+ });
76
+ if (drain.ok) {
77
+ const d = (await drain.json()) as {
78
+ ok: boolean;
79
+ result: Array<{ update_id: number }>;
80
+ };
81
+ if (d.ok && d.result.length > 0) {
82
+ lastId = d.result[d.result.length - 1].update_id;
83
+ }
84
+ }
85
+ } catch {
86
+ /* ignore drain errors */
87
+ }
88
88
 
89
- while (Date.now() < deadline) {
90
- const res = await fetch(
91
- `${BASE_URL}${token}/getUpdates`,
92
- {
93
- method: "POST",
94
- headers: { "Content-Type": "application/json" },
95
- body: JSON.stringify({ offset: lastId + 1, timeout: 5 }),
96
- },
97
- );
98
- if (!res.ok) {
99
- await new Promise((r) => setTimeout(r, 3000));
100
- continue;
101
- }
102
- const data = (await res.json()) as {
103
- ok: boolean;
104
- result: Array<{
105
- update_id: number;
106
- message?: {
107
- message_id: number;
108
- chat: { id: number };
109
- text?: string;
110
- };
111
- callback_query?: {
112
- id: string;
113
- message: { message_id: number; chat: { id: number } };
114
- data: string;
115
- };
116
- }>;
117
- };
118
- if (!data.ok) {
119
- await new Promise((r) => setTimeout(r, 2000));
120
- continue;
121
- }
122
- for (const u of data.result) {
123
- lastId = u.update_id;
124
- // Check for text reply
125
- if (
126
- u.message &&
127
- u.message.chat.id === Number(chatId) &&
128
- u.message.message_id > sentId &&
129
- u.message.text
130
- ) {
131
- return u.message.text;
132
- }
133
- // Check for callback query (inline button)
134
- if (
135
- u.callback_query &&
136
- u.callback_query.message.chat.id === Number(chatId)
137
- ) {
138
- await telegramApi("answerCallbackQuery", {
139
- callback_query_id: u.callback_query.id,
140
- });
141
- return u.callback_query.data;
142
- }
143
- }
144
- await new Promise((r) => setTimeout(r, 2000));
145
- }
146
- return null; // timeout
89
+ while (Date.now() < deadline) {
90
+ const res = await fetch(`${BASE_URL}${token}/getUpdates`, {
91
+ method: "POST",
92
+ headers: { "Content-Type": "application/json" },
93
+ body: JSON.stringify({ offset: lastId + 1, timeout: 5 }),
94
+ });
95
+ if (!res.ok) {
96
+ await new Promise((r) => setTimeout(r, 3000));
97
+ continue;
98
+ }
99
+ const data = (await res.json()) as {
100
+ ok: boolean;
101
+ result: Array<{
102
+ update_id: number;
103
+ message?: {
104
+ message_id: number;
105
+ chat: { id: number };
106
+ text?: string;
107
+ };
108
+ callback_query?: {
109
+ id: string;
110
+ message: { message_id: number; chat: { id: number } };
111
+ data: string;
112
+ };
113
+ }>;
114
+ };
115
+ if (!data.ok) {
116
+ await new Promise((r) => setTimeout(r, 2000));
117
+ continue;
118
+ }
119
+ for (const u of data.result) {
120
+ lastId = u.update_id;
121
+ // Check for text reply
122
+ if (
123
+ u.message &&
124
+ u.message.chat.id === Number(chatId) &&
125
+ u.message.message_id > sentId &&
126
+ u.message.text
127
+ ) {
128
+ return u.message.text;
129
+ }
130
+ // Check for callback query (inline button)
131
+ if (
132
+ u.callback_query &&
133
+ u.callback_query.message.chat.id === Number(chatId)
134
+ ) {
135
+ await telegramApi("answerCallbackQuery", {
136
+ callback_query_id: u.callback_query.id,
137
+ });
138
+ return u.callback_query.data;
139
+ }
140
+ }
141
+ await new Promise((r) => setTimeout(r, 2000));
142
+ }
143
+ return null; // timeout
147
144
  }
148
145
 
149
146
  // ── Listener State ──────────────────────────────────────────────────
150
147
 
151
148
  export async function getBotId(): Promise<number | null> {
152
- try {
153
- const r = (await telegramApi("getMe", {})) as { result: { id: number } };
154
- return r.result.id;
155
- } catch {
156
- return null;
157
- }
149
+ try {
150
+ const r = (await telegramApi("getMe", {})) as { result: { id: number } };
151
+ return r.result.id;
152
+ } catch {
153
+ return null;
154
+ }
158
155
  }
159
156
 
160
157
  export async function seedLastUpdateId(
161
- lastUpdateId: number,
162
- signal?: AbortSignal,
158
+ lastUpdateId: number,
159
+ signal?: AbortSignal,
163
160
  ): Promise<number> {
164
- if (lastUpdateId !== 0) return lastUpdateId;
165
- const token = getToken();
166
- try {
167
- const res = await fetch(`${BASE_URL}${token}/getUpdates`, {
168
- method: "POST",
169
- headers: { "Content-Type": "application/json" },
170
- body: JSON.stringify({ timeout: 0 }),
171
- signal,
172
- });
173
- if (res.ok) {
174
- const d = (await res.json()) as {
175
- ok: boolean;
176
- result: Array<{ update_id: number }>;
177
- };
178
- if (d.ok && d.result.length > 0) {
179
- return d.result[d.result.length - 1].update_id;
180
- }
181
- }
182
- } catch {
183
- /* ignore */
184
- }
185
- return lastUpdateId;
161
+ if (lastUpdateId !== 0) return lastUpdateId;
162
+ const token = getToken();
163
+ try {
164
+ const res = await fetch(`${BASE_URL}${token}/getUpdates`, {
165
+ method: "POST",
166
+ headers: { "Content-Type": "application/json" },
167
+ body: JSON.stringify({ timeout: 0 }),
168
+ signal,
169
+ });
170
+ if (res.ok) {
171
+ const d = (await res.json()) as {
172
+ ok: boolean;
173
+ result: Array<{ update_id: number }>;
174
+ };
175
+ if (d.ok && d.result.length > 0) {
176
+ return d.result[d.result.length - 1].update_id;
177
+ }
178
+ }
179
+ } catch {
180
+ /* ignore */
181
+ }
182
+ return lastUpdateId;
186
183
  }
187
184
 
188
185
  export async function pollUpdates(
189
- token: string,
190
- chatId: string,
191
- botId: number | null,
192
- lastUpdateId: number,
193
- signal: AbortSignal,
194
- onMessage: (text: string) => void,
195
- onUpdateId: (id: number) => void,
186
+ token: string,
187
+ chatId: string,
188
+ botId: number | null,
189
+ lastUpdateId: number,
190
+ signal: AbortSignal,
191
+ onMessage: (text: string) => void,
192
+ onUpdateId: (id: number) => void,
196
193
  ): Promise<void> {
197
- while (!signal.aborted) {
198
- try {
199
- const res = await fetch(
200
- `${BASE_URL}${token}/getUpdates`,
201
- {
202
- method: "POST",
203
- headers: { "Content-Type": "application/json" },
204
- body: JSON.stringify({ offset: lastUpdateId + 1, timeout: 10 }),
205
- signal,
206
- },
207
- );
208
- if (!res.ok) {
209
- await new Promise((r) => setTimeout(r, 3000));
210
- continue;
211
- }
212
- const data = (await res.json()) as {
213
- ok: boolean;
214
- result: Array<{
215
- update_id: number;
216
- message?: {
217
- message_id: number;
218
- chat: { id: number };
219
- from?: { id: number; is_bot?: boolean };
220
- text?: string;
221
- };
222
- callback_query?: unknown;
223
- }>;
224
- };
225
- if (!data.ok) {
226
- await new Promise((r) => setTimeout(r, 2000));
227
- continue;
228
- }
229
- for (const u of data.result) {
230
- lastUpdateId = u.update_id;
231
- onUpdateId(lastUpdateId);
232
- // Skip callback queries (handled by telegram_ask)
233
- if (u.callback_query) continue;
234
- // Only process text messages from the configured chat, not from our bot
235
- if (
236
- u.message &&
237
- u.message.text &&
238
- String(u.message.chat.id) === chatId
239
- ) {
240
- if (u.message.from?.is_bot || u.message.from?.id === botId) {
241
- continue;
242
- }
243
- onMessage(u.message.text);
244
- }
245
- }
246
- } catch (e: unknown) {
247
- if (e instanceof Error && e.name === "AbortError") break;
248
- await new Promise((r) => setTimeout(r, 3000));
249
- }
250
- await new Promise((r) => setTimeout(r, 5000));
251
- }
194
+ while (!signal.aborted) {
195
+ try {
196
+ const res = await fetch(`${BASE_URL}${token}/getUpdates`, {
197
+ method: "POST",
198
+ headers: { "Content-Type": "application/json" },
199
+ body: JSON.stringify({ offset: lastUpdateId + 1, timeout: 10 }),
200
+ signal,
201
+ });
202
+ if (!res.ok) {
203
+ await new Promise((r) => setTimeout(r, 3000));
204
+ continue;
205
+ }
206
+ const data = (await res.json()) as {
207
+ ok: boolean;
208
+ result: Array<{
209
+ update_id: number;
210
+ message?: {
211
+ message_id: number;
212
+ chat: { id: number };
213
+ from?: { id: number; is_bot?: boolean };
214
+ text?: string;
215
+ };
216
+ callback_query?: unknown;
217
+ }>;
218
+ };
219
+ if (!data.ok) {
220
+ await new Promise((r) => setTimeout(r, 2000));
221
+ continue;
222
+ }
223
+ for (const u of data.result) {
224
+ lastUpdateId = u.update_id;
225
+ onUpdateId(lastUpdateId);
226
+ // Skip callback queries (handled by telegram_ask)
227
+ if (u.callback_query) continue;
228
+ // Only process text messages from the configured chat, not from our bot
229
+ if (
230
+ u.message &&
231
+ u.message.text &&
232
+ String(u.message.chat.id) === chatId
233
+ ) {
234
+ if (u.message.from?.is_bot || u.message.from?.id === botId) {
235
+ continue;
236
+ }
237
+ onMessage(u.message.text);
238
+ }
239
+ }
240
+ } catch (e: unknown) {
241
+ if (e instanceof Error && e.name === "AbortError") break;
242
+ await new Promise((r) => setTimeout(r, 3000));
243
+ }
244
+ await new Promise((r) => setTimeout(r, 5000));
245
+ }
252
246
  }
package/src/index.ts CHANGED
@@ -77,7 +77,11 @@ export default function telegramBridge(pi: ExtensionAPI) {
77
77
  // onMessage: forward to pi as user message
78
78
  async (text: string) => {
79
79
  pi.sendUserMessage(text);
80
- await sendMsg(`👂 Got it! Working on: _${text.slice(0, 100)}_`);
80
+ await sendMsg(
81
+ `👂 Got it! Working on: _${text.slice(0, 100)}_`,
82
+ undefined,
83
+ "Markdown",
84
+ );
81
85
  },
82
86
  // onUpdateId: persist offset
83
87
  (id: number) => {
@@ -226,9 +230,13 @@ export default function telegramBridge(pi: ExtensionAPI) {
226
230
  const opts = params.options ?? ["Yes", "No", "Explain"];
227
231
  const kb = opts.map((o: string) => [{ text: o, callback_data: o }]);
228
232
  const timeoutMs = (params.timeoutMinutes ?? 30) * 60000;
229
- const mid = await sendMsg(`❓ *${params.question}*`, {
230
- inline_keyboard: kb,
231
- });
233
+ const mid = await sendMsg(
234
+ `❓ *${params.question}*`,
235
+ {
236
+ inline_keyboard: kb,
237
+ },
238
+ "Markdown",
239
+ );
232
240
  const reply = await pollReply(mid, chatId, timeoutMs);
233
241
  if (!reply) {
234
242
  await sendMsg("⏰ No reply. Proceeding autonomously.");
@@ -296,7 +304,7 @@ export default function telegramBridge(pi: ExtensionAPI) {
296
304
  message += `\n_What should I do?_`;
297
305
 
298
306
  const kb = opts.map((o: string) => [{ text: o, callback_data: o }]);
299
- const mid = await sendMsg(message, { inline_keyboard: kb });
307
+ const mid = await sendMsg(message, { inline_keyboard: kb }, "Markdown");
300
308
  const reply = await pollReply(mid, chatId, timeoutMs);
301
309
 
302
310
  if (!reply) {
@@ -328,7 +336,7 @@ export default function telegramBridge(pi: ExtensionAPI) {
328
336
  action = "explain";
329
337
  }
330
338
 
331
- await sendMsg(`✅ Choice received: _${choice}_`);
339
+ await sendMsg(`✅ Choice received: _${choice}_`, undefined, "Markdown");
332
340
 
333
341
  return {
334
342
  content: [