@delt/claude-alarm 1.3.0 → 1.4.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
@@ -49,6 +49,7 @@ type ChannelMessage = {
49
49
  title: string;
50
50
  message: string;
51
51
  level?: NotifyLevel;
52
+ to?: MessageSource;
52
53
  } | {
53
54
  type: 'reply';
54
55
  sessionId: string;
@@ -78,6 +79,10 @@ type ChannelMessage = {
78
79
  sessionId: string;
79
80
  reason: string;
80
81
  withText: boolean;
82
+ } | {
83
+ type: 'message_rejected';
84
+ sessionId: string;
85
+ reason: string;
81
86
  } | {
82
87
  type: 'sessions_list';
83
88
  sessions: SessionInfo[];
@@ -321,8 +326,8 @@ declare class TelegramBot {
321
326
  private polling;
322
327
  private pollTimer;
323
328
  private messageSessionMap;
324
- onMessageToSession?: (sessionId: string, content: string) => void;
325
- onImageToSession?: (sessionId: string, imagePath: string, mimeType: string, caption?: string) => void;
329
+ onMessageToSession?: (sessionId: string, content: string) => boolean;
330
+ onImageToSession?: (sessionId: string, imagePath: string, mimeType: string, caption?: string) => boolean;
326
331
  onPermissionVerdict?: (sessionId: string, requestId: string, behavior: 'allow' | 'deny') => void;
327
332
  onChoiceVerdict?: (sessionId: string, requestId: string, choiceId: string) => void;
328
333
  private choiceTokens;
@@ -345,6 +350,7 @@ declare class TelegramBot {
345
350
  private scheduleNextPoll;
346
351
  private handleIncomingMessage;
347
352
  private deliverToSession;
353
+ private deliverPending;
348
354
  private deliverPhotoToSession;
349
355
  private deliverPhotoToSessionByFileId;
350
356
  private photoNotDelivered;
package/dist/index.js CHANGED
@@ -249,6 +249,7 @@ var MAX_SELECTIONS = 20;
249
249
  var MAX_VISIBLE_CHARS = 4e3;
250
250
  var TRUNCATED = "\u2026(truncated)";
251
251
  var MAX_PHOTO_BYTES = 10 * 1024 * 1024;
252
+ var NO_LONGER_CONNECTED = "the session is no longer connected";
252
253
  function visibleLength(html) {
253
254
  return html.replace(/<[^>]*>/g, "").replace(/&(?:amp|lt|gt);/g, "&").length;
254
255
  }
@@ -422,13 +423,7 @@ ${TRUNCATED}`);
422
423
  this.selections.delete(latest);
423
424
  const session = this.pendingSession(pending, parseInt(selectMatch[1], 10) - 1);
424
425
  if (session) {
425
- let delivered = true;
426
- if (pending.photoFileId) {
427
- delivered = await this.deliverPhotoToSessionByFileId(session.id, pending.photoFileId, pending.caption);
428
- } else if (pending.text) {
429
- this.deliverToSession(session.id, pending.text);
430
- }
431
- if (delivered) this.sendMessage(`Sent to [${this.getLabel(session)}]`);
426
+ if (await this.deliverPending(session.id, pending)) this.sendMessage(`Sent to [${this.getLabel(session)}]`);
432
427
  } else {
433
428
  this.sendMessage("Invalid session number.");
434
429
  }
@@ -470,9 +465,13 @@ ${TRUNCATED}`);
470
465
  this.sendMessage("Multiple sessions active. Select one:", void 0, { inline_keyboard: rows });
471
466
  }
472
467
  deliverToSession(sessionId, content) {
473
- if (this.onMessageToSession) {
474
- this.onMessageToSession(sessionId, content);
475
- }
468
+ const delivered = this.onMessageToSession?.(sessionId, content) === true;
469
+ if (!delivered) void this.sendMessage(`Not delivered: ${NO_LONGER_CONNECTED}`);
470
+ return delivered;
471
+ }
472
+ async deliverPending(sessionId, pending) {
473
+ if (pending.photoFileId) return this.deliverPhotoToSessionByFileId(sessionId, pending.photoFileId, pending.caption);
474
+ return pending.text ? this.deliverToSession(sessionId, pending.text) : false;
476
475
  }
477
476
  async deliverPhotoToSession(sessionId, photos, caption) {
478
477
  const largest = photos[photos.length - 1];
@@ -504,8 +503,19 @@ ${TRUNCATED}`);
504
503
  const filePath = path2.join(UPLOADS_DIR, filename);
505
504
  fs.writeFileSync(filePath, buffer);
506
505
  logger.info(`Telegram photo saved: ${filename} (${buffer.length} bytes)`);
507
- if (this.onImageToSession) {
508
- this.onImageToSession(sessionId, filePath, mimeType, caption);
506
+ let accepted = false;
507
+ try {
508
+ accepted = this.onImageToSession?.(sessionId, filePath, mimeType, caption) === true;
509
+ } catch (err) {
510
+ logger.warn(`Telegram photo hand-off failed: ${err.message}`);
511
+ }
512
+ if (!accepted) {
513
+ try {
514
+ fs.unlinkSync(filePath);
515
+ } catch {
516
+ }
517
+ this.photoNotDelivered(NO_LONGER_CONNECTED);
518
+ return false;
509
519
  }
510
520
  setTimeout(() => {
511
521
  try {
@@ -513,7 +523,7 @@ ${TRUNCATED}`);
513
523
  } catch {
514
524
  }
515
525
  }, 5 * 60 * 1e3).unref();
