@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.
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;
@@ -583,23 +648,23 @@ var HubServer = class {
583
648
  }
584
649
  serveDashboard(res) {
585
650
  const candidates = [
586
- path3.join(__dirname, "..", "dashboard", "index.html"),
651
+ path4.join(__dirname, "..", "dashboard", "index.html"),
587
652
  // from dist/hub/
588
- path3.join(__dirname, "dashboard", "index.html"),
653
+ path4.join(__dirname, "dashboard", "index.html"),
589
654
  // from dist/ (bundled index.js)
590
- path3.join(__dirname, "..", "..", "src", "dashboard", "index.html"),
655
+ path4.join(__dirname, "..", "..", "src", "dashboard", "index.html"),
591
656
  // from dist/hub/ -> src/
592
- path3.join(__dirname, "..", "src", "dashboard", "index.html"),
657
+ path4.join(__dirname, "..", "src", "dashboard", "index.html"),
593
658
  // from dist/ -> src/
594
- path3.join(process.cwd(), "dist", "dashboard", "index.html"),
659
+ path4.join(process.cwd(), "dist", "dashboard", "index.html"),
595
660
  // from cwd
596
- path3.join(process.cwd(), "src", "dashboard", "index.html")
661
+ path4.join(process.cwd(), "src", "dashboard", "index.html")
597
662
  // from cwd/src
598
663
  ];
599
664
  logger.debug(`Dashboard candidates: ${JSON.stringify(candidates)}`);
600
665
  for (const candidate of candidates) {
601
- if (fs2.existsSync(candidate)) {
602
- const html = fs2.readFileSync(candidate, "utf-8");
666
+ if (fs3.existsSync(candidate)) {
667
+ const html = fs3.readFileSync(candidate, "utf-8");
603
668
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
604
669
  res.end(html);
605
670
  return;
@@ -777,11 +842,11 @@ var HubServer = class {
777
842
  logger.warn("Image upload rejected: exceeds 10MB");
778
843
  return;
779
844
  }
780
- fs2.mkdirSync(UPLOADS_DIR, { recursive: true });
845
+ fs3.mkdirSync(UPLOADS_DIR, { recursive: true });
781
846
  const ext = mimeType.split("/")[1] === "jpeg" ? "jpg" : mimeType.split("/")[1];
782
- const filename = `${randomUUID2()}.${ext}`;
783
- const filePath = path3.join(UPLOADS_DIR, filename);
784
- fs2.writeFileSync(filePath, buffer);
847
+ const filename = `${randomUUID3()}.${ext}`;
848
+ const filePath = path4.join(UPLOADS_DIR, filename);
849
+ fs3.writeFileSync(filePath, buffer);
785
850
  const forwardMsg = {
786
851
  type: "image_to_session",
787
852
  sessionId,
@@ -794,7 +859,7 @@ var HubServer = class {
794
859
  logger.info(`Image saved and forwarded: ${filename} (${buffer.length} bytes)`);
795
860
  setTimeout(() => {
796
861
  try {
797
- fs2.unlinkSync(filePath);
862
+ fs3.unlinkSync(filePath);
798
863
  } catch {
799
864
  }
800
865
  }, 5 * 60 * 1e3);
@@ -827,6 +892,14 @@ var HubServer = class {
827
892
  logger.info(`Telegram message forwarded to session: ${sessionId}`);
828
893
  }
829
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
+ };
830
903
  this.notifier.configure({ telegramBot: this.telegramBot });
831
904
  this.telegramBot.startPolling();
832
905
  logger.info("Telegram bot initialized");
@@ -931,11 +1004,11 @@ var HubServer = class {
931
1004
  }
932
1005
  cleanupUploads() {
933
1006
  try {
934
- if (!fs2.existsSync(UPLOADS_DIR)) return;
935
- const files = fs2.readdirSync(UPLOADS_DIR);
1007
+ if (!fs3.existsSync(UPLOADS_DIR)) return;
1008
+ const files = fs3.readdirSync(UPLOADS_DIR);
936
1009
  for (const file of files) {
937
1010
  try {
938
- fs2.unlinkSync(path3.join(UPLOADS_DIR, file));
1011
+ fs3.unlinkSync(path4.join(UPLOADS_DIR, file));
939
1012
  } catch {
940
1013
  }
941
1014
  }