@delt/claude-alarm 0.5.2 → 0.5.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/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/hub/server.ts
2
2
  import http from "http";
3
- import fs2 from "fs";
4
- import path3 from "path";
3
+ import fs3 from "fs";
4
+ import path4 from "path";
5
5
  import { fileURLToPath } from "url";
6
6
  import { WebSocketServer, WebSocket } from "ws";
7
7
 
@@ -24,7 +24,7 @@ var logger = {
24
24
  };
25
25
 
26
26
  // src/hub/server.ts
27
- import { randomUUID as randomUUID2 } from "crypto";
27
+ import { randomUUID as randomUUID3 } from "crypto";
28
28
 
29
29
  // src/shared/constants.ts
30
30
  import path from "path";
@@ -187,6 +187,9 @@ var Notifier = class {
187
187
  };
188
188
 
189
189
  // src/hub/telegram.ts
190
+ import fs from "fs";
191
+ import path2 from "path";
192
+ import { randomUUID } from "crypto";
190
193
  var TELEGRAM_API = "https://api.telegram.org/bot";
191
194
  var TelegramBot = class {
192
195
  config;
@@ -195,13 +198,15 @@ var TelegramBot = class {
195
198
  pollTimer = null;
196
199
  // message_id -> sessionId mapping for reply-based routing
197
200
  messageSessionMap = /* @__PURE__ */ new Map();
198
- // Callback: when a message arrives from Telegram for a session
201
+ // Callback: when a text message arrives from Telegram for a session
199
202
  onMessageToSession;
203
+ // Callback: when an image arrives from Telegram for a session
204
+ onImageToSession;
200
205
  // Callback: get current sessions list
201
206
  getSessions;
202
- // Callback: when user needs to select a session (sends inline keyboard)
207
+ // Pending messages for session selection
203
208
  pendingMessages = /* @__PURE__ */ new Map();
204
- // chatId -> pending message text
209
+ // chatId -> pending
205
210
  constructor(config) {
206
211
  this.config = config;
207
212
  }
@@ -297,31 +302,42 @@ ${message}`;
297
302
  if (!this.polling) return;
298
303
  this.pollTimer = setTimeout(() => this.poll(), delay);
299
304
  }
300
- handleIncomingMessage(msg) {
301
- if (!msg.text) return;
305
+ async handleIncomingMessage(msg) {
302
306
  if (String(msg.chat.id) !== String(this.config.chatId)) return;
303
- const text = msg.text.trim();
307
+ const hasPhoto = msg.photo && msg.photo.length > 0;
308
+ const text = (msg.text || msg.caption || "").trim();
309
+ if (!text && !hasPhoto) return;
304
310
  if (msg.reply_to_message) {
305
311
  const sessionId = this.messageSessionMap.get(msg.reply_to_message.message_id);
306
312
  if (sessionId) {
307
- this.deliverToSession(sessionId, text);
313
+ if (hasPhoto) {
314
+ await this.deliverPhotoToSession(sessionId, msg.photo, text);
315
+ } else {
316
+ this.deliverToSession(sessionId, text);
317
+ }
308
318
  return;
309
319
  }
310
320
  }
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.");
321
+ if (text) {
322
+ const selectMatch = text.match(/^\/s_(\d+)$/);
323
+ if (selectMatch) {
324
+ const pending = this.pendingMessages.get(msg.chat.id);
325
+ if (pending) {
326
+ this.pendingMessages.delete(msg.chat.id);
327
+ const sessions2 = this.getSessions?.() ?? [];
328
+ const idx = parseInt(selectMatch[1], 10) - 1;
329
+ if (idx >= 0 && idx < sessions2.length) {
330
+ if (pending.photoFileId) {
331
+ await this.deliverPhotoToSessionByFileId(sessions2[idx].id, pending.photoFileId, pending.caption);
332
+ } else if (pending.text) {
333
+ this.deliverToSession(sessions2[idx].id, pending.text);
334
+ }
335
+ this.sendMessage(`Sent to [${this.getLabel(sessions2[idx])}]`);
336
+ } else {
337
+ this.sendMessage("Invalid session number.");
338
+ }
339
+ return;
323
340
  }
324
- return;
325
341
  }
326
342
  }
327
343
  const sessions = this.getSessions?.() ?? [];
@@ -330,10 +346,19 @@ ${message}`;
330
346
  return;
331
347
  }
332
348
  if (sessions.length === 1) {
333
- this.deliverToSession(sessions[0].id, text);
349
+ if (hasPhoto) {
350
+ await this.deliverPhotoToSession(sessions[0].id, msg.photo, text);
351
+ } else {
352
+ this.deliverToSession(sessions[0].id, text);
353
+ }
334
354
  return;
335
355
  }
336
- this.pendingMessages.set(msg.chat.id, text);
356
+ if (hasPhoto) {
357
+ const largest = msg.photo[msg.photo.length - 1];
358
+ this.pendingMessages.set(msg.chat.id, { photoFileId: largest.file_id, caption: text });
359
+ } else {
360
+ this.pendingMessages.set(msg.chat.id, { text });
361
+ }
337
362
  const sessionList = sessions.map((s, i) => `/s_${i + 1} - ${this.getLabel(s)}`).join("\n");
338
363
  this.sendMessage(`Multiple sessions active. Reply with a command to select:
339
364
 
@@ -344,6 +369,46 @@ ${sessionList}`);
344
369
  this.onMessageToSession(sessionId, content);
345
370
  }
