@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.
@@ -227,6 +227,8 @@
227
227
  .messages-panel {
228
228
  display: flex;
229
229
  flex-direction: column;
230
+ overflow: hidden;
231
+ min-height: 0;
230
232
  }
231
233
  .messages-header {
232
234
  padding: 12px 20px;
@@ -237,6 +239,7 @@
237
239
  .messages-list {
238
240
  flex: 1;
239
241
  overflow-y: auto;
242
+ min-height: 0;
240
243
  padding: 16px 20px;
241
244
  }
242
245
  .message {
@@ -611,7 +614,7 @@
611
614
  <header>
612
615
  <h1>Claude Alarm</h1>
613
616
  <div class="header-right">
614
- <button class="theme-toggle" id="webhookToggle" title="Webhook settings">&#9881;</button>
617
+ <button class="theme-toggle" id="settingsToggle" title="Notification settings">&#9881;</button>
615
618
  <button class="theme-toggle" id="themeToggle" title="Toggle theme">&#9790;</button>
616
619
  <div class="status-badge">
617
620
  <span class="status-dot" id="connDot"></span>
@@ -630,15 +633,42 @@
630
633
  </div>
631
634
  </div>
632
635
 
633
- <div class="token-overlay hidden" id="webhookOverlay">
634
- <div class="token-form" style="max-width:500px;text-align:left">
635
- <h2 style="text-align:center;margin-bottom:4px">Webhook Settings</h2>
636
- <p style="text-align:center;margin-bottom:16px">Send notifications to Slack, Discord, or any webhook endpoint.</p>
637
- <div id="webhookList" style="max-height:240px;overflow-y:auto"></div>
638
- <button id="webhookAdd" style="width:100%;margin-top:10px;background:none;color:var(--text-dim);border:1px dashed var(--border);border-radius:6px;padding:10px;cursor:pointer;font-size:13px;transition:border-color 0.15s">+ Add Webhook</button>
639
- <div style="display:flex;gap:8px;margin-top:20px">
640
- <button id="webhookSave" style="flex:1;padding:10px">Save</button>
641
- <button id="webhookCancel" style="flex:1;padding:10px;background:none;color:var(--text);border:1px solid var(--border);border-radius:6px;cursor:pointer;font-size:14px;font-weight:500">Cancel</button>
636
+ <div class="token-overlay hidden" id="settingsOverlay">
637
+ <div class="token-form" style="max-width:520px;text-align:left">
638
+ <h2 style="text-align:center;margin-bottom:12px">Notification Settings</h2>
639
+ <div style="display:flex;border-bottom:1px solid var(--border);margin-bottom:16px">
640
+ <button class="settings-tab active" data-settings-tab="webhook" style="flex:1;padding:10px;background:none;border:none;border-bottom:2px solid var(--accent);color:var(--text);cursor:pointer;font-size:13px;font-weight:600;transition:all 0.15s">Webhook</button>
641
+ <button class="settings-tab" data-settings-tab="telegram" style="flex:1;padding:10px;background:none;border:none;border-bottom:2px solid transparent;color:var(--text-dim);cursor:pointer;font-size:13px;font-weight:500;transition:all 0.15s">Telegram</button>
642
+ </div>
643
+ <div id="settingsWebhook">
644
+ <p style="margin-bottom:12px;font-size:12px;color:var(--text-dim)">Send notifications to Slack, Discord, or any webhook endpoint.</p>
645
+ <div id="webhookList" style="max-height:200px;overflow-y:auto"></div>
646
+ <button id="webhookAdd" style="width:100%;margin-top:10px;background:none;color:var(--text-dim);border:1px dashed var(--border);border-radius:6px;padding:10px;cursor:pointer;font-size:13px;transition:border-color 0.15s">+ Add Webhook</button>
647
+ <div style="display:flex;gap:8px;margin-top:16px">
648
+ <button id="webhookSave" style="flex:1;padding:10px">Save</button>
649
+ <button id="settingsCancel1" style="flex:1;padding:10px;background:none;color:var(--text);border:1px solid var(--border);border-radius:6px;cursor:pointer;font-size:14px;font-weight:500">Cancel</button>
650
+ </div>
651
+ </div>
652
+ <div id="settingsTelegram" style="display:none">
653
+ <p style="margin-bottom:12px;font-size:12px;color:var(--text-dim)">Connect a Telegram bot to receive notifications and send messages.</p>
654
+ <div style="margin-bottom:12px">
655
+ <label style="display:block;font-size:12px;color:var(--text-dim);margin-bottom:4px">Bot Token</label>
656
+ <input type="text" id="tgBotToken" placeholder="123456:ABC-DEF..." autocomplete="off" style="width:100%;box-sizing:border-box;padding:10px;background:var(--input-bg);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:13px;font-family:monospace">
657
+ </div>
658
+ <div style="margin-bottom:12px">
659
+ <label style="display:block;font-size:12px;color:var(--text-dim);margin-bottom:4px">Chat ID</label>
660
+ <input type="text" id="tgChatId" placeholder="-1001234567890" autocomplete="off" style="width:100%;box-sizing:border-box;padding:10px;background:var(--input-bg);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:13px;font-family:monospace">
661
+ </div>
662
+ <div style="margin-bottom:16px;display:flex;align-items:center;gap:8px">
663
+ <input type="checkbox" id="tgEnabled" style="width:16px;height:16px;accent-color:var(--accent)">
664
+ <label for="tgEnabled" style="font-size:13px;color:var(--text)">Enable Telegram notifications</label>
665
+ </div>
666
+ <div id="tgStatus" style="display:none;margin-bottom:12px;padding:8px 12px;border-radius:6px;font-size:12px"></div>
667
+ <div style="display:flex;gap:8px">
668
+ <button id="tgTest" style="flex:1;padding:10px;background:none;color:var(--accent);border:1px solid var(--accent);border-radius:6px;cursor:pointer;font-size:13px;font-weight:500">Test</button>
669
+ <button id="tgSave" style="flex:1;padding:10px">Save</button>
670
+ <button id="settingsCancel2" style="flex:1;padding:10px;background:none;color:var(--text);border:1px solid var(--border);border-radius:6px;cursor:pointer;font-size:14px;font-weight:500">Cancel</button>
671
+ </div>
642
672
  </div>
643
673
  </div>
644
674
  </div>
@@ -1144,9 +1174,32 @@
1144
1174
  });
