@delt/claude-alarm 1.3.0 → 1.4.0

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.
@@ -1286,13 +1286,20 @@
1286
1286
  }
1287
1287
 
1288
1288
  function showUploadRejected(msg) {
1289
- state.waitingReply[msg.sessionId] = false;
1290
- const label = msg.withText ? 'Image and message not delivered' : 'Image not delivered';
1291
- if (state.selectedSession === msg.sessionId) {
1289
+ showNotDelivered(msg.sessionId, msg.withText ? 'Image and message not delivered' : 'Image not delivered', msg.reason);
1290
+ }
1291
+
1292
+ function showMessageRejected(msg) {
1293
+ showNotDelivered(msg.sessionId, 'Message not delivered', msg.reason);
1294
+ }
1295
+
1296
+ function showNotDelivered(sessionId, label, reason) {
1297
+ state.waitingReply[sessionId] = false;
1298
+ if (state.selectedSession === sessionId) {
1292
1299
  renderMessages();
1293
- showMentionError(`${label}: ${msg.reason}`);
1300
+ showMentionError(`${label}: ${reason}`);
1294
1301
  }
1295
- state.notifications.unshift({ sessionId: msg.sessionId, title: label, message: msg.reason, level: 'warning', time: Date.now() });
1302
+ state.notifications.unshift({ sessionId, title: label, message: reason, level: 'warning', time: Date.now() });
1296
1303
  renderNotifications();
1297
1304
  }
1298
1305
 
@@ -1462,32 +1469,15 @@
1462
1469
  case 'upload_rejected':
1463
1470
  showUploadRejected(msg);
1464
1471
  break;
1472
+ case 'message_rejected':
1473
+ showMessageRejected(msg);
1474
+ break;
1465
1475
  case 'notification':
1466
1476
  state.notifications.unshift({ sessionId: msg.sessionId, title: msg.title, message: msg.message, level: msg.level || 'info', time: msg.timestamp });
1467
1477
  renderNotifications();
1468
1478
  break;
1469
1479
  case 'permission_request':
1470
- if (!state.permissionRequests[msg.sessionId]) state.permissionRequests[msg.sessionId] = [];
1471
- state.permissionRequests[msg.sessionId].unshift({
1472
- requestId: msg.requestId,
1473
- toolName: msg.toolName,
1474
- description: msg.description,
1475
- inputPreview: msg.inputPreview,
1476
- timestamp: msg.timestamp,
1477
- resolved: false,
1478
- behavior: null,
1479
- choices: msg.choices || null,
1480
- sent: null,
1481
- outcome: null,
1482
- });
1483
- // Auto-select session if none selected
1484
- if (!state.selectedSession && state.sessions[msg.sessionId]) selectSession(msg.sessionId);
1485
- if (state.selectedSession === msg.sessionId) { renderMessages(); renderPermissionBar(); }
1486
- // Also show in notifications with permission info
1487
- state.notifications.unshift({ sessionId: msg.sessionId, title: 'Permission Request', message: `${msg.toolName}: ${msg.description}`, level: 'warning', time: msg.timestamp, permRequestId: msg.requestId });
1488
- renderNotifications();
1489
- // Play notification sound effect via title flash
1490
- flashTitle('Permission Request');
1480
+ showPermissionRequest(msg);
1491
1481
  break;
1492
1482
  case 'permission_response':
1493
1483
  // Another source (Telegram, other dashboard) resolved a permission request
@@ -1501,11 +1491,7 @@
1501
1491
  }
1502
1492
  break;
1503
1493
  case 'permission_pending':
1504
- for (const m of applyPendingChoices(state.permissionRequests, msg.requests || [])) {
1505
- handleMessage({ type: 'permission_request', ...m });
1506
- }
1507
- renderPermissionBar();
1508
- renderNotifications();
1494
+ restorePendingRequests(msg.requests || []);
1509
1495
  break;
1510
1496
  case 'codex_adapters': {
1511
1497
  state.codexAdapters = msg.adapters || [];
@@ -2128,6 +2114,46 @@
2128
2114
  return missing;
2129
2115
  }
2130
2116
 
