@delt/claude-alarm 0.4.8 → 0.5.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 +215 -182
- package/dist/channel/server.js +1 -1
- package/dist/channel/server.js.map +1 -1
- package/dist/cli.js +274 -3
- package/dist/cli.js.map +1 -1
- package/dist/dashboard/index.html +130 -15
- package/dist/hub/server.js +266 -3
- package/dist/hub/server.js.map +1 -1
- package/dist/index.d.ts +43 -1
- package/dist/index.js +266 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/dashboard/index.html +1339 -1224
package/dist/cli.js
CHANGED
|
@@ -48,7 +48,7 @@ function loadConfig() {
|
|
|
48
48
|
try {
|
|
49
49
|
const raw = fs.readFileSync(CONFIG_FILE, "utf-8");
|
|
50
50
|
const parsed = JSON.parse(raw);
|
|
51
|
-
config2 = { ...DEFAULT_CONFIG, ...parsed, hub: { ...DEFAULT_CONFIG.hub, ...parsed.hub } };
|
|
51
|
+
config2 = { ...DEFAULT_CONFIG, ...parsed, hub: { ...DEFAULT_CONFIG.hub, ...parsed.hub }, ...parsed.telegram ? { telegram: parsed.telegram } : {} };
|
|
52
52
|
} catch {
|
|
53
53
|
config2 = { ...DEFAULT_CONFIG, hub: { ...DEFAULT_CONFIG.hub } };
|
|
54
54
|
}
|
|
@@ -189,12 +189,17 @@ var init_notifier = __esm({
|
|
|
189
189
|
desktopEnabled = true;
|
|
190
190
|
notificationSettingsOpened = false;
|
|
191
191
|
dashboardUrl;
|
|
192
|
+
telegramBot;
|
|
192
193
|
configure(options) {
|
|
193
194
|
if (options.dashboardUrl) this.dashboardUrl = options.dashboardUrl;
|
|
194
195
|
if (options.desktop !== void 0) this.desktopEnabled = options.desktop;
|
|
195
196
|
if (options.webhooks) this.webhooks = options.webhooks;
|
|
197
|
+
if (options.telegramBot) this.telegramBot = options.telegramBot;
|
|
196
198
|
}
|
|
197
199
|
async notify(title, message, level = "info") {
|
|
200
|
+
await this.notifyWithSession(void 0, void 0, title, message, level);
|
|
201
|
+
}
|
|
202
|
+
async notifyWithSession(sessionId2, sessionLabel, title, message, level = "info") {
|
|
198
203
|
const promises = [];
|
|
199
204
|
if (this.desktopEnabled) {
|
|
200
205
|
promises.push(this.sendDesktop(title, message, level));
|
|
@@ -202,6 +207,9 @@ var init_notifier = __esm({
|
|
|
202
207
|
for (const webhook of this.webhooks) {
|
|
203
208
|
promises.push(this.sendWebhook(webhook, title, message, level));
|
|
204
209
|
}
|
|
210
|
+
if (this.telegramBot && sessionId2 && sessionLabel) {
|
|
211
|
+
promises.push(this.telegramBot.sendNotification(sessionId2, sessionLabel, title, message));
|
|
212
|
+
}
|
|
205
213
|
await Promise.allSettled(promises);
|
|
206
214
|
}
|
|
207
215
|
async sendDesktop(title, message, _level) {
|
|
@@ -284,6 +292,183 @@ var init_notifier = __esm({
|
|
|
284
292
|
}
|
|
285
293
|
});
|
|
286
294
|
|
|
295
|
+
// src/hub/telegram.ts
|
|
296
|
+
var TELEGRAM_API, TelegramBot;
|
|
297
|
+
var init_telegram = __esm({
|
|
298
|
+
"src/hub/telegram.ts"() {
|
|
299
|
+
"use strict";
|
|
300
|
+
init_logger();
|
|
301
|
+
TELEGRAM_API = "https://api.telegram.org/bot";
|
|
302
|
+
TelegramBot = class {
|
|
303
|
+
config;
|
|
304
|
+
offset = 0;
|
|
305
|
+
polling = false;
|
|
306
|
+
pollTimer = null;
|
|
307
|
+
// message_id -> sessionId mapping for reply-based routing
|
|
308
|
+
messageSessionMap = /* @__PURE__ */ new Map();
|
|
309
|
+
// Callback: when a message arrives from Telegram for a session
|
|
310
|
+
onMessageToSession;
|
|
311
|
+
// Callback: get current sessions list
|
|
312
|
+
getSessions;
|
|
313
|
+
// Callback: when user needs to select a session (sends inline keyboard)
|
|
314
|
+
pendingMessages = /* @__PURE__ */ new Map();
|
|
315
|
+
// chatId -> pending message text
|
|
316
|
+
constructor(config2) {
|
|
317
|
+
this.config = config2;
|
|
318
|
+
}
|
|
319
|
+
get apiUrl() {
|
|
320
|
+
return `${TELEGRAM_API}${this.config.botToken}`;
|
|
321
|
+
}
|
|
322
|
+
/** Send a notification message to Telegram */
|
|
323
|
+
async sendNotification(sessionId2, sessionLabel, title, message) {
|
|
324
|
+
const text = `[${sessionLabel}] ${title}
|
|
325
|
+
${message}`;
|
|
326
|
+
const result = await this.sendMessage(text);
|
|
327
|
+
if (result?.message_id) {
|
|
328
|
+
this.messageSessionMap.set(result.message_id, sessionId2);
|
|
329
|
+
if (this.messageSessionMap.size > 200) {
|
|
330
|
+
const keys = [...this.messageSessionMap.keys()];
|
|
331
|
+
for (let i = 0; i < keys.length - 200; i++) {
|
|
332
|
+
this.messageSessionMap.delete(keys[i]);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
/** Send a text message to the configured chat */
|
|
338
|
+
async sendMessage(text, replyToMessageId, replyMarkup) {
|
|
339
|
+
try {
|
|
340
|
+
const body = {
|
|
341
|
+
chat_id: this.config.chatId,
|
|
342
|
+
text,
|
|
343
|
+
parse_mode: "HTML"
|
|
344
|
+
};
|
|
345
|
+
if (replyToMessageId) body.reply_to_message_id = replyToMessageId;
|
|
346
|
+
if (replyMarkup) body.reply_markup = replyMarkup;
|
|
347
|
+
const res = await fetch(`${this.apiUrl}/sendMessage`, {
|
|
348
|
+
method: "POST",
|
|
349
|
+
headers: { "Content-Type": "application/json" },
|
|
350
|
+
body: JSON.stringify(body)
|
|
351
|
+
});
|
|
352
|
+
if (!res.ok) {
|
|
353
|
+
const err = await res.text();
|
|
354
|
+
logger.warn(`Telegram sendMessage failed: ${res.status} ${err}`);
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
const data = await res.json();
|
|
358
|
+
return data.ok ? data.result : null;
|
|
359
|
+
} catch (err) {
|
|
360
|
+
logger.warn(`Telegram sendMessage error: ${err.message}`);
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
/** Start long polling for incoming messages */
|
|
365
|
+
startPolling() {
|
|
366
|
+
if (this.polling) return;
|
|
367
|
+
this.polling = true;
|
|
368
|
+
logger.info("Telegram bot polling started");
|
|
369
|
+
this.poll();
|
|
370
|
+
}
|
|
371
|
+
/** Stop polling */
|
|
372
|
+
stopPolling() {
|
|
373
|
+
this.polling = false;
|
|
374
|
+
if (this.pollTimer) {
|
|
375
|
+
clearTimeout(this.pollTimer);
|
|
376
|
+
this.pollTimer = null;
|
|
377
|
+
}
|
|
378
|
+
logger.info("Telegram bot polling stopped");
|
|
379
|
+
}
|
|
380
|
+
async poll() {
|
|
381
|
+
if (!this.polling) return;
|
|
382
|
+
try {
|
|
383
|
+
const res = await fetch(`${this.apiUrl}/getUpdates?offset=${this.offset}&timeout=30`, {
|
|
384
|
+
signal: AbortSignal.timeout(35e3)
|
|
385
|
+
});
|
|
386
|
+
if (!res.ok) {
|
|
387
|
+
logger.warn(`Telegram getUpdates failed: ${res.status}`);
|
|
388
|
+
this.scheduleNextPoll(5e3);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
const data = await res.json();
|
|
392
|
+
if (data.ok && data.result.length > 0) {
|
|
393
|
+
for (const update of data.result) {
|
|
394
|
+
this.offset = update.update_id + 1;
|
|
395
|
+
if (update.message) {
|
|
396
|
+
this.handleIncomingMessage(update.message);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
} catch (err) {
|
|
401
|
+
if (err.name !== "AbortError") {
|
|
402
|
+
logger.warn(`Telegram poll error: ${err.message}`);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
this.scheduleNextPoll(1e3);
|
|
406
|
+
}
|
|
407
|
+
scheduleNextPoll(delay) {
|
|
408
|
+
if (!this.polling) return;
|
|
409
|
+
this.pollTimer = setTimeout(() => this.poll(), delay);
|
|
410
|
+
}
|
|
411
|
+
handleIncomingMessage(msg) {
|
|
412
|
+
if (!msg.text) return;
|
|
413
|
+
if (String(msg.chat.id) !== String(this.config.chatId)) return;
|
|
414
|
+
const text = msg.text.trim();
|
|
415
|
+
if (msg.reply_to_message) {
|
|
416
|
+
const sessionId2 = this.messageSessionMap.get(msg.reply_to_message.message_id);
|
|
417
|
+
if (sessionId2) {
|
|
418
|
+
this.deliverToSession(sessionId2, text);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
const selectMatch = text.match(/^\/s_(\d+)$/);
|
|
423
|
+
if (selectMatch) {
|
|
424
|
+
const pendingText = this.pendingMessages.get(msg.chat.id);
|
|
425
|
+
if (pendingText) {
|
|
426
|
+
this.pendingMessages.delete(msg.chat.id);
|
|
427
|
+
const sessions2 = this.getSessions?.() ?? [];
|
|
428
|
+
const idx = parseInt(selectMatch[1], 10) - 1;
|
|
429
|
+
if (idx >= 0 && idx < sessions2.length) {
|
|
430
|
+
this.deliverToSession(sessions2[idx].id, pendingText);
|
|
431
|
+
this.sendMessage(`Sent to [${this.getLabel(sessions2[idx])}]`);
|
|
432
|
+
} else {
|
|
433
|
+
this.sendMessage("Invalid session number.");
|
|
434
|
+
}
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
const sessions = this.getSessions?.() ?? [];
|
|
439
|
+
if (sessions.length === 0) {
|
|
440
|
+
this.sendMessage("No active sessions connected.");
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
if (sessions.length === 1) {
|
|
444
|
+
this.deliverToSession(sessions[0].id, text);
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
this.pendingMessages.set(msg.chat.id, text);
|
|
448
|
+
const sessionList = sessions.map((s, i) => `/s_${i + 1} - ${this.getLabel(s)}`).join("\n");
|
|
449
|
+
this.sendMessage(`Multiple sessions active. Reply with a command to select:
|
|
450
|
+
|
|
451
|
+
${sessionList}`);
|
|
452
|
+
}
|
|
453
|
+
deliverToSession(sessionId2, content) {
|
|
454
|
+
if (this.onMessageToSession) {
|
|
455
|
+
this.onMessageToSession(sessionId2, content);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
getLabel(session) {
|
|
459
|
+
return session.cwd?.replace(/^.*[/\\]/, "") || session.name;
|
|
460
|
+
}
|
|
461
|
+
/** Update config (e.g., from dashboard settings) */
|
|
462
|
+
updateConfig(config2) {
|
|
463
|
+
const wasPolling = this.polling;
|
|
464
|
+
if (wasPolling) this.stopPolling();
|
|
465
|
+
this.config = config2;
|
|
466
|
+
if (wasPolling && config2.enabled) this.startPolling();
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
|
|
287
472
|
// src/hub/server.ts
|
|
288
473
|
var server_exports = {};
|
|
289
474
|
__export(server_exports, {
|
|
@@ -303,6 +488,7 @@ var init_server = __esm({
|
|
|
303
488
|
init_constants();
|
|
304
489
|
init_session_manager();
|
|
305
490
|
init_notifier();
|
|
491
|
+
init_telegram();
|
|
306
492
|
init_config();
|
|
307
493
|
__dirname = path3.dirname(fileURLToPath(import.meta.url));
|
|
308
494
|
HubServer = class {
|
|
@@ -318,6 +504,7 @@ var init_server = __esm({
|
|
|
318
504
|
localChannels = /* @__PURE__ */ new Set();
|
|
319
505
|
// All connected dashboard WebSockets
|
|
320
506
|
dashboardSockets = /* @__PURE__ */ new Set();
|
|
507
|
+
telegramBot;
|
|
321
508
|
host;
|
|
322
509
|
port;
|
|
323
510
|
token;
|
|
@@ -335,6 +522,10 @@ var init_server = __esm({
|
|
|
335
522
|
}
|
|
336
523
|
const displayHost = this.host === "0.0.0.0" ? "127.0.0.1" : this.host;
|
|
337
524
|
this.notifier.configure({ dashboardUrl: `http://${displayHost}:${this.port}` });
|
|
525
|
+
const fullConfig = loadConfig();
|
|
526
|
+
if (fullConfig.telegram?.enabled && fullConfig.telegram.botToken && fullConfig.telegram.chatId) {
|
|
527
|
+
this.initTelegram(fullConfig.telegram);
|
|
528
|
+
}
|
|
338
529
|
this.httpServer = http.createServer((req, res) => this.handleHttp(req, res));
|
|
339
530
|
this.wssChannel = new WebSocketServer({ noServer: true });
|
|
340
531
|
this.wssChannel.on("connection", (ws, req) => this.handleChannelConnection(ws, req));
|
|
@@ -376,6 +567,7 @@ var init_server = __esm({
|
|
|
376
567
|
}
|
|
377
568
|
stop() {
|
|
378
569
|
return new Promise((resolve) => {
|
|
570
|
+
if (this.telegramBot) this.telegramBot.stopPolling();
|
|
379
571
|
for (const ws of this.channelSockets.values()) ws.terminate();
|
|
380
572
|
for (const ws of this.dashboardSockets) ws.terminate();
|
|
381
573
|
this.channelSockets.clear();
|
|
@@ -437,6 +629,16 @@ var init_server = __esm({
|
|
|
437
629
|
this.jsonResponse(res, 200, { webhooks: config2.webhooks || [] });
|
|
438
630
|
} else if (url.pathname === "/api/webhooks" && req.method === "POST") {
|
|
439
631
|
this.handleWebhookSave(req, res);
|
|
632
|
+
} else if (url.pathname === "/api/telegram" && req.method === "GET") {
|
|
633
|
+
const cfg = loadConfig();
|
|
634
|
+
const tg = cfg.telegram ?? { botToken: "", chatId: "", enabled: false };
|
|
635
|
+
this.jsonResponse(res, 200, {
|
|
636
|
+
telegram: { ...tg, botToken: tg.botToken ? `${tg.botToken.slice(0, 8)}...` : "" }
|
|
637
|
+
});
|
|
638
|
+
} else if (url.pathname === "/api/telegram" && req.method === "POST") {
|
|
639
|
+
this.handleTelegramSave(req, res);
|
|
640
|
+
} else if (url.pathname === "/api/telegram/test" && req.method === "POST") {
|
|
641
|
+
this.handleTelegramTest(req, res);
|
|
440
642
|
} else {
|
|
441
643
|
this.jsonResponse(res, 404, { error: "Not found" });
|
|
442
644
|
}
|
|
@@ -557,7 +759,7 @@ var init_server = __esm({
|
|
|
557
759
|
this.sessions.updateActivity(msg.sessionId);
|
|
558
760
|
const notifySession = this.sessions.get(msg.sessionId);
|
|
559
761
|
const notifyLabel = this.getSessionLabel(notifySession);
|
|
560
|
-
this.notifier.
|
|
762
|
+
this.notifier.notifyWithSession(msg.sessionId, notifyLabel, `[${notifyLabel}] ${msg.title}`, msg.message, msg.level ?? "info");
|
|
561
763
|
this.broadcastToDashboards({
|
|
562
764
|
type: "notification",
|
|
563
765
|
sessionId: msg.sessionId,
|
|
@@ -572,7 +774,7 @@ var init_server = __esm({
|
|
|
572
774
|
this.sessions.updateActivity(msg.sessionId);
|
|
573
775
|
const replySession = this.sessions.get(msg.sessionId);
|
|
574
776
|
const replyLabel = this.getSessionLabel(replySession);
|
|
575
|
-
this.notifier.
|
|
777
|
+
this.notifier.notifyWithSession(msg.sessionId, replyLabel, `[${replyLabel}] Reply`, msg.content.slice(0, 200), "info");
|
|
576
778
|
this.broadcastToDashboards({
|
|
577
779
|
type: "reply_from_session",
|
|
578
780
|
sessionId: msg.sessionId,
|
|
@@ -675,6 +877,75 @@ var init_server = __esm({
|
|
|
675
877
|
this.notifier.configure({ webhooks });
|
|
676
878
|
this.jsonResponse(res, 200, { ok: true });
|
|
677
879
|
}
|
|
880
|
+
initTelegram(config2) {
|
|
881
|
+
this.telegramBot = new TelegramBot(config2);
|
|
882
|
+
this.telegramBot.getSessions = () => this.sessions.getAll();
|
|
883
|
+
this.telegramBot.onMessageToSession = (sessionId2, content) => {
|
|
884
|
+
const channelWs = this.channelSockets.get(sessionId2);
|
|
885
|
+
if (channelWs?.readyState === WebSocket.OPEN) {
|
|
886
|
+
const msg = { type: "message_to_session", sessionId: sessionId2, content };
|
|
887
|
+
channelWs.send(JSON.stringify(msg));
|
|
888
|
+
logger.info(`Telegram message forwarded to session: ${sessionId2}`);
|
|
889
|
+
}
|
|
890
|
+
};
|
|
891
|
+
this.notifier.configure({ telegramBot: this.telegramBot });
|
|
892
|
+
this.telegramBot.startPolling();
|
|
893
|
+
logger.info("Telegram bot initialized");
|
|
894
|
+
}
|
|
895
|
+
async handleTelegramSave(req, res) {
|
|
896
|
+
const body = await this.readBody(req);
|
|
897
|
+
if (!body) {
|
|
898
|
+
this.jsonResponse(res, 400, { error: "Invalid JSON" });
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
const { telegram } = body;
|
|
902
|
+
if (!telegram) {
|
|
903
|
+
this.jsonResponse(res, 400, { error: "telegram config required" });
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
const config2 = loadConfig();
|
|
907
|
+
config2.telegram = telegram;
|
|
908
|
+
saveConfig(config2);
|
|
909
|
+
if (this.telegramBot) {
|
|
910
|
+
this.telegramBot.stopPolling();
|
|
911
|
+
this.telegramBot = void 0;
|
|
912
|
+
this.notifier.configure({ telegramBot: void 0 });
|
|
913
|
+
}
|
|
914
|
+
if (telegram.enabled && telegram.botToken && telegram.chatId) {
|
|
915
|
+
this.initTelegram(telegram);
|
|
916
|
+
}
|
|
917
|
+
this.jsonResponse(res, 200, { ok: true });
|
|
918
|
+
}
|
|
919
|
+
async handleTelegramTest(req, res) {
|
|
920
|
+
const body = await this.readBody(req);
|
|
921
|
+
if (!body) {
|
|
922
|
+
this.jsonResponse(res, 400, { error: "Invalid JSON" });
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
const { botToken, chatId } = body;
|
|
926
|
+
if (!botToken || !chatId) {
|
|
927
|
+
this.jsonResponse(res, 400, { error: "botToken and chatId required" });
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
try {
|
|
931
|
+
const testRes = await fetch(`https://api.telegram.org/bot${botToken}/sendMessage`, {
|
|
932
|
+
method: "POST",
|
|
933
|
+
headers: { "Content-Type": "application/json" },
|
|
934
|
+
body: JSON.stringify({
|
|
935
|
+
chat_id: chatId,
|
|
936
|
+
text: "Claude Alarm test message! Connection successful."
|
|
937
|
+
})
|
|
938
|
+
});
|
|
939
|
+
if (testRes.ok) {
|
|
940
|
+
this.jsonResponse(res, 200, { ok: true });
|
|
941
|
+
} else {
|
|
942
|
+
const err = await testRes.json();
|
|
943
|
+
this.jsonResponse(res, 400, { error: err.description || "Telegram API error" });
|
|
944
|
+
}
|
|
945
|
+
} catch (err) {
|
|
946
|
+
this.jsonResponse(res, 500, { error: err.message });
|
|
947
|
+
}
|
|
948
|
+
}
|
|
678
949
|
cleanupUploads() {
|
|
679
950
|
try {
|
|
680
951
|
if (!fs2.existsSync(UPLOADS_DIR)) return;
|