1145
1175
  });
1146
1176
 
1147
- // --- Webhook settings ---
1177
+ // --- Settings modal (Webhook + Telegram) ---
1148
1178
  let webhookData = [];
1149
- $('#webhookToggle').addEventListener('click', async () => {
1179
+ const closeSettings = () => $('#settingsOverlay').classList.add('hidden');
1180
+
1181
+ // Tab switching
1182
+ document.querySelectorAll('.settings-tab').forEach(tab => {
1183
+ tab.addEventListener('click', () => {
1184
+ document.querySelectorAll('.settings-tab').forEach(t => {
1185
+ t.classList.remove('active');
1186
+ t.style.borderBottomColor = 'transparent';
1187
+ t.style.color = 'var(--text-dim)';
1188
+ t.style.fontWeight = '500';
1189
+ });
1190
+ tab.classList.add('active');
1191
+ tab.style.borderBottomColor = 'var(--accent)';
1192
+ tab.style.color = 'var(--text)';
1193
+ tab.style.fontWeight = '600';
1194
+ const target = tab.dataset.settingsTab;
1195
+ $('#settingsWebhook').style.display = target === 'webhook' ? 'block' : 'none';
1196
+ $('#settingsTelegram').style.display = target === 'telegram' ? 'block' : 'none';
1197
+ });
1198
+ });
1199
+
1200
+ // Open settings
1201
+ $('#settingsToggle').addEventListener('click', async () => {
1202
+ // Load webhook data
1150
1203
  try {
1151
1204
  const tokenQuery = state.token ? `?token=${encodeURIComponent(state.token)}` : '';
1152
1205
  const res = await fetch(`/api/webhooks${tokenQuery}`);
@@ -1154,9 +1207,27 @@
1154
1207
  webhookData = data.webhooks || [];
1155
1208
  } catch { webhookData = []; }
1156
1209
  renderWebhooks();
1157
- $('#webhookOverlay').classList.remove('hidden');
1210
+ // Load telegram data
1211
+ try {
1212
+ const tokenQuery = state.token ? `?token=${encodeURIComponent(state.token)}` : '';
1213
+ const res = await fetch(`/api/telegram${tokenQuery}`);
1214
+ const data = await res.json();
1215
+ const tg = data.telegram || {};
1216
+ if (!$('#tgBotToken').value || $('#tgBotToken').value.includes('...')) {
1217
+ $('#tgBotToken').value = tg.botToken || '';
1218
+ }
1219
+ $('#tgChatId').value = tg.chatId || '';
1220
+ $('#tgEnabled').checked = !!tg.enabled;
1221
+ } catch {}
1222
+ $('#tgStatus').style.display = 'none';
1223
+ $('#settingsOverlay').classList.remove('hidden');
1158
1224
  });
1159
- $('#webhookCancel').addEventListener('click', () => $('#webhookOverlay').classList.add('hidden'));
1225
+
1226
+ // Cancel buttons
1227
+ $('#settingsCancel1').addEventListener('click', closeSettings);
1228
+ $('#settingsCancel2').addEventListener('click', closeSettings);
1229
+
1230
+ // Webhook
1160
1231
  $('#webhookAdd').addEventListener('click', () => {
1161
1232
  webhookData.push({ url: '' });
1162
1233
  renderWebhooks();
@@ -1171,7 +1242,7 @@
1171
1242
  body: JSON.stringify({ webhooks: valid }),
1172
1243
  });
1173
1244
  } catch {}
1174
- $('#webhookOverlay').classList.add('hidden');
1245
+ closeSettings();
1175
1246
  });