346
371
  }
372
+ async deliverPhotoToSession(sessionId, photos, caption) {
373
+ const largest = photos[photos.length - 1];
374
+ await this.deliverPhotoToSessionByFileId(sessionId, largest.file_id, caption);
375
+ }
376
+ async deliverPhotoToSessionByFileId(sessionId, fileId, caption) {
377
+ try {
378
+ const fileRes = await fetch(`${this.apiUrl}/getFile?file_id=${fileId}`);
379
+ if (!fileRes.ok) {
380
+ logger.warn("Failed to get Telegram file info");
381
+ return;
382
+ }
383
+ const fileData = await fileRes.json();
384
+ if (!fileData.ok) return;
385
+ const downloadUrl = `https://api.telegram.org/file/bot${this.config.botToken}/${fileData.result.file_path}`;
386
+ const imgRes = await fetch(downloadUrl);
387
+ if (!imgRes.ok) {
388
+ logger.warn("Failed to download Telegram photo");
389
+ return;
390
+ }
391
+ const buffer = Buffer.from(await imgRes.arrayBuffer());
392
+ const ext = fileData.result.file_path.split(".").pop() || "jpg";
393
+ const mimeType = ext === "png" ? "image/png" : ext === "gif" ? "image/gif" : ext === "webp" ? "image/webp" : "image/jpeg";
394
+ fs.mkdirSync(UPLOADS_DIR, { recursive: true });
395
+ const filename = `${randomUUID()}.${ext}`;
396
+ const filePath = path2.join(UPLOADS_DIR, filename);
397
+ fs.writeFileSync(filePath, buffer);
398
+ logger.info(`Telegram photo saved: ${filename} (${buffer.length} bytes)`);
399
+ if (this.onImageToSession) {
400
+ this.onImageToSession(sessionId, filePath, mimeType, caption);
401
+ }
402
+ setTimeout(() => {
403
+ try {
404
+ fs.unlinkSync(filePath);
405
+ } catch {
406
+ }
407
+ }, 5 * 60 * 1e3);
408
+ } catch (err) {
409
+ logger.warn(`Telegram photo download failed: ${err.message}`);
410
+ }
411
+ }
347
412
  getLabel(session) {
348
413
  return session.cwd?.replace(/^.*[/\\]/, "") || session.name;
349
414
  }
