@bytesbrains/pi-telegram-bridge 1.1.2 → 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 +2 -1
- package/src/helpers.ts +201 -206
- package/src/index.ts +15 -7
- package/src/templates.ts +26 -34
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bytesbrains/pi-telegram-bridge",
|
|
3
|
-
"version": "1.1.
|
|
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,246 +6,241 @@ const BASE_URL = "https://api.telegram.org/bot";
|
|
|
6
6
|
// ── Config ──────────────────────────────────────────────────────────
|
|
7
7
|
|
|
8
8
|
export function getToken(): string {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
24
|
-
|
|
23
|
+
method: string,
|
|
24
|
+
body: Record<string, unknown>,
|
|
25
25
|
): Promise<unknown> {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
42
|
-
|
|
41
|
+
text: string,
|
|
42
|
+
replyMarkup?: object,
|
|
43
|
+
parseMode: "Markdown" | "HTML" | undefined = undefined,
|
|
43
44
|
): Promise<number> {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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;
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
// ── Polling ─────────────────────────────────────────────────────────
|
|
58
59
|
|
|
59
60
|
export async function pollReply(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
sentId: number,
|
|
62
|
+
chatId: string,
|
|
63
|
+
timeoutMs: number,
|
|
63
64
|
): Promise<string | null> {
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
const token = getToken();
|
|
66
|
+
const deadline = Date.now() + timeoutMs;
|
|
66
67
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
+
}
|
|
87
88
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
await new Promise((r) => setTimeout(r, 2000));
|
|
144
|
-
}
|
|
145
|
-
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
|
|
146
144
|
}
|
|
147
145
|
|
|
148
146
|
// ── Listener State ──────────────────────────────────────────────────
|
|
149
147
|
|
|
150
148
|
export async function getBotId(): Promise<number | null> {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
149
|
+
try {
|
|
150
|
+
const r = (await telegramApi("getMe", {})) as { result: { id: number } };
|
|
151
|
+
return r.result.id;
|
|
152
|
+
} catch {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
157
155
|
}
|
|
158
156
|
|
|
159
157
|
export async function seedLastUpdateId(
|
|
160
|
-
|
|
161
|
-
|
|
158
|
+
lastUpdateId: number,
|
|
159
|
+
signal?: AbortSignal,
|
|
162
160
|
): Promise<number> {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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;
|
|
185
183
|
}
|
|
186
184
|
|
|
187
185
|
export async function pollUpdates(
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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,
|
|
195
193
|
): Promise<void> {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
}
|
|
249
|
-
await new Promise((r) => setTimeout(r, 5000));
|
|
250
|
-
}
|
|
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
|
+
}
|
|
251
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(
|
|
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(
|
|
230
|
-
|
|
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: [
|
|
@@ -381,7 +389,7 @@ export default function telegramBridge(pi: ExtensionAPI) {
|
|
|
381
389
|
const message = buildNotification(
|
|
382
390
|
params as unknown as import("./templates").NotifyInput,
|
|
383
391
|
);
|
|
384
|
-
const mid = await sendMsg(message);
|
|
392
|
+
const mid = await sendMsg(message, undefined, "HTML");
|
|
385
393
|
return {
|
|
386
394
|
content: [{ type: "text", text: `Notification sent (id:${mid})` }],
|
|
387
395
|
details: {},
|
package/src/templates.ts
CHANGED
|
@@ -2,33 +2,37 @@
|
|
|
2
2
|
* pi-telegram-bridge — Rich Message Templates
|
|
3
3
|
*
|
|
4
4
|
* Well-designed notification templates for Telegram.
|
|
5
|
-
*
|
|
5
|
+
* Uses HTML parse mode for reliable formatting with special characters.
|
|
6
6
|
* Supports: issue, PR, CI/CD, work progress, agent session, factory, alerts.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
// ── Helpers ─────────────────────────────────────────────────────────
|
|
10
10
|
|
|
11
|
-
const BAR = "━".repeat(
|
|
11
|
+
const BAR = "━".repeat(20);
|
|
12
12
|
|
|
13
13
|
function bold(s: string) {
|
|
14
|
-
return
|
|
14
|
+
return `<b>${s}</b>`;
|
|
15
15
|
}
|
|
16
16
|
function italic(s: string) {
|
|
17
|
-
return
|
|
17
|
+
return `<i>${s}</i>`;
|
|
18
18
|
}
|
|
19
19
|
function code(s: string) {
|
|
20
|
-
return
|
|
20
|
+
return `<code>${s}</code>`;
|
|
21
21
|
}
|
|
22
22
|
function strike(s: string) {
|
|
23
|
-
return
|
|
23
|
+
return `<s>${s}</s>`;
|
|
24
24
|
}
|
|
25
25
|
function labelValue(label: string, value: string) {
|
|
26
26
|
return `${bold(label + ":")} ${value}`;
|
|
27
27
|
}
|
|
28
|
+
function link(url: string, text: string) {
|
|
29
|
+
return `<a href="${url}">${text}</a>`;
|
|
30
|
+
}
|
|
28
31
|
function progressBar(pct: number, width = 12) {
|
|
29
32
|
const filled = Math.round((pct / 100) * width);
|
|
30
|
-
return
|
|
33
|
+
return `<code>${"█".repeat(filled)}${"░".repeat(width - filled)}</code> ${pct}%`;
|
|
31
34
|
}
|
|
35
|
+
|
|
32
36
|
// ── CI/CD Status Icons ─────────────────────────────────────────────
|
|
33
37
|
|
|
34
38
|
function jobIcon(status: string): string {
|
|
@@ -51,7 +55,7 @@ export interface IssueCard {
|
|
|
51
55
|
number: number;
|
|
52
56
|
title: string;
|
|
53
57
|
state: "open" | "closed";
|
|
54
|
-
status?: string;
|
|
58
|
+
status?: string;
|
|
55
59
|
assignee?: string;
|
|
56
60
|
labels?: string[];
|
|
57
61
|
milestone?: string;
|
|
@@ -86,12 +90,14 @@ export function issueCard(i: IssueCard): string {
|
|
|
86
90
|
msg += labelValue("Repo", italic(`${i.repo.owner}/${i.repo.name}`)) + "\n";
|
|
87
91
|
msg += `${BAR}\n`;
|
|
88
92
|
if (i.branch) msg += labelValue("Branch", code(i.branch)) + "\n";
|
|
89
|
-
if (i.pr) {
|
|
93
|
+
if (i.pr && i.url) {
|
|
90
94
|
msg +=
|
|
91
|
-
labelValue("PR",
|
|
95
|
+
labelValue("PR", `${link(i.url, `#${i.pr.number}`)} — ${i.pr.status}`) +
|
|
92
96
|
"\n";
|
|
97
|
+
} else if (i.pr) {
|
|
98
|
+
msg += labelValue("PR", `#${i.pr.number} — ${i.pr.status}`) + "\n";
|
|
93
99
|
}
|
|
94
|
-
if (i.url) msg += `\n
|
|
100
|
+
if (i.url) msg += `\n${link(i.url, "View on GitHub")}`;
|
|
95
101
|
return msg;
|
|
96
102
|
}
|
|
97
103
|
|
|
@@ -137,7 +143,6 @@ export function prCard(pr: PRCard): string {
|
|
|
137
143
|
msg += labelValue("Author", code(pr.author)) + "\n";
|
|
138
144
|
msg += labelValue("Branch", `${code(pr.base)} ← ${code(pr.head)}`) + "\n";
|
|
139
145
|
|
|
140
|
-
// CI status
|
|
141
146
|
if (pr.ci) {
|
|
142
147
|
const icon = pr.ci.status
|
|
143
148
|
? jobIcon(pr.ci.status)
|
|
@@ -152,7 +157,6 @@ export function prCard(pr: PRCard): string {
|
|
|
152
157
|
) + "\n";
|
|
153
158
|
}
|
|
154
159
|
|
|
155
|
-
// Reviews
|
|
156
160
|
if (pr.reviews) {
|
|
157
161
|
const parts: string[] = [];
|
|
158
162
|
if (pr.reviews.approved > 0)
|
|
@@ -174,7 +178,7 @@ export function prCard(pr: PRCard): string {
|
|
|
174
178
|
if (pr.breaking) msg += labelValue("Breaking", "⚠️ Yes") + "\n";
|
|
175
179
|
|
|
176
180
|
msg += `${BAR}\n`;
|
|
177
|
-
if (pr.url) msg +=
|
|
181
|
+
if (pr.url) msg += link(pr.url, "View on GitHub");
|
|
178
182
|
return msg;
|
|
179
183
|
}
|
|
180
184
|
|
|
@@ -184,8 +188,8 @@ export interface TaskProgress {
|
|
|
184
188
|
title: string;
|
|
185
189
|
emoji?: string;
|
|
186
190
|
status: "started" | "in-progress" | "paused" | "blocked" | "complete";
|
|
187
|
-
progress?: number;
|
|
188
|
-
elapsed?: string;
|
|
191
|
+
progress?: number;
|
|
192
|
+
elapsed?: string;
|
|
189
193
|
eta?: string;
|
|
190
194
|
details?: string[];
|
|
191
195
|
metrics?: Record<string, string>;
|
|
@@ -234,7 +238,7 @@ export interface CIPipeline {
|
|
|
234
238
|
workflow: string;
|
|
235
239
|
runNumber: number;
|
|
236
240
|
branch: string;
|
|
237
|
-
event: string;
|
|
241
|
+
event: string;
|
|
238
242
|
jobs: Array<{ name: string; status: string; duration?: string }>;
|
|
239
243
|
triggeredBy?: string;
|
|
240
244
|
url?: string;
|
|
@@ -254,7 +258,7 @@ export function ciPipeline(p: CIPipeline): string {
|
|
|
254
258
|
msg += `${jobIcon(j.status)} ${code(j.name)}${dur}\n`;
|
|
255
259
|
}
|
|
256
260
|
|
|
257
|
-
if (p.url) msg += `\n
|
|
261
|
+
if (p.url) msg += `\n${link(p.url, "View Pipeline")}`;
|
|
258
262
|
return msg;
|
|
259
263
|
}
|
|
260
264
|
|
|
@@ -389,7 +393,7 @@ export function ack(text: string, emoji = "✅"): string {
|
|
|
389
393
|
return `${emoji} ${italic(text)}`;
|
|
390
394
|
}
|
|
391
395
|
|
|
392
|
-
// ── Decision Card
|
|
396
|
+
// ── Decision Card ───────────────────────────────────────────────────
|
|
393
397
|
|
|
394
398
|
export interface DecisionCard {
|
|
395
399
|
question: string;
|
|
@@ -415,7 +419,7 @@ export function decisionCard(d: DecisionCard): string {
|
|
|
415
419
|
if (d.command) msg += labelValue("Command", code(d.command)) + "\n";
|
|
416
420
|
if (d.reason) msg += labelValue("Reason", d.reason) + "\n";
|
|
417
421
|
msg += `\n${bold("❓ " + d.question)}\n`;
|
|
418
|
-
msg += `\n${d.options.map((o, i) => `${i + 1}
|
|
422
|
+
msg += `\n${d.options.map((o, i) => `${i + 1}. ${o}`).join("\n")}`;
|
|
419
423
|
return msg;
|
|
420
424
|
}
|
|
421
425
|
|
|
@@ -483,14 +487,14 @@ export function diffSummary(d: DiffSummary): string {
|
|
|
483
487
|
msg += ` • ${h}\n`;
|
|
484
488
|
}
|
|
485
489
|
}
|
|
486
|
-
if (d.url) msg += `\n
|
|
490
|
+
if (d.url) msg += `\n${link(d.url, "View Diff")}`;
|
|
487
491
|
return msg;
|
|
488
492
|
}
|
|
489
493
|
|
|
490
494
|
// ── Quick Pickers (for telegram_ask buttons) ────────────────────────
|
|
491
495
|
|
|
492
496
|
export function quickActions(label: string, actions: string[]): string {
|
|
493
|
-
return `${bold(label)}\n${actions.map((a, i) => `${i + 1}
|
|
497
|
+
return `${bold(label)}\n${actions.map((a, i) => `${i + 1}. ${code(a)}`).join(" \n")}`;
|
|
494
498
|
}
|
|
495
499
|
|
|
496
500
|
export function yesNoExplain(prompt: string): string {
|
|
@@ -499,7 +503,6 @@ export function yesNoExplain(prompt: string): string {
|
|
|
499
503
|
|
|
500
504
|
// ── Build Notification (main dispatch for telegram_notify tool) ────
|
|
501
505
|
|
|
502
|
-
// Flattened input from the tool; only the relevant subset is used per kind.
|
|
503
506
|
export interface NotifyInput {
|
|
504
507
|
kind:
|
|
505
508
|
| "issue"
|
|
@@ -513,7 +516,6 @@ export interface NotifyInput {
|
|
|
513
516
|
| "diff"
|
|
514
517
|
| "decision"
|
|
515
518
|
| "ack";
|
|
516
|
-
// issue
|
|
517
519
|
issueNumber?: number;
|
|
518
520
|
issueTitle?: string;
|
|
519
521
|
issueState?: "open" | "closed";
|
|
@@ -527,7 +529,6 @@ export interface NotifyInput {
|
|
|
527
529
|
issuePRNumber?: number;
|
|
528
530
|
issuePRStatus?: string;
|
|
529
531
|
issueUrl?: string;
|
|
530
|
-
// pr
|
|
531
532
|
prNumber?: number;
|
|
532
533
|
prTitle?: string;
|
|
533
534
|
prState?: "open" | "closed" | "merged" | "draft";
|
|
@@ -549,7 +550,6 @@ export interface NotifyInput {
|
|
|
549
550
|
prBreaking?: boolean;
|
|
550
551
|
prDraft?: boolean;
|
|
551
552
|
prUrl?: string;
|
|
552
|
-
// task
|
|
553
553
|
taskTitle?: string;
|
|
554
554
|
taskEmoji?: string;
|
|
555
555
|
taskStatus?: "started" | "in-progress" | "paused" | "blocked" | "complete";
|
|
@@ -558,7 +558,6 @@ export interface NotifyInput {
|
|
|
558
558
|
taskEta?: string;
|
|
559
559
|
taskDetails?: string[];
|
|
560
560
|
taskMetrics?: Record<string, string>;
|
|
561
|
-
// pipeline
|
|
562
561
|
pipelineWorkflow?: string;
|
|
563
562
|
pipelineRunNumber?: number;
|
|
564
563
|
pipelineBranch?: string;
|
|
@@ -566,7 +565,6 @@ export interface NotifyInput {
|
|
|
566
565
|
pipelineJobs?: Array<{ name: string; status: string; duration?: string }>;
|
|
567
566
|
pipelineTriggeredBy?: string;
|
|
568
567
|
pipelineUrl?: string;
|
|
569
|
-
// session
|
|
570
568
|
sessionName?: string;
|
|
571
569
|
sessionBranch?: string;
|
|
572
570
|
sessionModel?: string;
|
|
@@ -575,14 +573,12 @@ export interface NotifyInput {
|
|
|
575
573
|
sessionTokensTotal?: number;
|
|
576
574
|
sessionDuration?: string;
|
|
577
575
|
sessionStatus?: "idle" | "streaming" | "tool-exec" | "waiting";
|
|
578
|
-
// alert
|
|
579
576
|
alertLevel?: "info" | "warning" | "error" | "critical";
|
|
580
577
|
alertTitle?: string;
|
|
581
578
|
alertSource?: string;
|
|
582
579
|
alertDetail?: string;
|
|
583
580
|
alertAction?: string;
|
|
584
581
|
alertStack?: string;
|
|
585
|
-
// factory-job
|
|
586
582
|
jobId?: string;
|
|
587
583
|
jobMode?: "single" | "chain" | "parallel";
|
|
588
584
|
jobStatus?: "pending" | "running" | "success" | "failure" | "cancelled";
|
|
@@ -591,28 +587,24 @@ export interface NotifyInput {
|
|
|
591
587
|
jobEnvironment?: string;
|
|
592
588
|
jobDuration?: string;
|
|
593
589
|
jobWorker?: string;
|
|
594
|
-
// standup
|
|
595
590
|
standupDate?: string;
|
|
596
591
|
standupAgent?: string;
|
|
597
592
|
standupWorkedOn?: string[];
|
|
598
593
|
standupNextUp?: string[];
|
|
599
594
|
standupBlockers?: string[];
|
|
600
595
|
standupMetrics?: Record<string, string>;
|
|
601
|
-
// diff
|
|
602
596
|
diffTitle?: string;
|
|
603
597
|
diffAdded?: number;
|
|
604
598
|
diffRemoved?: number;
|
|
605
599
|
diffFiles?: number;
|
|
606
600
|
diffHighlights?: string[];
|
|
607
601
|
diffUrl?: string;
|
|
608
|
-
// decision
|
|
609
602
|
decisionQuestion?: string;
|
|
610
603
|
decisionContext?: string;
|
|
611
604
|
decisionOptions?: string[];
|
|
612
605
|
decisionRisk?: "low" | "medium" | "high" | "critical";
|
|
613
606
|
decisionCommand?: string;
|
|
614
607
|
decisionReason?: string;
|
|
615
|
-
// ack
|
|
616
608
|
ackText?: string;
|
|
617
609
|
ackEmoji?: string;
|
|
618
610
|
}
|