1176
1247
  function renderWebhooks() {
1177
1248
  const el = $('#webhookList');
@@ -1198,6 +1269,50 @@
1198
1269
  });
1199
1270
  }
1200
1271
 
1272
+ // Telegram
1273
+ function showTgStatus(msg, ok) {
1274
+ const el = $('#tgStatus');
1275
+ el.textContent = msg;
1276
+ el.style.display = 'block';
1277
+ el.style.background = ok ? 'rgba(34,197,94,0.15)' : 'rgba(239,68,68,0.15)';
1278
+ el.style.color = ok ? 'var(--green)' : 'var(--red)';
1279
+ }
1280
+
1281
+ $('#tgTest').addEventListener('click', async () => {
1282
+ const botToken = $('#tgBotToken').value.trim();
1283
+ const chatId = $('#tgChatId').value.trim();
1284
+ if (!botToken || !chatId) { showTgStatus('Bot Token and Chat ID are required.', false); return; }
1285
+ if (botToken.includes('...')) { showTgStatus('Please enter full Bot Token (not masked).', false); return; }
1286
+ try {
1287
+ const tokenQuery = state.token ? `?token=${encodeURIComponent(state.token)}` : '';
1288
+ const res = await fetch(`/api/telegram/test${tokenQuery}`, {
1289
+ method: 'POST',
1290
+ headers: { 'Content-Type': 'application/json' },
1291
+ body: JSON.stringify({ botToken, chatId }),
1292
+ });
1293
+ const data = await res.json();
1294
+ if (data.ok) { showTgStatus('Test message sent! Check your Telegram.', true); }
1295
+ else { showTgStatus(data.error || 'Test failed.', false); }
1296
+ } catch (e) { showTgStatus('Connection error.', false); }
1297
+ });
1298
+
1299
+ $('#tgSave').addEventListener('click', async () => {
1300
+ const botToken = $('#tgBotToken').value.trim();
1301
+ const chatId = $('#tgChatId').value.trim();
1302
+ const enabled = $('#tgEnabled').checked;
1303
+ if (enabled && botToken.includes('...')) { showTgStatus('Please enter full Bot Token to enable.', false); return; }
1304
+ try {
1305
+ const tokenQuery = state.token ? `?token=${encodeURIComponent(state.token)}` : '';
1306
+ await fetch(`/api/telegram${tokenQuery}`, {
1307
+ method: 'POST',
1308
+ headers: { 'Content-Type': 'application/json' },
1309
+ body: JSON.stringify({ telegram: { botToken, chatId, enabled } }),
1310
+ });
1311
+ showTgStatus('Saved!', true);
1312
+ setTimeout(closeSettings, 800);
1313
+ } catch { showTgStatus('Save failed.', false); }
1314
+ });
1315
+
1201
1316
  // Refresh relative times every 30s