2117
+ function addPermissionRequest(msg) {
2118
+ if (!state.permissionRequests[msg.sessionId]) state.permissionRequests[msg.sessionId] = [];
2119
+ state.permissionRequests[msg.sessionId].unshift({
2120
+ requestId: msg.requestId,
2121
+ toolName: msg.toolName,
2122
+ description: msg.description,
2123
+ inputPreview: msg.inputPreview,
2124
+ timestamp: msg.timestamp,
2125
+ resolved: false,
2126
+ behavior: null,
2127
+ choices: msg.choices || null,
2128
+ sent: null,
2129
+ outcome: null,
2130
+ });
2131
+ if (!state.selectedSession && state.sessions[msg.sessionId]) selectSession(msg.sessionId);
2132
+ if (state.selectedSession === msg.sessionId) { renderMessages(); renderPermissionBar(); }
2133
+ }
2134
+
2135
+ function showPermissionRequest(msg) {
2136
+ addPermissionRequest(msg);
2137
+ state.notifications.unshift({ sessionId: msg.sessionId, title: 'Permission Request', message: `${msg.toolName}: ${msg.description}`, level: 'warning', time: msg.timestamp, permRequestId: msg.requestId });
2138
+ renderNotifications();
2139
+ flashTitle('Permission Request');
2140
+ }
2141
+
2142
+ // A reconnect can bring back many requests at once; one row and one flash keep them from burying the notifications.
2143
+ function restorePendingRequests(requests) {
2144
+ const restored = applyPendingChoices(state.permissionRequests, requests);
2145
+ for (const m of restored) addPermissionRequest(m);
2146
+ if (restored.length) {
2147
+ const label = `${restored.length} approval request(s) waiting`;
2148
+ const listed = restored.slice(0, 3).map(m => `${m.toolName}: ${m.description}`).join(' · ');
2149
+ const message = restored.length > 3 ? `${listed} …and ${restored.length - 3} more` : listed;
2150
+ state.notifications.unshift({ sessionId: restored[0].sessionId, title: label, message, level: 'warning', time: Date.now() });
2151
+ flashTitle(label);
2152
+ }
2153
+ renderPermissionBar();
2154
+ renderNotifications();
2155
+ }
2156
+
2131
2157
  function sendPermissionChoice(sessionId, requestId, choiceId) {
2132
2158
  if (!state.ws || state.ws.readyState !== WebSocket.OPEN) return;
2133
2159
  state.ws.send(JSON.stringify({ type: 'permission_response', sessionId, requestId, choiceId }));
@@ -2211,23 +2237,28 @@
2211
2237
  }, true);
2212
2238
 
2213
2239
  // Flash title for attention
