@delt/claude-alarm 1.0.1 → 1.1.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
@@ -2,6 +2,19 @@
2
2
  type SessionStatus = 'idle' | 'working' | 'waiting_input';
3
3
  type AgentKind = 'claude' | 'codex';
4
4
  type MessageSource = 'dashboard' | 'telegram' | 'api';
5
+ interface PermissionChoice {
6
+ id: string;
7
+ label: string;
8
+ }
9
+ interface PendingChoiceRequest {
10
+ sessionId: string;
11
+ requestId: string;
12
+ toolName: string;
13
+ description: string;
14
+ inputPreview: string;
15
+ timestamp: number;
16
+ choices: PermissionChoice[];
17
+ }
5
18
  /** Session info tracked by the hub */
6
19
  interface SessionInfo {
7
20
  id: string;
@@ -90,11 +103,21 @@ type ChannelMessage = {
90
103
  description: string;
91
104
  inputPreview: string;
92
105
  timestamp: number;
106
+ choices?: PermissionChoice[];
93
107
  } | {
94
108
  type: 'permission_response';
95
109
  sessionId: string;
96
110
  requestId: string;
97
- behavior: 'allow' | 'deny';
111
+ behavior?: 'allow' | 'deny';
112
+ choiceId?: string;
113
+ } | {
114
+ type: 'permission_resolved';
115
+ sessionId: string;
116
+ requestId: string;
117
+ state: 'resolved' | 'expired';
118
+ } | {
119
+ type: 'permission_pending';
120
+ requests: PendingChoiceRequest[];
98
121
  } | {
99
122
  type: 'error';
100
123
  message: string;
@@ -155,6 +178,7 @@ declare class HubServer {
155
178
  private heartbeatInterval?;
156
179
  private channelAlive;
157
180
  private socketOwners;
181
+ private choiceRequests;
158
182
  private host;
159
183
  private port;
160
184
  private token?;
@@ -171,6 +195,9 @@ declare class HubServer {
171
195
  private handleChannelConnection;
172
196
  private handleChannelMessage;
173
197
  private handleDashboardConnection;
198
+ private forwardPermissionResponse;
199
+ private resolveChoice;
200
+ private expireChoices;
174
201
  private broadcastToDashboards;
175
202
  private handleImageUpload;
176
203
  private handleWebhookSave;
@@ -235,6 +262,9 @@ declare class TelegramBot {
235
262
  onMessageToSession?: (sessionId: string, content: string) => void;
236
263
  onImageToSession?: (sessionId: string, imagePath: string, mimeType: string, caption?: string) => void;
237
264
  onPermissionVerdict?: (sessionId: string, requestId: string, behavior: 'allow' | 'deny') => void;
265
+ onChoiceVerdict?: (sessionId: string, requestId: string, choiceId: string) => void;
266
+ private choiceTokens;
267
+ private choiceMessages;
238
268
  getSessions?: () => SessionInfo[];
239
269
  private pendingMessages;
240
270
  constructor(config: TelegramConfig);
@@ -257,6 +287,11 @@ declare class TelegramBot {
257
287
  private getLabel;
258
288
  /** Send a permission request with inline buttons */
259
289
  sendPermissionRequest(sessionId: string, sessionLabel: string, requestId: string, toolName: string, description: string, inputPreview: string): Promise<void>;
290
+ sendChoiceRequest(sessionId: string, sessionLabel: string, requestId: string, toolName: string, description: string, inputPreview: string, choices: PermissionChoice[]): Promise<void>;
291
+ resolveChoiceRequest(sessionId: string, requestId: string, state: 'resolved' | 'expired'): Promise<void>;
292
+ private handleChoiceCallback;
293
+ private dropChoiceTokens;
294
+ private trimChoiceMessages;
260
295
  private handleCallbackQuery;
261
296
  private handleSessionSelectCallback;
262
297
  private answerCallbackQuery;
@@ -323,4 +358,4 @@ declare const WS_PATH_DASHBOARD = "/ws/dashboard";
323
358
  declare const CHANNEL_SERVER_NAME = "claude-alarm";
324
359
  declare const CHANNEL_SERVER_VERSION = "0.1.0";
325
360
 
326
- export { type AgentKind, type AppConfig, CHANNEL_SERVER_NAME, CHANNEL_SERVER_VERSION, CODEX_LOG_FILE, CODEX_PID_FILE, CONFIG_DIR, CONFIG_FILE, type ChannelMessage, type CodexConfig, DEFAULT_HUB_HOST, DEFAULT_HUB_PORT, HubClient, HubServer, type HubStatus, LOG_FILE, type MessageSource, Notifier, type NotifyLevel, PID_FILE, type SessionInfo, SessionManager, type SessionStatus, type TelegramConfig, UPLOADS_DIR, WS_PATH_CHANNEL, WS_PATH_DASHBOARD, type WebhookConfig, loadConfig, logger, saveConfig, setupMcpConfig };
361
+ export { type AgentKind, type AppConfig, CHANNEL_SERVER_NAME, CHANNEL_SERVER_VERSION, CODEX_LOG_FILE, CODEX_PID_FILE, CONFIG_DIR, CONFIG_FILE, type ChannelMessage, type CodexConfig, DEFAULT_HUB_HOST, DEFAULT_HUB_PORT, HubClient, HubServer, type HubStatus, LOG_FILE, type MessageSource, Notifier, type NotifyLevel, PID_FILE, type PendingChoiceRequest, type PermissionChoice, type SessionInfo, SessionManager, type SessionStatus, type TelegramConfig, UPLOADS_DIR, WS_PATH_CHANNEL, WS_PATH_DASHBOARD, type WebhookConfig, loadConfig, logger, saveConfig, setupMcpConfig };
package/dist/index.js CHANGED
@@ -234,8 +234,15 @@ function sessionLabel(session) {
234
234
  return session.agentKind === "codex" ? `Codex \xB7 ${base}` : base;
235
235
  }
236
236
 
237
+ // src/shared/permission-key.ts
238
+ function permissionKey(sessionId, requestId) {
239
+ return `${sessionId}
240
+ ${requestId}`;
241
+ }
242
+
237
243
  // src/hub/telegram.ts
238
244
  var TELEGRAM_API = "https://api.telegram.org/bot";
245
+ var MAX_CHOICE_MESSAGES = 200;
239
246
  var TelegramBot = class _TelegramBot {
240
247
  config;
241
248
  offset = 0;
@@ -249,6 +256,11 @@ var TelegramBot = class _TelegramBot {
249
256
  onImageToSession;
250
257
  // Callback: when a permission verdict arrives from Telegram
251
258
  onPermissionVerdict;
259
+ // Callback: when a Codex approval choice arrives from Telegram
260
+ onChoiceVerdict;
261
+ // Telegram caps callback_data at 64 bytes, so buttons carry a short token instead of the ids.
262
+ choiceTokens = /* @__PURE__ */ new Map();
263
+ choiceMessages = /* @__PURE__ */ new Map();
252
264
  // Callback: get current sessions list
253
265
  getSessions;
254
266
  // Pending messages for session selection
@@ -537,6 +549,71 @@ ${previewHtml}${truncNote}`;
537
549
  };
538
550
  await this.sendMessage(text, void 0, replyMarkup);
539
551
  }
552
+ async sendChoiceRequest(sessionId, sessionLabel2, requestId, toolName, description, inputPreview, choices) {
553
+ let preview = inputPreview;
554
+ try {
555
+ const p = JSON.parse(inputPreview);
556
+ if (typeof p.command === "string") preview = `$ ${p.command}`;
557
+ else if (typeof p.content === "string") preview = p.content;
558
+ } catch {
559
+ }
560
+ const slice = preview.slice(0, 3e3);
561
+ const html = `\u26A0\uFE0F <b>Permission Request</b> \u2014 ${this.escHtml(sessionLabel2)}
562
+
563
+ \u{1F527} <b>${this.escHtml(toolName)}</b>` + (description ? `
564
+ ${this.escHtml(description)}` : "") + (slice ? `
565
+ <pre>${this.escHtml(slice)}</pre>` : "") + (preview.length > slice.length ? "\n<i>...truncated</i>" : "");
566
+ const entry = { html };
567
+ this.choiceMessages.set(permissionKey(sessionId, requestId), entry);
568
+ const rows = choices.map((c) => {
569
+ const token = randomUUID().replace(/-/g, "").slice(0, 16);
570
+ this.choiceTokens.set(token, { sessionId, requestId, choiceId: c.id, label: c.label });
571
+ return [{ text: c.label, callback_data: `pc:${token}` }];
572
+ });
573
+ this.trimChoiceMessages();
574
+ const sent = await this.sendMessage(html, void 0, { inline_keyboard: rows });
575
+ if (!sent) return;
576
+ entry.messageId = sent.message_id;
577
+ if (entry.outcome) await this.editMessageText(this.config.chatId, sent.message_id, html + entry.outcome);
578
+ }
579
+ async resolveChoiceRequest(sessionId, requestId, state) {
580
+ this.dropChoiceTokens(sessionId, requestId);
581
+ const key = permissionKey(sessionId, requestId);
582
+ const entry = this.choiceMessages.get(key);
583
+ if (!entry) return;
584
+ this.choiceMessages.delete(key);
585
+ entry.outcome = state === "resolved" ? "\n\n\u2705 <b>Resolved</b>" : "\n\n\u231B <b>Expired</b>";
586
+ if (entry.messageId !== void 0) await this.editMessageText(this.config.chatId, entry.messageId, entry.html + entry.outcome);
587
+ }
588
+ async handleChoiceCallback(query) {
589
+ const choice = this.choiceTokens.get(query.data.slice("pc:".length));
590
+ if (!choice) {
591
+ await this.answerCallbackQuery(query.id, "Expired");
592
+ return;
593
+ }
594
+ this.dropChoiceTokens(choice.sessionId, choice.requestId);
595
+ this.onChoiceVerdict?.(choice.sessionId, choice.requestId, choice.choiceId);
596
+ await this.answerCallbackQuery(query.id, `Sent: ${choice.label}`);
597
+ const entry = this.choiceMessages.get(permissionKey(choice.sessionId, choice.requestId));
598
+ if (entry?.messageId !== void 0 && !entry.outcome) {
599
+ await this.editMessageText(this.config.chatId, entry.messageId, `${entry.html}
600
+
601
+ \u23F3 <b>Sent: ${this.escHtml(choice.label)}</b>`);
602
+ }
603
+ }
604
+ dropChoiceTokens(sessionId, requestId) {
605
+ for (const [token, c] of this.choiceTokens) {
606
+ if (c.sessionId === sessionId && c.requestId === requestId) this.choiceTokens.delete(token);
607
+ }
608
+ }
609
+ trimChoiceMessages() {
610
+ while (this.choiceMessages.size > MAX_CHOICE_MESSAGES) {
611
+ const oldest = this.choiceMessages.keys().next().value;
612
+ this.choiceMessages.delete(oldest);
613
+ const [sessionId, requestId] = oldest.split("\n");
614
+ this.dropChoiceTokens(sessionId, requestId);
615
+ }
616
+ }
540
617
  async handleCallbackQuery(query) {
541
618
  if (!query.data) return;
542
619
  if (String(query.message?.chat.id) !== String(this.config.chatId)) return;
@@ -544,6 +621,10 @@ ${previewHtml}${truncNote}`;
544
621
  await this.handleSessionSelectCallback(query);
545
622
  return;
546
623
  }
624
+ if (query.data.startsWith("pc:")) {
625
+ await this.handleChoiceCallback(query);
626
+ return;
627
+ }
547
628
  if (!query.data.startsWith("perm:")) return;
548
629
  const parts = query.data.split(":");
549
630
  if (parts.length < 4) return;
@@ -923,6 +1004,11 @@ function isSecureRequest(req) {
923
1004
  // src/hub/server.ts
924
1005
  var __dirname = path5.dirname(fileURLToPath(import.meta.url));
925
1006
  var MAX_WS_PAYLOAD = 16 * 1024 * 1024;
1007
+ function validChoices(choices) {
1008
+ if (!Array.isArray(choices)) return void 0;
1009
+ const valid = choices.filter((c) => typeof c?.id === "string" && typeof c?.label === "string");
1010
+ return valid.length ? valid : void 0;
1011
+ }
926
1012
  var HubServer = class {
927
1013
  httpServer;
928
1014
  wssChannel;
@@ -941,6 +1027,8 @@ var HubServer = class {
941
1027
  channelAlive = /* @__PURE__ */ new Map();
942
1028
  // sessionId -> alive flag
943
1029
  socketOwners = /* @__PURE__ */ new WeakMap();
1030
+ // Codex approvals carry their own choices; a response is forwarded only in the mode of the request it answers.
1031
+ choiceRequests = /* @__PURE__ */ new Map();
944
1032
  host;
945
1033
  port;
946
1034
  token;
@@ -1078,6 +1166,7 @@ var HubServer = class {
1078
1166
  this.channelSockets.delete(sessionId);
1079
1167
  this.localChannels.delete(sessionId);
1080
1168
  this.channelAlive.delete(sessionId);
1169
+ this.expireChoices(sessionId);
1081
1170
  if (session) {
1082
1171
  this.broadcastToDashboards({ type: "session_disconnected", sessionId });
1083
1172
  this.jsonResponse(res, 200, { ok: true });
@@ -1221,6 +1310,7 @@ var HubServer = class {
1221
1310
  this.channelSockets.delete(sessionId);
1222
1311
  this.localChannels.delete(sessionId);
1223
1312
  this.channelAlive.delete(sessionId);
1313
+ this.expireChoices(sessionId);
1224
1314
  logger.info(`Channel disconnected: ${sessionId}`);
1225
1315
  this.broadcastToDashboards({
1226
1316
  type: "session_disconnected",
@@ -1308,7 +1398,22 @@ var HubServer = class {
1308
1398
  }
1309
1399
  case "permission_request": {
1310
1400
  this.sessions.updateActivity(msg.sessionId);
1401
+ const choices = validChoices(msg.choices);
1311
1402
  logger.info(`Permission request [${msg.requestId}] from ${msg.sessionId}: ${msg.toolName}`);
1403
+ if (choices) {
1404
+ this.choiceRequests.set(permissionKey(msg.sessionId, msg.requestId), {
1405
+ request: {
1406
+ sessionId: msg.sessionId,
1407
+ requestId: msg.requestId,
1408
+ toolName: msg.toolName,
1409
+ description: msg.description,
1410
+ inputPreview: msg.inputPreview,
1411
+ timestamp: msg.timestamp,
1412
+ choices
1413
+ },
1414
+ choiceIds: new Set(choices.map((c) => c.id))
1415
+ });
1416
+ }
1312
1417
  this.broadcastToDashboards({
1313
1418
  type: "permission_request",
1314
1419
  sessionId: msg.sessionId,
@@ -1316,15 +1421,23 @@ var HubServer = class {
1316
1421
  toolName: msg.toolName,
1317
1422
  description: msg.description,
1318
1423
  inputPreview: msg.inputPreview,
1319
- timestamp: msg.timestamp
1424
+ timestamp: msg.timestamp,
1425
+ ...choices ? { choices } : {}
1320
1426
  });
1321
1427
  if (this.telegramBot) {
1322
- const session = this.sessions.get(msg.sessionId);
1323
- const label = this.getSessionLabel(session);
1324
- this.telegramBot.sendPermissionRequest(msg.sessionId, label, msg.requestId, msg.toolName, msg.description, msg.inputPreview);
1428
+ const label = this.getSessionLabel(this.sessions.get(msg.sessionId));
1429
+ if (choices) {
1430
+ void this.telegramBot.sendChoiceRequest(msg.sessionId, label, msg.requestId, msg.toolName, msg.description, msg.inputPreview, choices);
1431
+ } else {
1432
+ this.telegramBot.sendPermissionRequest(msg.sessionId, label, msg.requestId, msg.toolName, msg.description, msg.inputPreview);
1433
+ }
1325
1434
  }
1326
1435
  break;
1327
1436
  }
1437
+ case "permission_resolved": {
1438
+ this.resolveChoice(msg.sessionId, msg.requestId, msg.state === "expired" ? "expired" : "resolved");
1439
+ break;
1440
+ }
1328
1441
  }
1329
1442
  }
1330
1443
  // --- Dashboard WebSocket ---
@@ -1340,6 +1453,11 @@ var HubServer = class {
1340
1453
  sessions: this.sessions.getAll()
1341
1454
  };
1342
1455
  ws.send(JSON.stringify(sessionsMsg));
1456
+ const pendingMsg = {
1457
+ type: "permission_pending",
1458
+ requests: [...this.choiceRequests.values()].map((c) => c.request)
1459
+ };
1460
+ ws.send(JSON.stringify(pendingMsg));
1343
1461
  ws.on("message", (data) => {
1344
1462
  try {
1345
1463
  const msg = JSON.parse(data.toString());
@@ -1351,10 +1469,9 @@ var HubServer = class {
1351
1469
  } else if (msg.type === "image_upload") {
1352
1470
  this.handleImageUpload(msg);
1353
1471
  } else if (msg.type === "permission_response") {
1354
- const channelWs = this.channelSockets.get(msg.sessionId);
1355
- if (channelWs?.readyState === WebSocket.OPEN) {
1356
- channelWs.send(JSON.stringify(msg));
1357
- logger.info(`Permission verdict [${msg.requestId}]: ${msg.behavior} -> session ${msg.sessionId}`);
1472
+ if (this.forwardPermissionResponse(msg)) {
1473
+ const verdict = msg.choiceId !== void 0 ? `choice ${msg.choiceId}` : msg.behavior;
1474
+ logger.info(`Permission verdict [${msg.requestId}]: ${verdict} -> session ${msg.sessionId}`);
1358
1475
  }
1359
1476
  }
1360
1477
  } catch {
@@ -1367,6 +1484,32 @@ var HubServer = class {
1367
1484
  });
1368
1485
  }
1369
1486
  // --- Helpers ---
1487
+ forwardPermissionResponse(msg) {
1488
+ const pending = this.choiceRequests.get(permissionKey(msg.sessionId, msg.requestId));
1489
+ let out;
1490
+ if (pending) {
1491
+ if (typeof msg.choiceId !== "string" || !pending.choiceIds.has(msg.choiceId)) return false;
1492
+ out = { type: "permission_response", sessionId: msg.sessionId, requestId: msg.requestId, choiceId: msg.choiceId };
1493
+ } else {
1494
+ if (msg.behavior !== "allow" && msg.behavior !== "deny" || msg.choiceId !== void 0) return false;
1495
+ if (this.sessions.get(msg.sessionId)?.agentKind === "codex") return false;
1496
+ out = { type: "permission_response", sessionId: msg.sessionId, requestId: msg.requestId, behavior: msg.behavior };
1497
+ }
1498
+ const channelWs = this.channelSockets.get(msg.sessionId);
1499
+ if (channelWs?.readyState !== WebSocket.OPEN) return false;
1500
+ channelWs.send(JSON.stringify(out));
1501
+ return true;
1502
+ }
1503
+ resolveChoice(sessionId, requestId, state) {
1504
+ if (!this.choiceRequests.delete(permissionKey(sessionId, requestId))) return;
1505
+ this.broadcastToDashboards({ type: "permission_resolved", sessionId, requestId, state });
1506
+ void this.telegramBot?.resolveChoiceRequest(sessionId, requestId, state);
1507
+ }
1508
+ expireChoices(sessionId) {
1509
+ for (const pending of [...this.choiceRequests.values()]) {
1510
+ if (pending.request.sessionId === sessionId) this.resolveChoice(sessionId, pending.request.requestId, "expired");
1511
+ }
1512
+ }
1370
1513
  broadcastToDashboards(msg) {
1371
1514
  const payload = JSON.stringify(msg);
1372
1515
  for (const ws of this.dashboardSockets) {
@@ -1450,14 +1593,16 @@ var HubServer = class {
1450
1593
  }
1451
1594
  };
1452
1595
  this.telegramBot.onPermissionVerdict = (sessionId, requestId, behavior) => {
1453
- const channelWs = this.channelSockets.get(sessionId);
1454
- if (channelWs?.readyState === WebSocket.OPEN) {
1455
- const msg = { type: "permission_response", sessionId, requestId, behavior };
1456
- channelWs.send(JSON.stringify(msg));
1596
+ if (this.forwardPermissionResponse({ sessionId, requestId, behavior })) {
1457
1597
  logger.info(`Telegram permission verdict [${requestId}]: ${behavior} -> session ${sessionId}`);
1458
1598
  }
1459
1599
  this.broadcastToDashboards({ type: "permission_response", sessionId, requestId, behavior });
1460
1600
  };
1601
+ this.telegramBot.onChoiceVerdict = (sessionId, requestId, choiceId) => {
1602
+ if (this.forwardPermissionResponse({ sessionId, requestId, choiceId })) {
1603
+ logger.info(`Telegram choice [${requestId}]: ${choiceId} -> session ${sessionId}`);
1604
+ }
1605
+ };
1461
1606
  this.notifier.configure({ telegramBot: this.telegramBot });
1462
1607
  this.telegramBot.startPolling();
1463
1608
  logger.info("Telegram bot initialized");