@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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@delt/claude-alarm",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Monitor and get notifications from multiple Claude Code sessions via MCP Channels",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -28,7 +28,7 @@
28
28
  "dev": "tsup --watch",
29
29
  "start:hub": "node dist/cli.js hub start",
30
30
  "start:channel": "node dist/channel/server.js",
31
- "test": "node --import tsx --import ./test/isolate-home.ts --test --test-timeout=30000 \"test/**/*.test.ts\""
31
+ "test": "node --import tsx --import ./test/isolate-home.ts --test --test-timeout=60000 \"test/**/*.test.ts\""
32
32
  },
33
33
  "keywords": [
34
34
  "claude",
@@ -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