@delt/claude-alarm 0.5.3 → 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.
@@ -2,8 +2,8 @@
2
2
 
3
3
  // src/hub/server.ts
4
4
  import http from "http";
5
- import fs2 from "fs";
6
- import path3 from "path";
5
+ import fs3 from "fs";
6
+ import path4 from "path";
7
7
  import { fileURLToPath } from "url";
8
8
  import { WebSocketServer, WebSocket } from "ws";
9
9
 
@@ -26,7 +26,7 @@ var logger = {
26
26
  };
27
27
 
28
28
  // src/hub/server.ts
29
- import { randomUUID as randomUUID2 } from "crypto";
29
+ import { randomUUID as randomUUID3 } from "crypto";
30
30
 
31
31
  // src/shared/constants.ts
32
32
  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,18 +456,18 @@ 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
 
404
469
  // src/hub/server.ts
405
- var __dirname = path3.dirname(fileURLToPath(import.meta.url));
470
+ var __dirname = path4.dirname(fileURLToPath(import.meta.url));
406
471
  var HubServer = class {
407
472
  httpServer;
408
473
  wssChannel;
@@ -559,23 +624,23 @@ var HubServer = class {
559
624
  }
560
625
  serveDashboard(res) {
561
626
  const candidates = [
562
- path3.join(__dirname, "..", "dashboard", "index.html"),
627
+ path4.join(__dirname, "..", "dashboard", "index.html"),
563
628
  // from dist/hub/
564
- path3.join(__dirname, "dashboard", "index.html"),
629
+ path4.join(__dirname, "dashboard", "index.html"),
565
630
  // from dist/ (bundled index.js)
566
- path3.join(__dirname, "..", "..", "src", "dashboard", "index.html"),
631
+ path4.join(__dirname, "..", "..", "src", "dashboard", "index.html"),
567
632
  // from dist/hub/ -> src/
568
- path3.join(__dirname, "..", "src", "dashboard", "index.html"),
633
+ path4.join(__dirname, "..", "src", "dashboard", "index.html"),
569
634
  // from dist/ -> src/
570
- path3.join(process.cwd(), "dist", "dashboard", "index.html"),
635
+ path4.join(process.cwd(), "dist", "dashboard", "index.html"),
571
636
  // from cwd
572
- path3.join(process.cwd(), "src", "dashboard", "index.html")
637
+ path4.join(process.cwd(), "src", "dashboard", "index.html")
573
638
  // from cwd/src
574
639
  ];
575
640
  logger.debug(`Dashboard candidates: ${JSON.stringify(candidates)}`);
576
641
  for (const candidate of candidates) {
577
- if (fs2.existsSync(candidate)) {
578
- const html = fs2.readFileSync(candidate, "utf-8");
642
+ if (fs3.existsSync(candidate)) {
643
+ const html = fs3.readFileSync(candidate, "utf-8");
579
644
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
580
645
  res.end(html);
581
646
  return;
@@ -753,11 +818,11 @@ var HubServer = class {
753
818
  logger.warn("Image upload rejected: exceeds 10MB");
754
819
  return;
755
820
  }
756
- fs2.mkdirSync(UPLOADS_DIR, { recursive: true });
821
+ fs3.mkdirSync(UPLOADS_DIR, { recursive: true });
757
822
  const ext = mimeType.split("/")[1] === "jpeg" ? "jpg" : mimeType.split("/")[1];
758
- const filename = `${randomUUID2()}.${ext}`;
759
- const filePath = path3.join(UPLOADS_DIR, filename);
760
- fs2.writeFileSync(filePath, buffer);
823
+ const filename = `${randomUUID3()}.${ext}`;
824
+ const filePath = path4.join(UPLOADS_DIR, filename);
825
+ fs3.writeFileSync(filePath, buffer);
761
826
  const forwardMsg = {
762
827
  type: "image_to_session",
763
828
  sessionId,
@@ -770,7 +835,7 @@ var HubServer = class {
770
835
  logger.info(`Image saved and forwarded: ${filename} (${buffer.length} bytes)`);
771
836
  setTimeout(() => {
772
837
  try {
773
- fs2.unlinkSync(filePath);
838
+ fs3.unlinkSync(filePath);
774
839
  } catch {
775
840
  }
776
841
  }, 5 * 60 * 1e3);
@@ -803,6 +868,14 @@ var HubServer = class {
803
868
  logger.info(`Telegram message forwarded to session: ${sessionId}`);
804
869
  }
805
870
  };
871
+ this.telegramBot.onImageToSession = (sessionId, imagePath, mimeType, caption) => {
872
+ const channelWs = this.channelSockets.get(sessionId);
873
+ if (channelWs?.readyState === WebSocket.OPEN) {
874
+ const msg = { type: "image_to_session", sessionId, imagePath, mimeType, content: caption };
875
+ channelWs.send(JSON.stringify(msg));
876
+ logger.info(`Telegram photo forwarded to session: ${sessionId}`);
877
+ }
878
+ };
806
879
  this.notifier.configure({ telegramBot: this.telegramBot });
807
880
  this.telegramBot.startPolling();
808
881
  logger.info("Telegram bot initialized");
@@ -907,11 +980,11 @@ var HubServer = class {
907
980
  }
908
981
  cleanupUploads() {
909
982
  try {
910
- if (!fs2.existsSync(UPLOADS_DIR)) return;
911
- const files = fs2.readdirSync(UPLOADS_DIR);
983
+ if (!fs3.existsSync(UPLOADS_DIR)) return;
984
+ const files = fs3.readdirSync(UPLOADS_DIR);
912
985
  for (const file of files) {
913
986
  try {
914
- fs2.unlinkSync(path3.join(UPLOADS_DIR, file));
987
+ fs3.unlinkSync(path4.join(UPLOADS_DIR, file));
915
988
  } catch {
916
989
  }
917
990
  }