@@ -357,9 +422,9 @@ ${sessionList}`);
357
422
  };
358
423
 
359
424
  // src/shared/config.ts
360
- import fs from "fs";
361
- import path2 from "path";
362
- import { randomUUID } from "crypto";
425
+ import fs2 from "fs";
426
+ import path3 from "path";
427
+ import { randomUUID as randomUUID2 } from "crypto";
363
428
  var DEFAULT_CONFIG = {
364
429
  hub: {
365
430
  host: DEFAULT_HUB_HOST,
@@ -372,18 +437,18 @@ var DEFAULT_CONFIG = {
372
437
  webhooks: []
373
438
  };
374
439
  function ensureConfigDir() {
375
- if (!fs.existsSync(CONFIG_DIR)) {
376
- fs.mkdirSync(CONFIG_DIR, { recursive: true });
440
+ if (!fs2.existsSync(CONFIG_DIR)) {
441
+ fs2.mkdirSync(CONFIG_DIR, { recursive: true });
377
442
  }
378
443
  }
379
444
  function loadConfig() {
380
445
  ensureConfigDir();
381
446
  let config;
382
- if (!fs.existsSync(CONFIG_FILE)) {
447
+ if (!fs2.existsSync(CONFIG_FILE)) {
383
448
  config = { ...DEFAULT_CONFIG, hub: { ...DEFAULT_CONFIG.hub } };
384
449
  } else {
385
450
  try {
386
- const raw = fs.readFileSync(CONFIG_FILE, "utf-8");
451
+ const raw = fs2.readFileSync(CONFIG_FILE, "utf-8");
387
452
  const parsed = JSON.parse(raw);
388
453
  config = { ...DEFAULT_CONFIG, ...parsed, hub: { ...DEFAULT_CONFIG.hub, ...parsed.hub }, ...parsed.telegram ? { telegram: parsed.telegram } : {} };
389
454
  } catch {
@@ -391,22 +456,22 @@ function loadConfig() {
391
456
  }
392
457
  }
393
458
  if (!config.hub.token) {
394
- config.hub.token = randomUUID();
459
+ config.hub.token = randomUUID2();
395
460
  saveConfig(config);
396
461
  }
397
462
  return config;
398
463
  }
399
464
  function saveConfig(config) {
400
465
  ensureConfigDir();
401
- fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { encoding: "utf-8", mode: 384 });
466
+ fs2.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { encoding: "utf-8", mode: 384 });
402
467
  }
403
468
  function setupMcpConfig(targetDir) {
404
469
  const dir = targetDir ?? process.cwd();
405
- const mcpPath = path2.join(dir, ".mcp.json");
470
+ const mcpPath = path3.join(dir, ".mcp.json");
406
471
  let mcpConfig = {};
407
- if (fs.existsSync(mcpPath)) {
472
+ if (fs2.existsSync(mcpPath)) {
408
473
  try {
409
- mcpConfig = JSON.parse(fs.readFileSync(mcpPath, "utf-8"));
474
+ mcpConfig = JSON.parse(fs2.readFileSync(mcpPath, "utf-8"));
410
475
  } catch {
411
476
  mcpConfig = {};
412
477
  }
@@ -418,15 +483,15 @@ function setupMcpConfig(targetDir) {
418
483
  command: "npx",
419
484
  args: ["-y", "@delt/claude-alarm", "serve"],
420
485
  env: {
421
- CLAUDE_ALARM_SESSION_NAME: path2.basename(dir)
486
+ CLAUDE_ALARM_SESSION_NAME: path3.basename(dir)
422
487
  }
423
488
  };
424
- fs.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2), "utf-8");
489
+ fs2.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2), "utf-8");
425
490
  return mcpPath;
426
491
  }
427
492
 
428
493
  // src/hub/server.ts
429
- var __dirname = path3.dirname(fileURLToPath(import.meta.url));
494
+ var __dirname = path4.dirname(fileURLToPath(import.meta.url));
430
495
  var HubServer = class {
431
496
  httpServer;
432
497
  wssChannel;
@@ -575,29 +640,31 @@ var HubServer = class {
575
640
  this.handleTelegramSave(req, res);
576
641
  } else if (url.pathname === "/api/telegram/test" && req.method === "POST") {
577
642
  this.handleTelegramTest(req, res);
643
+ } else if (url.pathname === "/api/telegram/detect" && req.method === "POST") {
644
+ this.handleTelegramDetect(req, res);
578
645
  } else {
579
646
  this.jsonResponse(res, 404, { error: "Not found" });
580
647
  }
581
648
  }
582
649
  serveDashboard(res) {
583
650
  const candidates = [
584
- path3.join(__dirname, "..", "dashboard", "index.html"),
651
+ path4.join(__dirname, "..", "dashboard", "index.html"),
585
652
  // from dist/hub/
586
- path3.join(__dirname, "dashboard", "index.html"),
653
+ path4.join(__dirname, "dashboard", "index.html"),
587
654
  // from dist/ (bundled index.js)
588
- path3.join(__dirname, "..", "..", "src", "dashboard", "index.html"),
655
+ path4.join(__dirname, "..", "..", "src", "dashboard", "index.html"),
589
656
  // from dist/hub/ -> src/
590
- path3.join(__dirname, "..", "src", "dashboard", "index.html"),
657
+ path4.join(__dirname, "..", "src", "dashboard", "index.html"),
591
658
  // from dist/ -> src/
592
- path3.join(process.cwd(), "dist", "dashboard", "index.html"),
659
+ path4.join(process.cwd(), "dist", "dashboard", "index.html"),
593
660
  // from cwd
594
- path3.join(process.cwd(), "src", "dashboard", "index.html")
661
+ path4.join(process.cwd(), "src", "dashboard", "index.html")
595
662
  // from cwd/src
596
663
  ];
597
664
  logger.debug(`Dashboard candidates: ${JSON.stringify(candidates)}`);
598
665
  for (const candidate of candidates) {
599
- if (fs2.existsSync(candidate)) {
600
- const html = fs2.readFileSync(candidate, "utf-8");
666
+ if (fs3.existsSync(candidate)) {
667
+ const html = fs3.readFileSync(candidate, "utf-8");
601
668
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
602
669
  res.end(html);
603
670
  return;
@@ -775,11 +842,11 @@ var HubServer = class {
775
842
  logger.warn("Image upload rejected: exceeds 10MB");
776
843
  return;
777
844
  }
778
- fs2.mkdirSync(UPLOADS_DIR, { recursive: true });
845
+ fs3.mkdirSync(UPLOADS_DIR, { recursive: true });
779
846
  const ext = mimeType.split("/")[1] === "jpeg" ? "jpg" : mimeType.split("/")[1];
780
- const filename = `${randomUUID2()}.${ext}`;
781
- const filePath = path3.join(UPLOADS_DIR, filename);
782
- fs2.writeFileSync(filePath, buffer);
847
+ const filename = `${randomUUID3()}.${ext}`;
848
+ const filePath = path4.join(UPLOADS_DIR, filename);
849
+ fs3.writeFileSync(filePath, buffer);
783
850
  const forwardMsg = {
784
851
  type: "image_to_session",
785
852
  sessionId,
@@ -792,7 +859,7 @@ var HubServer = class {
792
859
  logger.info(`Image saved and forwarded: ${filename} (${buffer.length} bytes)`);
793
860
  setTimeout(() => {
794
861
  try {
795
- fs2.unlinkSync(filePath);
862
+ fs3.unlinkSync(filePath);
796
863
  } catch {
797
864
  }
798
865
  }, 5 * 60 * 1e3);
@@ -825,6 +892,14 @@ var HubServer = class {
825
892
  logger.info(`Telegram message forwarded to session: ${sessionId}`);
826
893
  }
827
894
  };
895
+ this.telegramBot.onImageToSession = (sessionId, imagePath, mimeType, caption) => {
896
+ const channelWs = this.channelSockets.get(sessionId);
897
+ if (channelWs?.readyState === WebSocket.OPEN) {
898
+ const msg = { type: "image_to_session", sessionId, imagePath, mimeType, content: caption };
899
+ channelWs.send(JSON.stringify(msg));
900
+ logger.info(`Telegram photo forwarded to session: ${sessionId}`);
901
+ }
902
+ };
828
903
  this.notifier.configure({ telegramBot: this.telegramBot });
829
904
  this.telegramBot.startPolling();
830
905
  logger.info("Telegram bot initialized");
@@ -883,13 +958,57 @@ var HubServer = class {
883
958
  this.jsonResponse(res, 500, { error: err.message });
884
959
  }
885
960
  }
961
+ async handleTelegramDetect(req, res) {
962
+ const body = await this.readBody(req);
963
+ if (!body) {
964
+ this.jsonResponse(res, 400, { error: "Invalid JSON" });
965
+ return;
966
+ }
967
+ const { botToken } = body;
968
+ if (!botToken) {
969
+ this.jsonResponse(res, 400, { error: "botToken required" });
970
+ return;
971
+ }
972
+ try {
973
+ const detectRes = await fetch(`https://api.telegram.org/bot${botToken}/getUpdates?timeout=0&limit=10`, {
974
+ signal: AbortSignal.timeout(1e4)
975
+ });
976
+ if (!detectRes.ok) {
977
+ const err = await detectRes.json();
978
+ this.jsonResponse(res, 400, { error: err.description || "Invalid bot token" });
979
+ return;
980
+ }
981
+ const data = await detectRes.json();
982
+ if (!data.ok || !data.result.length) {
983
+ this.jsonResponse(res, 200, { ok: false, chats: [] });
984
+ return;
985
+ }
986
+ const chatMap = /* @__PURE__ */ new Map();
987
+ for (const update of data.result) {
988
+ if (update.message?.chat) {
989
+ const chat = update.message.chat;
990
+ const id = String(chat.id);
991
+ if (!chatMap.has(id)) {
992
+ chatMap.set(id, {
993
+ id,
994
+ name: chat.title || chat.first_name || id,
995
+ type: chat.type
996
+ });
997
+ }
998
+ }
999
+ }
1000
+ this.jsonResponse(res, 200, { ok: true, chats: [...chatMap.values()] });
1001
+ } catch (err) {
1002
+ this.jsonResponse(res, 500, { error: err.message });
1003
+ }
1004
+ }
886
1005
  cleanupUploads() {
887
1006
  try {
888
- if (!fs2.existsSync(UPLOADS_DIR)) return;
889
- const files = fs2.readdirSync(UPLOADS_DIR);
1007
+ if (!fs3.existsSync(UPLOADS_DIR)) return;
1008
+ const files = fs3.readdirSync(UPLOADS_DIR);
890
1009
  for (const file of files) {
891
1010
  try {
892
- fs2.unlinkSync(path3.join(UPLOADS_DIR, file));
1011
+ fs3.unlinkSync(path4.join(UPLOADS_DIR, file));
893
1012
  } catch {
894
1013
  }
895
1014
  }