2214
- let flashInterval = null;
2240
+ let flash = null;
2215
2241
  function flashTitle(msg) {
2216
- const original = document.title;
2217
- let on = true;
2218
- clearInterval(flashInterval);
2219
- flashInterval = setInterval(() => {
2220
- document.title = on ? `** ${msg} **` : original;
2221
- on = !on;
2222
- }, 500);
2223
- const stopFlash = () => {
2224
- clearInterval(flashInterval);
2225
- flashInterval = null;
2226
- document.title = original;
2227
- window.removeEventListener('focus', stopFlash);
2242
+ // While flashing, document.title may hold the flash text, so only a flash that starts from rest reads the real title.
2243
+ if (flash) {
2244
+ flash.msg = msg;
2245
+ return;
2246
+ }
2247
+ const current = { msg, original: document.title, on: true, interval: null, timeout: null };
2248
+ const stop = () => {
2249
+ clearInterval(current.interval);
2250
+ clearTimeout(current.timeout);
2251
+ window.removeEventListener('focus', stop);
2252
+ document.title = current.original;
2253
+ flash = null;
2228
2254
  };
2229
- window.addEventListener('focus', stopFlash);
2230
- setTimeout(stopFlash, 30000);
2255
+ current.interval = setInterval(() => {
2256
+ document.title = current.on ? `** ${current.msg} **` : current.original;
2257
+ current.on = !current.on;
2258
+ }, 500);
2259
+ current.timeout = setTimeout(stop, 30000);
2260
+ window.addEventListener('focus', stop);
2261
+ flash = current;
2231
2262
  }
2232
2263
 
2233
2264
  // Clear all notifications
@@ -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) {
@@ -1471,7 +1476,16 @@ var HubServer = class {
1471
1476
  this.sessions.updateActivity(msg.sessionId);
1472
1477
  const notifySession = this.sessions.get(msg.sessionId);
1473
1478
  const notifyLabel = this.getSessionLabel(notifySession);
1474
- this.notifier.notifyWithSession(msg.sessionId, notifyLabel, `[${notifyLabel}] ${msg.title}`, msg.message, msg.level ?? "info");
1479
+ const notifyTitle = `[${notifyLabel}] ${msg.title}`;
1480
+ if (msg.to === "api") {
1481
+ logger.info(`${notifyTitle}: ${msg.message}`);
1482
+ break;
1483
+ }
1484
+ if (msg.to === "telegram") {
1485
+ void this.telegramBot?.sendNotification(msg.sessionId, notifyLabel, notifyTitle, msg.message);
1486
+ break;
1487
+ }
1488
+ if (msg.to !== "dashboard") this.notifier.notifyWithSession(msg.sessionId, notifyLabel, notifyTitle, msg.message, msg.level ?? "info");
1475
1489
  this.broadcastToDashboards({
1476
1490
  type: "notification",
1477
1491
  sessionId: msg.sessionId,
@@ -1673,6 +1687,11 @@ var HubServer = class {
1673
1687
  const channelWs = this.channelSockets.get(msg.sessionId);
1674
1688
  if (channelWs?.readyState === WebSocket.OPEN) {
1675
1689
  channelWs.send(JSON.stringify({ ...msg, source: "dashboard" }));
1690
+ } else {
1691
+ const reason = "the session is not connected";
1692
+ logger.warn(`Message rejected for ${msg.sessionId}: ${reason}`);
1693
+ const rejected = { type: "message_rejected", sessionId: msg.sessionId, reason };
1694
+ if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(rejected));
1676
1695
  }
1677
1696
  } else if (msg.type === "image_upload") {
1678
1697
  this.handleImageUpload(ws, msg);
@@ -1798,19 +1817,25 @@ var HubServer = class {
1798
1817
  this.telegramBot.getSessions = () => this.sessions.getAll();
1799
1818
  this.telegramBot.onMessageToSession = (sessionId, content) => {
1800
1819
  const channelWs = this.channelSockets.get(sessionId);
1801
- if (channelWs?.readyState === WebSocket.OPEN) {
1802
- const msg = { type: "message_to_session", sessionId, content, source: "telegram" };
1803
- channelWs.send(JSON.stringify(msg));
1804
- logger.info(`Telegram message forwarded to session: ${sessionId}`);
1820
+ if (channelWs?.readyState !== WebSocket.OPEN) {
1821
+ logger.warn(`Telegram message not delivered to ${sessionId}: the session is not connected`);
1822
+ return false;
1805
1823
  }
1824
+ const msg = { type: "message_to_session", sessionId, content, source: "telegram" };
1825
+ channelWs.send(JSON.stringify(msg));
1826
+ logger.info(`Telegram message forwarded to session: ${sessionId}`);
1827
+ return true;
1806
1828
  };
1807
1829
  this.telegramBot.onImageToSession = (sessionId, imagePath, mimeType, caption) => {
1808
1830
  const channelWs = this.channelSockets.get(sessionId);
1809
- if (channelWs?.readyState === WebSocket.OPEN) {
1810
- const msg = { type: "image_to_session", sessionId, imagePath, mimeType, content: caption, source: "telegram" };
1811
- channelWs.send(JSON.stringify(msg));
1812
- logger.info(`Telegram photo forwarded to session: ${sessionId}`);
1831
+ if (channelWs?.readyState !== WebSocket.OPEN) {
1832
+ logger.warn(`Telegram photo not delivered to ${sessionId}: the session is not connected`);
1833
+ return false;
1813
1834
  }
1835
+ const msg = { type: "image_to_session", sessionId, imagePath, mimeType, content: caption, source: "telegram" };
1836
+ channelWs.send(JSON.stringify(msg));
1837
+ logger.info(`Telegram photo forwarded to session: ${sessionId}`);
1838
+ return true;
1814
1839
  };
1815
1840
  this.telegramBot.onPermissionVerdict = (sessionId, requestId, behavior) => {
1816
1841
  if (this.forwardPermissionResponse({ sessionId, requestId, behavior })) {