@bytesbrains/pi-telegram-bridge 1.0.2 → 1.1.1
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 +1 -1
- package/src/__tests__/index.test.ts +503 -483
- package/src/index.ts +403 -346
- package/src/templates.ts +789 -0
- package/src/tools/telegram.ts +201 -8
package/src/index.ts
CHANGED
|
@@ -9,378 +9,435 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
11
11
|
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
12
|
+
getToken,
|
|
13
|
+
getChatId,
|
|
14
|
+
telegramApi,
|
|
15
|
+
sendMsg,
|
|
16
|
+
pollReply,
|
|
17
|
+
getBotId,
|
|
18
|
+
seedLastUpdateId,
|
|
19
|
+
pollUpdates,
|
|
20
20
|
} from "./helpers";
|
|
21
21
|
import {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
listenSchema,
|
|
23
|
+
sendSchema,
|
|
24
|
+
askSchema,
|
|
25
|
+
statusSchema,
|
|
26
|
+
overrideSchema,
|
|
27
|
+
notifySchema,
|
|
27
28
|
} from "./tools/telegram";
|
|
28
29
|
|
|
29
30
|
export default function telegramBridge(pi: ExtensionAPI) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
let botId: number | null = null;
|
|
32
|
+
let listenerAbort: AbortController | null = null;
|
|
33
|
+
let lastUpdateId = 0;
|
|
33
34
|
|
|
34
|
-
|
|
35
|
+
// ── Session lifecycle ───────────────────────────────────────────
|
|
35
36
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
37
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
38
|
+
// Restore lastUpdateId from session state
|
|
39
|
+
for (const entry of ctx.sessionManager.getEntries()) {
|
|
40
|
+
if (entry.type === "custom" && entry.customType === "tg-last-update") {
|
|
41
|
+
lastUpdateId = (entry.data as { update_id: number }).update_id;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// Start background listener
|
|
45
|
+
startListener();
|
|
46
|
+
});
|
|
46
47
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
pi.on("session_shutdown", () => {
|
|
49
|
+
listenerAbort?.abort();
|
|
50
|
+
listenerAbort = null;
|
|
51
|
+
});
|
|
51
52
|
|
|
52
|
-
|
|
53
|
+
// ── Background listener ─────────────────────────────────────────
|
|
53
54
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
55
|
+
async function startListener() {
|
|
56
|
+
if (listenerAbort) return; // already running
|
|
57
|
+
listenerAbort = new AbortController();
|
|
58
|
+
const signal = listenerAbort.signal;
|
|
59
|
+
const token = getToken();
|
|
60
|
+
const chatId = getChatId();
|
|
60
61
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
// Get bot's own ID once to filter out self-messages
|
|
63
|
+
if (!botId) {
|
|
64
|
+
botId = await getBotId();
|
|
65
|
+
}
|
|
65
66
|
|
|
66
|
-
|
|
67
|
-
|
|
67
|
+
// Seed lastUpdateId
|
|
68
|
+
lastUpdateId = await seedLastUpdateId(lastUpdateId, signal);
|
|
68
69
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
70
|
+
// Fire and forget — poll in background
|
|
71
|
+
pollUpdates(
|
|
72
|
+
token,
|
|
73
|
+
chatId,
|
|
74
|
+
botId,
|
|
75
|
+
lastUpdateId,
|
|
76
|
+
signal,
|
|
77
|
+
// onMessage: forward to pi as user message
|
|
78
|
+
async (text: string) => {
|
|
79
|
+
pi.sendUserMessage(text);
|
|
80
|
+
await sendMsg(`👂 Got it! Working on: _${text.slice(0, 100)}_`);
|
|
81
|
+
},
|
|
82
|
+
// onUpdateId: persist offset
|
|
83
|
+
(id: number) => {
|
|
84
|
+
lastUpdateId = id;
|
|
85
|
+
pi.appendEntry("tg-last-update", { update_id: id });
|
|
86
|
+
},
|
|
87
|
+
);
|
|
88
|
+
}
|
|
88
89
|
|
|
89
|
-
|
|
90
|
+
// ── Tools ───────────────────────────────────────────────────────
|
|
90
91
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
92
|
+
// telegram_listen
|
|
93
|
+
pi.registerTool({
|
|
94
|
+
name: "telegram_listen",
|
|
95
|
+
label: "Telegram Listen",
|
|
96
|
+
description:
|
|
97
|
+
"Check Telegram for any new inbound messages from the human. Returns the latest message text, or indicates no new messages.",
|
|
98
|
+
parameters: listenSchema,
|
|
99
|
+
async execute() {
|
|
100
|
+
try {
|
|
101
|
+
const token = getToken();
|
|
102
|
+
const chatId = getChatId();
|
|
103
|
+
if (!botId) botId = await getBotId();
|
|
103
104
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
105
|
+
const res = await fetch(
|
|
106
|
+
`https://api.telegram.org/bot${token}/getUpdates`,
|
|
107
|
+
{
|
|
108
|
+
method: "POST",
|
|
109
|
+
headers: { "Content-Type": "application/json" },
|
|
110
|
+
body: JSON.stringify({ offset: lastUpdateId + 1, timeout: 5 }),
|
|
111
|
+
},
|
|
112
|
+
);
|
|
113
|
+
if (!res.ok)
|
|
114
|
+
return {
|
|
115
|
+
content: [{ type: "text", text: "Could not reach Telegram." }],
|
|
116
|
+
details: { message: "" },
|
|
117
|
+
isError: true,
|
|
118
|
+
};
|
|
117
119
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
120
|
+
const data = (await res.json()) as {
|
|
121
|
+
ok: boolean;
|
|
122
|
+
result: Array<{
|
|
123
|
+
update_id: number;
|
|
124
|
+
message?: {
|
|
125
|
+
message_id: number;
|
|
126
|
+
chat: { id: number };
|
|
127
|
+
from?: { id: number; is_bot?: boolean };
|
|
128
|
+
text?: string;
|
|
129
|
+
};
|
|
130
|
+
callback_query?: unknown;
|
|
131
|
+
}>;
|
|
132
|
+
};
|
|
133
|
+
if (!data.ok)
|
|
134
|
+
return {
|
|
135
|
+
content: [{ type: "text", text: "Telegram API error." }],
|
|
136
|
+
details: { message: "" },
|
|
137
|
+
isError: true,
|
|
138
|
+
};
|
|
136
139
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
140
|
+
for (const u of data.result) {
|
|
141
|
+
lastUpdateId = u.update_id;
|
|
142
|
+
pi.appendEntry("tg-last-update", { update_id: lastUpdateId });
|
|
143
|
+
if (u.callback_query) continue;
|
|
144
|
+
if (
|
|
145
|
+
u.message &&
|
|
146
|
+
u.message.text &&
|
|
147
|
+
String(u.message.chat.id) === chatId
|
|
148
|
+
) {
|
|
149
|
+
if (u.message.from?.is_bot || u.message.from?.id === botId)
|
|
150
|
+
continue;
|
|
151
|
+
return {
|
|
152
|
+
content: [
|
|
153
|
+
{
|
|
154
|
+
type: "text",
|
|
155
|
+
text: `📩 New message: "${u.message.text}"`,
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
details: { message: u.message.text },
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
content: [{ type: "text", text: "No new messages." }],
|
|
164
|
+
details: { message: "" },
|
|
165
|
+
};
|
|
166
|
+
} catch (e: unknown) {
|
|
167
|
+
return {
|
|
168
|
+
content: [
|
|
169
|
+
{
|
|
170
|
+
type: "text",
|
|
171
|
+
text: `Failed: ${e instanceof Error ? e.message : e}`,
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
details: { message: "" },
|
|
175
|
+
isError: true,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
});
|
|
173
180
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
181
|
+
// telegram_send
|
|
182
|
+
pi.registerTool({
|
|
183
|
+
name: "telegram_send",
|
|
184
|
+
label: "Telegram Send",
|
|
185
|
+
description: "Send a one-way message to Telegram. Use for status updates.",
|
|
186
|
+
parameters: sendSchema,
|
|
187
|
+
async execute(_id: string, params: { message: string }) {
|
|
188
|
+
try {
|
|
189
|
+
const mid = await sendMsg(params.message);
|
|
190
|
+
return {
|
|
191
|
+
content: [{ type: "text", text: `Sent (id:${mid})` }],
|
|
192
|
+
details: {},
|
|
193
|
+
};
|
|
194
|
+
} catch (e: unknown) {
|
|
195
|
+
return {
|
|
196
|
+
content: [
|
|
197
|
+
{
|
|
198
|
+
type: "text",
|
|
199
|
+
text: `Failed: ${e instanceof Error ? e.message : e}`,
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
details: {},
|
|
203
|
+
isError: true,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
});
|
|
198
208
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
209
|
+
// telegram_ask
|
|
210
|
+
pi.registerTool({
|
|
211
|
+
name: "telegram_ask",
|
|
212
|
+
label: "Telegram Ask",
|
|
213
|
+
description:
|
|
214
|
+
"Ask a question on Telegram and WAIT for a human reply. BLOCKS until you answer. Use when you need human input.",
|
|
215
|
+
parameters: askSchema,
|
|
216
|
+
async execute(
|
|
217
|
+
_id: string,
|
|
218
|
+
params: {
|
|
219
|
+
question: string;
|
|
220
|
+
options?: string[];
|
|
221
|
+
timeoutMinutes?: number;
|
|
222
|
+
},
|
|
223
|
+
) {
|
|
224
|
+
try {
|
|
225
|
+
const chatId = getChatId();
|
|
226
|
+
const opts = params.options ?? ["Yes", "No", "Explain"];
|
|
227
|
+
const kb = opts.map((o: string) => [{ text: o, callback_data: o }]);
|
|
228
|
+
const timeoutMs = (params.timeoutMinutes ?? 30) * 60000;
|
|
229
|
+
const mid = await sendMsg(`❓ *${params.question}*`, {
|
|
230
|
+
inline_keyboard: kb,
|
|
231
|
+
});
|
|
232
|
+
const reply = await pollReply(mid, chatId, timeoutMs);
|
|
233
|
+
if (!reply) {
|
|
234
|
+
await sendMsg("⏰ No reply. Proceeding autonomously.");
|
|
235
|
+
return {
|
|
236
|
+
content: [
|
|
237
|
+
{ type: "text", text: "Timeout — proceeding autonomously" },
|
|
238
|
+
],
|
|
239
|
+
details: { timedOut: true, answer: "" },
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
content: [{ type: "text", text: `Reply: "${reply}"` }],
|
|
244
|
+
details: { timedOut: false, answer: reply },
|
|
245
|
+
};
|
|
246
|
+
} catch (e: unknown) {
|
|
247
|
+
return {
|
|
248
|
+
content: [
|
|
249
|
+
{
|
|
250
|
+
type: "text",
|
|
251
|
+
text: `Failed: ${e instanceof Error ? e.message : e}`,
|
|
252
|
+
},
|
|
253
|
+
],
|
|
254
|
+
details: { timedOut: false, answer: "" },
|
|
255
|
+
isError: true,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
});
|
|
249
260
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
261
|
+
// telegram_override
|
|
262
|
+
pi.registerTool({
|
|
263
|
+
name: "telegram_override",
|
|
264
|
+
label: "Telegram Override",
|
|
265
|
+
description:
|
|
266
|
+
"Ask a human on Telegram to approve or reject a blocked action. " +
|
|
267
|
+
"Use when the supervisor blocks a dangerous command (force push, " +
|
|
268
|
+
"destructive git ops, file deletion, etc.) and you need human approval. " +
|
|
269
|
+
"Returns the human's choice so you can call supervisor_override or abort.",
|
|
270
|
+
parameters: overrideSchema,
|
|
271
|
+
async execute(
|
|
272
|
+
_id: string,
|
|
273
|
+
params: {
|
|
274
|
+
command: string;
|
|
275
|
+
reason: string;
|
|
276
|
+
context?: string;
|
|
277
|
+
options?: string[];
|
|
278
|
+
timeoutMinutes?: number;
|
|
279
|
+
},
|
|
280
|
+
) {
|
|
281
|
+
try {
|
|
282
|
+
const chatId = getChatId();
|
|
283
|
+
const opts = params.options ?? [
|
|
284
|
+
"Yes, proceed",
|
|
285
|
+
"No, cancel",
|
|
286
|
+
"Explain more",
|
|
287
|
+
];
|
|
288
|
+
const timeoutMs = (params.timeoutMinutes ?? 30) * 60000;
|
|
278
289
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
290
|
+
let message = `🛑 *Supervisor blocked an action*\n\n`;
|
|
291
|
+
message += `*Command:* \`${params.command.slice(0, 200)}\`\n`;
|
|
292
|
+
message += `*Reason:* ${params.reason.slice(0, 300)}\n`;
|
|
293
|
+
if (params.context) {
|
|
294
|
+
message += `*Context:* ${params.context.slice(0, 300)}\n`;
|
|
295
|
+
}
|
|
296
|
+
message += `\n_What should I do?_`;
|
|
286
297
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
298
|
+
const kb = opts.map((o: string) => [{ text: o, callback_data: o }]);
|
|
299
|
+
const mid = await sendMsg(message, { inline_keyboard: kb });
|
|
300
|
+
const reply = await pollReply(mid, chatId, timeoutMs);
|
|
290
301
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
302
|
+
if (!reply) {
|
|
303
|
+
await sendMsg("⏰ No reply to override request. Aborting action.");
|
|
304
|
+
return {
|
|
305
|
+
content: [
|
|
306
|
+
{
|
|
307
|
+
type: "text",
|
|
308
|
+
text: "Timeout — no human response. Action aborted.",
|
|
309
|
+
},
|
|
310
|
+
],
|
|
311
|
+
details: {
|
|
312
|
+
timedOut: true,
|
|
313
|
+
action: "abort",
|
|
314
|
+
command: params.command,
|
|
315
|
+
choice: "",
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
}
|
|
307
319
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
320
|
+
// Map the reply to an action
|
|
321
|
+
const choice = reply.trim();
|
|
322
|
+
let action: string;
|
|
323
|
+
if (choice === opts[0]) {
|
|
324
|
+
action = "proceed";
|
|
325
|
+
} else if (choice === opts[1]) {
|
|
326
|
+
action = "abort";
|
|
327
|
+
} else {
|
|
328
|
+
action = "explain";
|
|
329
|
+
}
|
|
318
330
|
|
|
319
|
-
|
|
331
|
+
await sendMsg(`✅ Choice received: _${choice}_`);
|
|
320
332
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
333
|
+
return {
|
|
334
|
+
content: [
|
|
335
|
+
{
|
|
336
|
+
type: "text",
|
|
337
|
+
text: `Human chose: "${choice}" → action: ${action}`,
|
|
338
|
+
},
|
|
339
|
+
],
|
|
340
|
+
details: {
|
|
341
|
+
choice,
|
|
342
|
+
action,
|
|
343
|
+
command: params.command,
|
|
344
|
+
timedOut: false,
|
|
345
|
+
},
|
|
346
|
+
};
|
|
347
|
+
} catch (e: unknown) {
|
|
348
|
+
return {
|
|
349
|
+
content: [
|
|
350
|
+
{
|
|
351
|
+
type: "text",
|
|
352
|
+
text: `Failed: ${e instanceof Error ? e.message : e}`,
|
|
353
|
+
},
|
|
354
|
+
],
|
|
355
|
+
details: {
|
|
356
|
+
timedOut: false,
|
|
357
|
+
action: "",
|
|
358
|
+
command: "",
|
|
359
|
+
choice: "",
|
|
360
|
+
},
|
|
361
|
+
isError: true,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
},
|
|
365
|
+
});
|
|
348
366
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
367
|
+
// telegram_notify — rich formatted notifications
|
|
368
|
+
pi.registerTool({
|
|
369
|
+
name: "telegram_notify",
|
|
370
|
+
label: "Telegram Notify",
|
|
371
|
+
description:
|
|
372
|
+
"Send a rich, well-formatted notification to Telegram. " +
|
|
373
|
+
"Choose a kind: issue, pr, task, pipeline, session, alert, " +
|
|
374
|
+
"factory-job, standup, diff, decision, or ack. " +
|
|
375
|
+
"Fill the matching fields for that kind. " +
|
|
376
|
+
"All other fields are ignored — only provide fields relevant to your chosen kind.",
|
|
377
|
+
parameters: notifySchema,
|
|
378
|
+
async execute(_id: string, params: Record<string, unknown>) {
|
|
379
|
+
try {
|
|
380
|
+
const { buildNotification } = await import("./templates");
|
|
381
|
+
const message = buildNotification(
|
|
382
|
+
params as unknown as import("./templates").NotifyInput,
|
|
383
|
+
);
|
|
384
|
+
const mid = await sendMsg(message);
|
|
385
|
+
return {
|
|
386
|
+
content: [{ type: "text", text: `Notification sent (id:${mid})` }],
|
|
387
|
+
details: {},
|
|
388
|
+
};
|
|
389
|
+
} catch (e: unknown) {
|
|
390
|
+
return {
|
|
391
|
+
content: [
|
|
392
|
+
{
|
|
393
|
+
type: "text",
|
|
394
|
+
text: `Failed: ${e instanceof Error ? e.message : e}`,
|
|
395
|
+
},
|
|
396
|
+
],
|
|
397
|
+
details: {},
|
|
398
|
+
isError: true,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
},
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
// telegram_status
|
|
405
|
+
pi.registerTool({
|
|
406
|
+
name: "telegram_status",
|
|
407
|
+
label: "Telegram Status",
|
|
408
|
+
description: "Check if the Telegram bridge is configured and running.",
|
|
409
|
+
parameters: statusSchema,
|
|
410
|
+
async execute() {
|
|
411
|
+
if (!process.env.TELEGRAM_BOT_TOKEN || !process.env.TELEGRAM_CHAT_ID) {
|
|
412
|
+
return {
|
|
413
|
+
content: [
|
|
414
|
+
{
|
|
415
|
+
type: "text",
|
|
416
|
+
text: "Not configured. Set TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID.",
|
|
417
|
+
},
|
|
418
|
+
],
|
|
419
|
+
details: {},
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
try {
|
|
423
|
+
const r = (await telegramApi("getMe", {})) as {
|
|
424
|
+
result: { username: string };
|
|
425
|
+
};
|
|
426
|
+
return {
|
|
427
|
+
content: [
|
|
428
|
+
{
|
|
429
|
+
type: "text",
|
|
430
|
+
text: `Active. Bot: @${r.result.username} (listener: ${listenerAbort ? "🟢 running" : "🔴 stopped"})`,
|
|
431
|
+
},
|
|
432
|
+
],
|
|
433
|
+
details: {},
|
|
434
|
+
};
|
|
435
|
+
} catch {
|
|
436
|
+
return {
|
|
437
|
+
content: [{ type: "text", text: "Token set but unreachable." }],
|
|
438
|
+
details: {},
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
},
|
|
442
|
+
});
|
|
386
443
|
}
|