1202
1317
  setInterval(() => { renderMessages(); renderNotifications(); }, 30000);
1203
1318
 
@@ -85,12 +85,17 @@ var Notifier = class {
85
85
  desktopEnabled = true;
86
86
  notificationSettingsOpened = false;
87
87
  dashboardUrl;
88
+ telegramBot;
88
89
  configure(options) {
89
90
  if (options.dashboardUrl) this.dashboardUrl = options.dashboardUrl;
90
91
  if (options.desktop !== void 0) this.desktopEnabled = options.desktop;
91
92
  if (options.webhooks) this.webhooks = options.webhooks;
93
+ if (options.telegramBot) this.telegramBot = options.telegramBot;
92
94
  }
93
95
  async notify(title, message, level = "info") {
96
+ await this.notifyWithSession(void 0, void 0, title, message, level);
97
+ }
98
+ async notifyWithSession(sessionId, sessionLabel, title, message, level = "info") {
94
99
  const promises = [];
95
100
  if (this.desktopEnabled) {
96
101
  promises.push(this.sendDesktop(title, message, level));
@@ -98,6 +103,9 @@ var Notifier = class {
98
103
  for (const webhook of this.webhooks) {
99
104
  promises.push(this.sendWebhook(webhook, title, message, level));
100
105
  }
106
+ if (this.telegramBot && sessionId && sessionLabel) {
107
+ promises.push(this.telegramBot.sendNotification(sessionId, sessionLabel, title, message));
108
+ }
101
109
  await Promise.allSettled(promises);
102
110
  }
103
111
  async sendDesktop(title, message, _level) {
@@ -178,6 +186,176 @@ var Notifier = class {
178
186
  }
179
187
  };
180
188
 
189
+ // src/hub/telegram.ts
190
+ var TELEGRAM_API = "https://api.telegram.org/bot";
191
+ var TelegramBot = class {
192
+ config;
193
+ offset = 0;
194
+ polling = false;
195
+ pollTimer = null;
196
+ // message_id -> sessionId mapping for reply-based routing
197
+ messageSessionMap = /* @__PURE__ */ new Map();
198
+ // Callback: when a message arrives from Telegram for a session
199
+ onMessageToSession;
200
+ // Callback: get current sessions list
201
+ getSessions;
202
+ // Callback: when user needs to select a session (sends inline keyboard)
203
+ pendingMessages = /* @__PURE__ */ new Map();
204
+ // chatId -> pending message text
205
+ constructor(config) {
206
+ this.config = config;
207
+ }
208
+ get apiUrl() {
209
+ return `${TELEGRAM_API}${this.config.botToken}`;
210
+ }
211
+ /** Send a notification message to Telegram */
212
+ async sendNotification(sessionId, sessionLabel, title, message) {
213
+ const text = `[${sessionLabel}] ${title}
214
+ ${message}`;
215
+ const result = await this.sendMessage(text);
216
+ if (result?.message_id) {
217
+ this.messageSessionMap.set(result.message_id, sessionId);
218
+ if (this.messageSessionMap.size > 200) {
219
+ const keys = [...this.messageSessionMap.keys()];
220
+ for (let i = 0; i < keys.length - 200; i++) {
221
+ this.messageSessionMap.delete(keys[i]);
222
+ }
223
+ }
224
+ }
225
+ }
226
+ /** Send a text message to the configured chat */
227
+ async sendMessage(text, replyToMessageId, replyMarkup) {
228
+ try {
229
+ const body = {
230
+ chat_id: this.config.chatId,
231
+ text,
232
+ parse_mode: "HTML"
233
+ };
234
+ if (replyToMessageId) body.reply_to_message_id = replyToMessageId;
235
+ if (replyMarkup) body.reply_markup = replyMarkup;
236
+ const res = await fetch(`${this.apiUrl}/sendMessage`, {
237
+ method: "POST",
238
+ headers: { "Content-Type": "application/json" },
239
+ body: JSON.stringify(body)
240
+ });
241
+ if (!res.ok) {
242
+ const err = await res.text();
243
+ logger.warn(`Telegram sendMessage failed: ${res.status} ${err}`);
244
+ return null;
245
+ }
246
+ const data = await res.json();
247
+ return data.ok ? data.result : null;
248
+ } catch (err) {
249
+ logger.warn(`Telegram sendMessage error: ${err.message}`);
250
+ return null;
251
+ }
252
+ }
253
+ /** Start long polling for incoming messages */
254
+ startPolling() {
255
+ if (this.polling) return;
256
+ this.polling = true;
257
+ logger.info("Telegram bot polling started");
258
+ this.poll();
259
+ }
260
+ /** Stop polling */
261
+ stopPolling() {
262
+ this.polling = false;
263
+ if (this.pollTimer) {
264
+ clearTimeout(this.pollTimer);
265
+ this.pollTimer = null;
266
+ }
267
+ logger.info("Telegram bot polling stopped");
268
+ }
269
+ async poll() {
270
+ if (!this.polling) return;
271
+ try {
272
+ const res = await fetch(`${this.apiUrl}/getUpdates?offset=${this.offset}&timeout=30`, {
273
+ signal: AbortSignal.timeout(35e3)
274
+ });
275
+ if (!res.ok) {
276
+ logger.warn(`Telegram getUpdates failed: ${res.status}`);
277
+ this.scheduleNextPoll(5e3);
278
+ return;
279
+ }
280
+ const data = await res.json();
281
+ if (data.ok && data.result.length > 0) {
282
+ for (const update of data.result) {
283
+ this.offset = update.update_id + 1;
284
+ if (update.message) {
285
+ this.handleIncomingMessage(update.message);
286
+ }
287
+ }
288
+ }
289
+ } catch (err) {
290
+ if (err.name !== "AbortError") {
291
+ logger.warn(`Telegram poll error: ${err.message}`);
292
+ }
293
+ }
294
+ this.scheduleNextPoll(1e3);
295
+ }
296
+ scheduleNextPoll(delay) {
297
+ if (!this.polling) return;
298
+ this.pollTimer = setTimeout(() => this.poll(), delay);
299
+ }
300
+ handleIncomingMessage(msg) {
301
+ if (!msg.text) return;
302
+ if (String(msg.chat.id) !== String(this.config.chatId)) return;
303
+ const text = msg.text.trim();
304
+ if (msg.reply_to_message) {
305
+ const sessionId = this.messageSessionMap.get(msg.reply_to_message.message_id);
306
+ if (sessionId) {
307
+ this.deliverToSession(sessionId, text);
308
+ return;
309
+ }
310
+ }
311
+ const selectMatch = text.match(/^\/s_(\d+)$/);
312
+ if (selectMatch) {
313
+ const pendingText = this.pendingMessages.get(msg.chat.id);
314
+ if (pendingText) {
315
+ this.pendingMessages.delete(msg.chat.id);
316
+ const sessions2 = this.getSessions?.() ?? [];
317
+ const idx = parseInt(selectMatch[1], 10) - 1;
318
+ if (idx >= 0 && idx < sessions2.length) {
319
+ this.deliverToSession(sessions2[idx].id, pendingText);
320
+ this.sendMessage(`Sent to [${this.getLabel(sessions2[idx])}]`);
321
+ } else {
322
+ this.sendMessage("Invalid session number.");
323
+ }
324
+ return;
325
+ }
326
+ }
327
+ const sessions = this.getSessions?.() ?? [];
328
+ if (sessions.length === 0) {
329
+ this.sendMessage("No active sessions connected.");
330
+ return;
331
+ }
332
+ if (sessions.length === 1) {
333
+ this.deliverToSession(sessions[0].id, text);
334
+ return;
335
+ }
336
+ this.pendingMessages.set(msg.chat.id, text);
337
+ const sessionList = sessions.map((s, i) => `/s_${i + 1} - ${this.getLabel(s)}`).join("\n");
338
+ this.sendMessage(`Multiple sessions active. Reply with a command to select:
339
+
340
+ ${sessionList}`);
341
+ }
342
+ deliverToSession(sessionId, content) {
343
+ if (this.onMessageToSession) {
344
+ this.onMessageToSession(sessionId, content);
345
+ }
346
+ }
347
+ getLabel(session) {
348
+ return session.cwd?.replace(/^.*[/\\]/, "") || session.name;
349
+ }
350
+ /** Update config (e.g., from dashboard settings) */
351
+ updateConfig(config) {
352
+ const wasPolling = this.polling;
353
+ if (wasPolling) this.stopPolling();
354
+ this.config = config;
355
+ if (wasPolling && config.enabled) this.startPolling();
356
+ }
357
+ };
358
+
181
359
  // src/shared/config.ts
182
360
  import fs from "fs";
183
361
  import path2 from "path";
@@ -207,7 +385,7 @@ function loadConfig() {
207
385
  try {
208
386
  const raw = fs.readFileSync(CONFIG_FILE, "utf-8");
209
387
  const parsed = JSON.parse(raw);
210
- config = { ...DEFAULT_CONFIG, ...parsed, hub: { ...DEFAULT_CONFIG.hub, ...parsed.hub } };
388
+ config = { ...DEFAULT_CONFIG, ...parsed, hub: { ...DEFAULT_CONFIG.hub, ...parsed.hub }, ...parsed.telegram ? { telegram: parsed.telegram } : {} };
211
389
  } catch {
212
390
  config = { ...DEFAULT_CONFIG, hub: { ...DEFAULT_CONFIG.hub } };
213
391
  }
@@ -238,6 +416,7 @@ var HubServer = class {
238
416
  localChannels = /* @__PURE__ */ new Set();
239
417
  // All connected dashboard WebSockets
240
418
  dashboardSockets = /* @__PURE__ */ new Set();
419
+ telegramBot;
241
420
  host;
242
421
  port;
243
422
  token;
@@ -255,6 +434,10 @@ var HubServer = class {
255
434
  }
256
435
  const displayHost = this.host === "0.0.0.0" ? "127.0.0.1" : this.host;
257
436
  this.notifier.configure({ dashboardUrl: `http://${displayHost}:${this.port}` });
437
+ const fullConfig = loadConfig();
438
+ if (fullConfig.telegram?.enabled && fullConfig.telegram.botToken && fullConfig.telegram.chatId) {
439
+ this.initTelegram(fullConfig.telegram);
440
+ }
258
441
  this.httpServer = http.createServer((req, res) => this.handleHttp(req, res));
259
442
  this.wssChannel = new WebSocketServer({ noServer: true });
260
443
  this.wssChannel.on("connection", (ws, req) => this.handleChannelConnection(ws, req));
@@ -296,6 +479,7 @@ var HubServer = class {
296
479
  }
297
480
  stop() {
298
481
  return new Promise((resolve) => {
482
+ if (this.telegramBot) this.telegramBot.stopPolling();
299
483
  for (const ws of this.channelSockets.values()) ws.terminate();
300
484
  for (const ws of this.dashboardSockets) ws.terminate();
301
485
  this.channelSockets.clear();
@@ -357,6 +541,16 @@ var HubServer = class {
357
541
  this.jsonResponse(res, 200, { webhooks: config.webhooks || [] });
358
542
  } else if (url.pathname === "/api/webhooks" && req.method === "POST") {
359
543
  this.handleWebhookSave(req, res);
544
+ } else if (url.pathname === "/api/telegram" && req.method === "GET") {
545
+ const cfg = loadConfig();
546
+ const tg = cfg.telegram ?? { botToken: "", chatId: "", enabled: false };
547
+ this.jsonResponse(res, 200, {
548
+ telegram: { ...tg, botToken: tg.botToken ? `${tg.botToken.slice(0, 8)}...` : "" }
549
+ });
550
+ } else if (url.pathname === "/api/telegram" && req.method === "POST") {
551
+ this.handleTelegramSave(req, res);
552
+ } else if (url.pathname === "/api/telegram/test" && req.method === "POST") {
553
+ this.handleTelegramTest(req, res);
360
554
  } else {
361
555
  this.jsonResponse(res, 404, { error: "Not found" });
362
556
  }
@@ -477,7 +671,7 @@ var HubServer = class {
477
671
  this.sessions.updateActivity(msg.sessionId);
478
672
  const notifySession = this.sessions.get(msg.sessionId);
479
673
  const notifyLabel = this.getSessionLabel(notifySession);
480
- this.notifier.notify(`[${notifyLabel}] ${msg.title}`, msg.message, msg.level ?? "info");
674
+ this.notifier.notifyWithSession(msg.sessionId, notifyLabel, `[${notifyLabel}] ${msg.title}`, msg.message, msg.level ?? "info");
481
675
  this.broadcastToDashboards({
482
676
  type: "notification",
483
677
  sessionId: msg.sessionId,
@@ -492,7 +686,7 @@ var HubServer = class {
492
686
  this.sessions.updateActivity(msg.sessionId);
493
687
  const replySession = this.sessions.get(msg.sessionId);
494
688
  const replyLabel = this.getSessionLabel(replySession);
495
- this.notifier.notify(`[${replyLabel}] Reply`, msg.content.slice(0, 200), "info");
689
+ this.notifier.notifyWithSession(msg.sessionId, replyLabel, `[${replyLabel}] Reply`, msg.content.slice(0, 200), "info");
496
690
  this.broadcastToDashboards({
497
691
  type: "reply_from_session",
498
692
  sessionId: msg.sessionId,
@@ -595,6 +789,75 @@ var HubServer = class {
595
789
  this.notifier.configure({ webhooks });
596
790
  this.jsonResponse(res, 200, { ok: true });
597
791
  }
792
+ initTelegram(config) {
793
+ this.telegramBot = new TelegramBot(config);
794
+ this.telegramBot.getSessions = () => this.sessions.getAll();
795
+ this.telegramBot.onMessageToSession = (sessionId, content) => {
796
+ const channelWs = this.channelSockets.get(sessionId);
797
+ if (channelWs?.readyState === WebSocket.OPEN) {
798
+ const msg = { type: "message_to_session", sessionId, content };
799
+ channelWs.send(JSON.stringify(msg));
800
+ logger.info(`Telegram message forwarded to session: ${sessionId}`);
801
+ }
802
+ };
803
+ this.notifier.configure({ telegramBot: this.telegramBot });
804
+ this.telegramBot.startPolling();
805
+ logger.info("Telegram bot initialized");
806
+ }
807
+ async handleTelegramSave(req, res) {
808
+ const body = await this.readBody(req);
809
+ if (!body) {
810
+ this.jsonResponse(res, 400, { error: "Invalid JSON" });
811
+ return;
812
+ }
813
+ const { telegram } = body;
814
+ if (!telegram) {
815
+ this.jsonResponse(res, 400, { error: "telegram config required" });
816
+ return;
817
+ }
818
+ const config = loadConfig();
819
+ config.telegram = telegram;
820
+ saveConfig(config);
821
+ if (this.telegramBot) {
822
+ this.telegramBot.stopPolling();
823
+ this.telegramBot = void 0;
824
+ this.notifier.configure({ telegramBot: void 0 });
825
+ }
826
+ if (telegram.enabled && telegram.botToken && telegram.chatId) {
827
+ this.initTelegram(telegram);
828
+ }
829
+ this.jsonResponse(res, 200, { ok: true });
830
+ }
831
+ async handleTelegramTest(req, res) {
832
+ const body = await this.readBody(req);
833
+ if (!body) {
834
+ this.jsonResponse(res, 400, { error: "Invalid JSON" });
835
+ return;
836
+ }
837
+ const { botToken, chatId } = body;
838
+ if (!botToken || !chatId) {
839
+ this.jsonResponse(res, 400, { error: "botToken and chatId required" });
840
+ return;
841
+ }
842
+ try {
843
+ const testRes = await fetch(`https://api.telegram.org/bot${botToken}/sendMessage`, {
844
+ method: "POST",
845
+ headers: { "Content-Type": "application/json" },
846
+ body: JSON.stringify({
847
+ chat_id: chatId,
848
+ text: "Claude Alarm test message! Connection successful."
849
+ })
850
+ });
851
+ if (testRes.ok) {
852
+ this.jsonResponse(res, 200, { ok: true });
853
+ } else {
854
+ const err = await testRes.json();
855
+ this.jsonResponse(res, 400, { error: err.description || "Telegram API error" });
856
+ }
857
+ } catch (err) {
858
+ this.jsonResponse(res, 500, { error: err.message });
859
+ }
860
+ }
598
861
  cleanupUploads() {
599
862
  try {
600
863
  if (!fs2.existsSync(UPLOADS_DIR)) return;