@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/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.notify(`[${notifyLabel}] ${msg.title}`, msg.message, msg.level ?? "info");
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.notify(`[${replyLabel}] Reply`, msg.content.slice(0, 200), "info");
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;