516
- return this.onImageToSession !== void 0;
526
+ return true;
517
527
  } catch (err) {
518
528
  logger.warn(`Telegram photo download failed: ${err.message}`);
519
529
  this.photoNotDelivered("the download failed");
@@ -708,15 +718,10 @@ ${label}`);
708
718
  return;
709
719
  }
710
720
  this.selections.delete(selectionId);
711
- if (pending.photoFileId) {
712
- const delivered = await this.deliverPhotoToSessionByFileId(session.id, pending.photoFileId, pending.caption);
713
- if (!delivered) {
714
- await this.answerCallbackQuery(query.id, "Photo not delivered");
715
- if (query.message) await this.removeButtons(query.message.chat.id, query.message.message_id);
716
- return;
717
- }
718
- } else if (pending.text) {
719
- this.deliverToSession(session.id, pending.text);
721
+ if (!await this.deliverPending(session.id, pending)) {
722
+ await this.answerCallbackQuery(query.id, pending.photoFileId ? "Photo not delivered" : "Not delivered");
723
+ if (query.message) await this.removeButtons(query.message.chat.id, query.message.message_id);
724
+ return;
720
725
  }
721
726
  await this.answerCallbackQuery(query.id, `Sent to ${this.getLabel(session)}`);
722
727
  if (query.message) {
@@ -841,6 +846,11 @@ ${bodyLines.join("\n")}`;
841
846
  import { spawn } from "child_process";
842
847
  import fs2 from "fs";
843
848
  import path3 from "path";
849
+
850
+ // src/codex/shutdown.ts
851
+ var SUPERVISOR_STOP_GRACE_MS = 3e4;
852
+
853
+ // src/hub/codex-supervisor.ts
844
854
  var HEALTHY_RUN_MS = 6e4;
845
855
  function resolveAdapterScript(baseDir) {
846
856
  return [path3.join(baseDir, "..", "codex", "main.js"), path3.join(baseDir, "codex", "main.js")].find((p) => fs2.existsSync(p));
@@ -856,7 +866,7 @@ var CodexSupervisor = class {
856
866
  this.spawnFn = opts.spawnFn ?? spawn;
857
867
  this.minDelayMs = opts.minDelayMs ?? 2e3;
858
868
  this.maxDelayMs = opts.maxDelayMs ?? 6e4;
859
- this.stopGraceMs = opts.stopGraceMs ?? 3e3;
869
+ this.stopGraceMs = opts.stopGraceMs ?? SUPERVISOR_STOP_GRACE_MS;
860
870
  this.env = opts.env;
861
871
  this.delay = this.minDelayMs;
862
872
  }
@@ -879,11 +889,22 @@ var CodexSupervisor = class {
879
889
  this.timer = void 0;
880
890
  const child = this.child;
881
891
  this.child = void 0;
882
- if (!child) return;
883
- child.stdin?.end();
884
- const force = setTimeout(() => child.kill(), this.stopGraceMs);
885
- force.unref();
886
- child.once("exit", () => clearTimeout(force));
892
+ if (!child) return Promise.resolve();
893
+ return new Promise((resolve) => {
894
+ const done = () => {
895
+ clearTimeout(force);
896
+ child.removeListener("exit", done);
897
+ resolve();
898
+ };
899
+ const force = setTimeout(() => {
900
+ logger.warn(`Codex adapter did not exit within ${this.stopGraceMs}ms of stop; killing it`);
901
+ child.kill();
902
+ done();
903
+ }, this.stopGraceMs);
904
+ force.unref();
905
+ child.once("exit", done);
906
+ child.stdin?.end();
907
+ });
887
908
  }
888
909
  launch() {
889
910
  const startedAt = Date.now();
@@ -1217,9 +1238,9 @@ var HubServer = class {
1217
1238
  });
1218
1239
  }
1219
1240
  stop() {
1220
- return new Promise((resolve) => {
1221
- this.codexSupervisor?.stop();
1222
- this.codexSupervisor = void 0;
1241
+ const adapterStopped = this.codexSupervisor?.stop();
1242
+ this.codexSupervisor = void 0;
1243
+ const serverStopped = new Promise((resolve) => {
1223
1244
  if (this.heartbeatInterval) clearInterval(this.heartbeatInterval);
1224
1245
  if (this.telegramBot) this.telegramBot.stopPolling();
1225
1246
  for (const ws of this.channelSockets.values()) ws.terminate();
@@ -1239,6 +1260,8 @@ var HubServer = class {
1239
1260
  resolve();
1240
1261
  }, 3e3);
1241
1262
  });
1263
+ return Promise.all([serverStopped, adapterStopped]).then(() => {
1264
+ });
1242
1265
  }
1243
1266
  // --- HTTP Handler ---
1244
1267
  handleHttp(req, res) {
@@ -1495,7 +1518,16 @@ var HubServer = class {
1495
1518
  this.sessions.updateActivity(msg.sessionId);
1496
1519
  const notifySession = this.sessions.get(msg.sessionId);
1497
1520
  const notifyLabel = this.getSessionLabel(notifySession);
1498
- this.notifier.notifyWithSession(msg.sessionId, notifyLabel, `[${notifyLabel}] ${msg.title}`, msg.message, msg.level ?? "info");
1521
+ const notifyTitle = `[${notifyLabel}] ${msg.title}`;
1522
+ if (msg.to === "api") {
1523
+ logger.info(`${notifyTitle}: ${msg.message}`);
1524
+ break;
1525
+ }
1526
+ if (msg.to === "telegram") {
1527
+ void this.telegramBot?.sendNotification(msg.sessionId, notifyLabel, notifyTitle, msg.message);
1528
+ break;
1529
+ }
1530
+ if (msg.to !== "dashboard") this.notifier.notifyWithSession(msg.sessionId, notifyLabel, notifyTitle, msg.message, msg.level ?? "info");
1499
1531
  this.broadcastToDashboards({
1500
1532
  type: "notification",
1501
1533
  sessionId: msg.sessionId,
@@ -1697,6 +1729,11 @@ var HubServer = class {
1697
1729
  const channelWs = this.channelSockets.get(msg.sessionId);
1698
1730
  if (channelWs?.readyState === WebSocket.OPEN) {
1699
1731
  channelWs.send(JSON.stringify({ ...msg, source: "dashboard" }));
1732
+ } else {
1733
+ const reason = "the session is not connected";
1734
+ logger.warn(`Message rejected for ${msg.sessionId}: ${reason}`);
1735
+ const rejected = { type: "message_rejected", sessionId: msg.sessionId, reason };
1736
+ if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(rejected));
1700
1737
  }
1701
1738
  } else if (msg.type === "image_upload") {
1702
1739
  this.handleImageUpload(ws, msg);
@@ -1822,19 +1859,25 @@ var HubServer = class {
1822
1859
  this.telegramBot.getSessions = () => this.sessions.getAll();
1823
1860
  this.telegramBot.onMessageToSession = (sessionId, content) => {
1824
1861
  const channelWs = this.channelSockets.get(sessionId);
1825
- if (channelWs?.readyState === WebSocket.OPEN) {
1826
- const msg = { type: "message_to_session", sessionId, content, source: "telegram" };
1827
- channelWs.send(JSON.stringify(msg));
1828
- logger.info(`Telegram message forwarded to session: ${sessionId}`);
1862
+ if (channelWs?.readyState !== WebSocket.OPEN) {
1863
+ logger.warn(`Telegram message not delivered to ${sessionId}: the session is not connected`);
1864
+ return false;
1829
1865
  }
1866
+ const msg = { type: "message_to_session", sessionId, content, source: "telegram" };
1867
+ channelWs.send(JSON.stringify(msg));
1868
+ logger.info(`Telegram message forwarded to session: ${sessionId}`);
1869
+ return true;
1830
1870
  };
1831
1871
  this.telegramBot.onImageToSession = (sessionId, imagePath, mimeType, caption) => {
1832
1872
  const channelWs = this.channelSockets.get(sessionId);
1833
- if (channelWs?.readyState === WebSocket.OPEN) {
1834
- const msg = { type: "image_to_session", sessionId, imagePath, mimeType, content: caption, source: "telegram" };
1835
- channelWs.send(JSON.stringify(msg));
1836
- logger.info(`Telegram photo forwarded to session: ${sessionId}`);
1873
+ if (channelWs?.readyState !== WebSocket.OPEN) {
1874
+ logger.warn(`Telegram photo not delivered to ${sessionId}: the session is not connected`);
1875
+ return false;
1837
1876
  }
1877
+ const msg = { type: "image_to_session", sessionId, imagePath, mimeType, content: caption, source: "telegram" };
1878
+ channelWs.send(JSON.stringify(msg));
1879
+ logger.info(`Telegram photo forwarded to session: ${sessionId}`);
1880
+ return true;
1838
1881
  };
1839
1882
  this.telegramBot.onPermissionVerdict = (sessionId, requestId, behavior) => {
1840
1883
  if (this.forwardPermissionResponse({ sessionId, requestId, behavior })) {