@delt/claude-alarm 0.4.9 → 0.5.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/dist/index.d.ts CHANGED
@@ -80,6 +80,12 @@ interface WebhookConfig {
80
80
  events?: string[];
81
81
  headers?: Record<string, string>;
82
82
  }
83
+ /** Telegram bot configuration */
84
+ interface TelegramConfig {
85
+ botToken: string;
86
+ chatId: string;
87
+ enabled: boolean;
88
+ }
83
89
  /** App configuration stored in ~/.claude-alarm/config.json */
84
90
  interface AppConfig {
85
91
  hub: {
@@ -92,6 +98,7 @@ interface AppConfig {
92
98
  sound: boolean;
93
99
  };
94
100
  webhooks: WebhookConfig[];
101
+ telegram?: TelegramConfig;
95
102
  }
96
103
  /** Hub status response */
97
104
  interface HubStatus {
@@ -112,6 +119,7 @@ declare class HubServer {
112
119
  private channelSockets;
113
120
  private localChannels;
114
121
  private dashboardSockets;
122
+ private telegramBot?;
115
123
  private host;
116
124
  private port;
117
125
  private token?;
@@ -128,6 +136,9 @@ declare class HubServer {
128
136
  private broadcastToDashboards;
129
137
  private handleImageUpload;
130
138
  private handleWebhookSave;
139
+ private initTelegram;
140
+ private handleTelegramSave;
141
+ private handleTelegramTest;
131
142
  private cleanupUploads;
132
143
  private getSessionLabel;
133
144
  private jsonResponse;
@@ -165,17 +176,48 @@ declare class SessionManager {
165
176
  count(): number;
166
177
  }
167
178
 
179
+ declare class TelegramBot {
180
+ private config;
181
+ private offset;
182
+ private polling;
183
+ private pollTimer;
184
+ private messageSessionMap;
185
+ onMessageToSession?: (sessionId: string, content: string) => void;
186
+ getSessions?: () => SessionInfo[];
187
+ private pendingMessages;
188
+ constructor(config: TelegramConfig);
189
+ private get apiUrl();
190
+ /** Send a notification message to Telegram */
191
+ sendNotification(sessionId: string, _sessionLabel: string, title: string, message: string): Promise<void>;
192
+ /** Send a text message to the configured chat */
193
+ private sendMessage;
194
+ /** Start long polling for incoming messages */
195
+ startPolling(): void;
196
+ /** Stop polling */
197
+ stopPolling(): void;
198
+ private poll;
199
+ private scheduleNextPoll;
200
+ private handleIncomingMessage;
201
+ private deliverToSession;
202
+ private getLabel;
203
+ /** Update config (e.g., from dashboard settings) */
204
+ updateConfig(config: TelegramConfig): void;
205
+ }
206
+
168
207
  declare class Notifier {
169
208
  private webhooks;
170
209
  private desktopEnabled;
171
210
  private notificationSettingsOpened;
172
211
  private dashboardUrl?;
212
+ private telegramBot?;
173
213
  configure(options: {
174
214
  desktop?: boolean;
175
215
  webhooks?: WebhookConfig[];
176
216
  dashboardUrl?: string;
217
+ telegramBot?: TelegramBot;
177
218
  }): void;
178
219
  notify(title: string, message: string, level?: NotifyLevel): Promise<void>;
220
+ notifyWithSession(sessionId: string | undefined, sessionLabel: string | undefined, title: string, message: string, level?: NotifyLevel): Promise<void>;
179
221
  private sendDesktop;
180
222
  private checkWindowsNotifications;
181
223
  private openNotificationSettings;
@@ -213,4 +255,4 @@ declare const WS_PATH_DASHBOARD = "/ws/dashboard";
213
255
  declare const CHANNEL_SERVER_NAME = "claude-alarm";
214
256
  declare const CHANNEL_SERVER_VERSION = "0.1.0";
215
257
 
216
- export { type AppConfig, CHANNEL_SERVER_NAME, CHANNEL_SERVER_VERSION, CONFIG_DIR, CONFIG_FILE, type ChannelMessage, DEFAULT_HUB_HOST, DEFAULT_HUB_PORT, HubClient, HubServer, type HubStatus, LOG_FILE, Notifier, type NotifyLevel, PID_FILE, type SessionInfo, SessionManager, type SessionStatus, UPLOADS_DIR, WS_PATH_CHANNEL, WS_PATH_DASHBOARD, type WebhookConfig, loadConfig, logger, saveConfig, setupMcpConfig };
258
+ export { type AppConfig, CHANNEL_SERVER_NAME, CHANNEL_SERVER_VERSION, CONFIG_DIR, CONFIG_FILE, type ChannelMessage, DEFAULT_HUB_HOST, DEFAULT_HUB_PORT, HubClient, HubServer, type HubStatus, LOG_FILE, Notifier, type NotifyLevel, PID_FILE, type SessionInfo, SessionManager, type SessionStatus, type TelegramConfig, UPLOADS_DIR, WS_PATH_CHANNEL, WS_PATH_DASHBOARD, type WebhookConfig, loadConfig, logger, saveConfig, setupMcpConfig };
package/dist/index.js CHANGED
@@ -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 = `${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
  }
@@ -262,6 +440,7 @@ var HubServer = class {
262
440
  localChannels = /* @__PURE__ */ new Set();
263
441
  // All connected dashboard WebSockets
264
442
  dashboardSockets = /* @__PURE__ */ new Set();
443
+ telegramBot;
265
444
  host;
266
445
  port;
267
446
  token;
@@ -279,6 +458,10 @@ var HubServer = class {
279
458
  }
280
459
  const displayHost = this.host === "0.0.0.0" ? "127.0.0.1" : this.host;
281
460
  this.notifier.configure({ dashboardUrl: `http://${displayHost}:${this.port}` });
461
+ const fullConfig = loadConfig();
462
+ if (fullConfig.telegram?.enabled && fullConfig.telegram.botToken && fullConfig.telegram.chatId) {
463
+ this.initTelegram(fullConfig.telegram);
464
+ }
282
465
  this.httpServer = http.createServer((req, res) => this.handleHttp(req, res));
283
466
  this.wssChannel = new WebSocketServer({ noServer: true });
284
467
  this.wssChannel.on("connection", (ws, req) => this.handleChannelConnection(ws, req));
@@ -320,6 +503,7 @@ var HubServer = class {
320
503
  }
321
504
  stop() {
322
505
  return new Promise((resolve) => {
506
+ if (this.telegramBot) this.telegramBot.stopPolling();
323
507
  for (const ws of this.channelSockets.values()) ws.terminate();
324
508
  for (const ws of this.dashboardSockets) ws.terminate();
325
509
  this.channelSockets.clear();
@@ -381,6 +565,16 @@ var HubServer = class {
381
565
  this.jsonResponse(res, 200, { webhooks: config.webhooks || [] });
382
566
  } else if (url.pathname === "/api/webhooks" && req.method === "POST") {
383
567
  this.handleWebhookSave(req, res);
568
+ } else if (url.pathname === "/api/telegram" && req.method === "GET") {
569
+ const cfg = loadConfig();
570
+ const tg = cfg.telegram ?? { botToken: "", chatId: "", enabled: false };
571
+ this.jsonResponse(res, 200, {
572
+ telegram: { ...tg, botToken: tg.botToken ? `${tg.botToken.slice(0, 8)}...` : "" }
573
+ });
574
+ } else if (url.pathname === "/api/telegram" && req.method === "POST") {
575
+ this.handleTelegramSave(req, res);
576
+ } else if (url.pathname === "/api/telegram/test" && req.method === "POST") {
577
+ this.handleTelegramTest(req, res);
384
578
  } else {
385
579
  this.jsonResponse(res, 404, { error: "Not found" });
386
580
  }
@@ -501,7 +695,7 @@ var HubServer = class {
501
695
  this.sessions.updateActivity(msg.sessionId);
502
696
  const notifySession = this.sessions.get(msg.sessionId);
503
697
  const notifyLabel = this.getSessionLabel(notifySession);
504
- this.notifier.notify(`[${notifyLabel}] ${msg.title}`, msg.message, msg.level ?? "info");
698
+ this.notifier.notifyWithSession(msg.sessionId, notifyLabel, `[${notifyLabel}] ${msg.title}`, msg.message, msg.level ?? "info");
505
699
  this.broadcastToDashboards({
506
700
  type: "notification",
507
701
  sessionId: msg.sessionId,
@@ -516,7 +710,7 @@ var HubServer = class {
516
710
  this.sessions.updateActivity(msg.sessionId);
517
711
  const replySession = this.sessions.get(msg.sessionId);
518
712
  const replyLabel = this.getSessionLabel(replySession);
519
- this.notifier.notify(`[${replyLabel}] Reply`, msg.content.slice(0, 200), "info");
713
+ this.notifier.notifyWithSession(msg.sessionId, replyLabel, `[${replyLabel}] Reply`, msg.content.slice(0, 200), "info");
520
714
  this.broadcastToDashboards({
521
715
  type: "reply_from_session",
522
716
  sessionId: msg.sessionId,
@@ -619,6 +813,75 @@ var HubServer = class {
619
813
  this.notifier.configure({ webhooks });
620
814
  this.jsonResponse(res, 200, { ok: true });
621
815
  }
816
+ initTelegram(config) {
817
+ this.telegramBot = new TelegramBot(config);
818
+ this.telegramBot.getSessions = () => this.sessions.getAll();
819
+ this.telegramBot.onMessageToSession = (sessionId, content) => {
820
+ const channelWs = this.channelSockets.get(sessionId);
821
+ if (channelWs?.readyState === WebSocket.OPEN) {
822
+ const msg = { type: "message_to_session", sessionId, content };
823
+ channelWs.send(JSON.stringify(msg));
824
+ logger.info(`Telegram message forwarded to session: ${sessionId}`);
825
+ }
826
+ };
827
+ this.notifier.configure({ telegramBot: this.telegramBot });
828
+ this.telegramBot.startPolling();
829
+ logger.info("Telegram bot initialized");
830
+ }
831
+ async handleTelegramSave(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 { telegram } = body;
838
+ if (!telegram) {
839
+ this.jsonResponse(res, 400, { error: "telegram config required" });
840
+ return;
841
+ }
842
+ const config = loadConfig();
843
+ config.telegram = telegram;
844
+ saveConfig(config);
845
+ if (this.telegramBot) {
846
+ this.telegramBot.stopPolling();
847
+ this.telegramBot = void 0;
848
+ this.notifier.configure({ telegramBot: void 0 });
849
+ }
850
+ if (telegram.enabled && telegram.botToken && telegram.chatId) {
851
+ this.initTelegram(telegram);
852
+ }
853
+ this.jsonResponse(res, 200, { ok: true });
854
+ }
855
+ async handleTelegramTest(req, res) {
856
+ const body = await this.readBody(req);
857
+ if (!body) {
858
+ this.jsonResponse(res, 400, { error: "Invalid JSON" });
859
+ return;
860
+ }
861
+ const { botToken, chatId } = body;
862
+ if (!botToken || !chatId) {
863
+ this.jsonResponse(res, 400, { error: "botToken and chatId required" });
864
+ return;
865
+ }
866
+ try {
867
+ const testRes = await fetch(`https://api.telegram.org/bot${botToken}/sendMessage`, {
868
+ method: "POST",
869
+ headers: { "Content-Type": "application/json" },
870
+ body: JSON.stringify({
871
+ chat_id: chatId,
872
+ text: "Claude Alarm test message! Connection successful."
873
+ })
874
+ });
875
+ if (testRes.ok) {
876
+ this.jsonResponse(res, 200, { ok: true });
877
+ } else {
878
+ const err = await testRes.json();
879
+ this.jsonResponse(res, 400, { error: err.description || "Telegram API error" });
880
+ }
881
+ } catch (err) {
882
+ this.jsonResponse(res, 500, { error: err.message });
883
+ }
884
+ }
622
885
  cleanupUploads() {
623
886
  try {
624
887
  if (!fs2.existsSync(UPLOADS_DIR)) return;