@delt/claude-alarm 1.0.0 → 1.1.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/dist/cli.js CHANGED
@@ -75,6 +75,9 @@ function setCodexEnabled(enabled) {
75
75
  saveConfig(config2);
76
76
  return config2;
77
77
  }
78
+ function shouldOfferCodex(config2, installed) {
79
+ return installed && config2.codex === void 0;
80
+ }
78
81
  function setupMcpConfig(targetDir) {
79
82
  const dir = targetDir ?? process.cwd();
80
83
  const mcpPath = path2.join(dir, ".mcp.json");
@@ -143,11 +146,11 @@ var init_logger = __esm({
143
146
  });
144
147
 
145
148
  // src/shared/crash-guard.ts
146
- import fs2 from "fs";
149
+ import fs3 from "fs";
147
150
  function appendToLog(text) {
148
151
  try {
149
- fs2.mkdirSync(CONFIG_DIR, { recursive: true });
150
- fs2.appendFileSync(LOG_FILE, text, "utf-8");
152
+ fs3.mkdirSync(CONFIG_DIR, { recursive: true });
153
+ fs3.appendFileSync(LOG_FILE, text, "utf-8");
151
154
  } catch {
152
155
  }
153
156
  }
@@ -409,18 +412,31 @@ var init_session_label = __esm({
409
412
  }
410
413
  });
411
414
 
415
+ // src/shared/permission-key.ts
416
+ function permissionKey(sessionId2, requestId) {
417
+ return `${sessionId2}
418
+ ${requestId}`;
419
+ }
420
+ var init_permission_key = __esm({
421
+ "src/shared/permission-key.ts"() {
422
+ "use strict";
423
+ }
424
+ });
425
+
412
426
  // src/hub/telegram.ts
413
- import fs3 from "fs";
414
- import path3 from "path";
427
+ import fs4 from "fs";
428
+ import path4 from "path";
415
429
  import { randomUUID as randomUUID2 } from "crypto";
416
- var TELEGRAM_API, TelegramBot;
430
+ var TELEGRAM_API, MAX_CHOICE_MESSAGES, TelegramBot;
417
431
  var init_telegram = __esm({
418
432
  "src/hub/telegram.ts"() {
419
433
  "use strict";
420
434
  init_logger();
421
435
  init_constants();
422
436
  init_session_label();
437
+ init_permission_key();
423
438
  TELEGRAM_API = "https://api.telegram.org/bot";
439
+ MAX_CHOICE_MESSAGES = 200;
424
440
  TelegramBot = class _TelegramBot {
425
441
  config;
426
442
  offset = 0;
@@ -434,6 +450,11 @@ var init_telegram = __esm({
434
450
  onImageToSession;
435
451
  // Callback: when a permission verdict arrives from Telegram
436
452
  onPermissionVerdict;
453
+ // Callback: when a Codex approval choice arrives from Telegram
454
+ onChoiceVerdict;
455
+ // Telegram caps callback_data at 64 bytes, so buttons carry a short token instead of the ids.
456
+ choiceTokens = /* @__PURE__ */ new Map();
457
+ choiceMessages = /* @__PURE__ */ new Map();
437
458
  // Callback: get current sessions list
438
459
  getSessions;
439
460
  // Pending messages for session selection
@@ -630,17 +651,17 @@ ${this.mdToHtml(message)}`;
630
651
  const buffer = Buffer.from(await imgRes.arrayBuffer());
631
652
  const ext = fileData.result.file_path.split(".").pop() || "jpg";
632
653
  const mimeType = ext === "png" ? "image/png" : ext === "gif" ? "image/gif" : ext === "webp" ? "image/webp" : "image/jpeg";
633
- fs3.mkdirSync(UPLOADS_DIR, { recursive: true });
654
+ fs4.mkdirSync(UPLOADS_DIR, { recursive: true });
634
655
  const filename = `${randomUUID2()}.${ext}`;
635
- const filePath = path3.join(UPLOADS_DIR, filename);
636
- fs3.writeFileSync(filePath, buffer);
656
+ const filePath = path4.join(UPLOADS_DIR, filename);
657
+ fs4.writeFileSync(filePath, buffer);
637
658
  logger.info(`Telegram photo saved: ${filename} (${buffer.length} bytes)`);
638
659
  if (this.onImageToSession) {
639
660
  this.onImageToSession(sessionId2, filePath, mimeType, caption);
640
661
  }
641
662
  setTimeout(() => {
642
663
  try {
643
- fs3.unlinkSync(filePath);
664
+ fs4.unlinkSync(filePath);
644
665
  } catch {
645
666
  }
646
667
  }, 5 * 60 * 1e3);
@@ -722,6 +743,71 @@ ${previewHtml}${truncNote}`;
722
743
  };
723
744
  await this.sendMessage(text, void 0, replyMarkup);
724
745
  }
746
+ async sendChoiceRequest(sessionId2, sessionLabel2, requestId, toolName, description, inputPreview, choices) {
747
+ let preview = inputPreview;
748
+ try {
749
+ const p = JSON.parse(inputPreview);
750
+ if (typeof p.command === "string") preview = `$ ${p.command}`;
751
+ else if (typeof p.content === "string") preview = p.content;
752
+ } catch {
753
+ }
754
+ const slice = preview.slice(0, 3e3);
755
+ const html = `\u26A0\uFE0F <b>Permission Request</b> \u2014 ${this.escHtml(sessionLabel2)}
756
+
757
+ \u{1F527} <b>${this.escHtml(toolName)}</b>` + (description ? `
758
+ ${this.escHtml(description)}` : "") + (slice ? `
759
+ <pre>${this.escHtml(slice)}</pre>` : "") + (preview.length > slice.length ? "\n<i>...truncated</i>" : "");
760
+ const entry = { html };
761
+ this.choiceMessages.set(permissionKey(sessionId2, requestId), entry);
762
+ const rows = choices.map((c) => {
763
+ const token = randomUUID2().replace(/-/g, "").slice(0, 16);
764
+ this.choiceTokens.set(token, { sessionId: sessionId2, requestId, choiceId: c.id, label: c.label });
765
+ return [{ text: c.label, callback_data: `pc:${token}` }];
766
+ });
767
+ this.trimChoiceMessages();
768
+ const sent = await this.sendMessage(html, void 0, { inline_keyboard: rows });
769
+ if (!sent) return;
770
+ entry.messageId = sent.message_id;
771
+ if (entry.outcome) await this.editMessageText(this.config.chatId, sent.message_id, html + entry.outcome);
772
+ }
773
+ async resolveChoiceRequest(sessionId2, requestId, state) {
774
+ this.dropChoiceTokens(sessionId2, requestId);
775
+ const key = permissionKey(sessionId2, requestId);
776
+ const entry = this.choiceMessages.get(key);
777
+ if (!entry) return;
778
+ this.choiceMessages.delete(key);
779
+ entry.outcome = state === "resolved" ? "\n\n\u2705 <b>Resolved</b>" : "\n\n\u231B <b>Expired</b>";
780
+ if (entry.messageId !== void 0) await this.editMessageText(this.config.chatId, entry.messageId, entry.html + entry.outcome);
781
+ }
782
+ async handleChoiceCallback(query) {
783
+ const choice = this.choiceTokens.get(query.data.slice("pc:".length));
784
+ if (!choice) {
785
+ await this.answerCallbackQuery(query.id, "Expired");
786
+ return;
787
+ }
788
+ this.dropChoiceTokens(choice.sessionId, choice.requestId);
789
+ this.onChoiceVerdict?.(choice.sessionId, choice.requestId, choice.choiceId);
790
+ await this.answerCallbackQuery(query.id, `Sent: ${choice.label}`);
791
+ const entry = this.choiceMessages.get(permissionKey(choice.sessionId, choice.requestId));
792
+ if (entry?.messageId !== void 0 && !entry.outcome) {
793
+ await this.editMessageText(this.config.chatId, entry.messageId, `${entry.html}
794
+
795
+ \u23F3 <b>Sent: ${this.escHtml(choice.label)}</b>`);
796
+ }
797
+ }
798
+ dropChoiceTokens(sessionId2, requestId) {
799
+ for (const [token, c] of this.choiceTokens) {
800
+ if (c.sessionId === sessionId2 && c.requestId === requestId) this.choiceTokens.delete(token);
801
+ }
802
+ }
803
+ trimChoiceMessages() {
804
+ while (this.choiceMessages.size > MAX_CHOICE_MESSAGES) {
805
+ const oldest = this.choiceMessages.keys().next().value;
806
+ this.choiceMessages.delete(oldest);
807
+ const [sessionId2, requestId] = oldest.split("\n");
808
+ this.dropChoiceTokens(sessionId2, requestId);
809
+ }
810
+ }
725
811
  async handleCallbackQuery(query) {
726
812
  if (!query.data) return;
727
813
  if (String(query.message?.chat.id) !== String(this.config.chatId)) return;
@@ -729,6 +815,10 @@ ${previewHtml}${truncNote}`;
729
815
  await this.handleSessionSelectCallback(query);
730
816
  return;
731
817
  }
818
+ if (query.data.startsWith("pc:")) {
819
+ await this.handleChoiceCallback(query);
820
+ return;
821
+ }
732
822
  if (!query.data.startsWith("perm:")) return;
733
823
  const parts = query.data.split(":");
734
824
  if (parts.length < 4) return;
@@ -823,8 +913,8 @@ ${label}`);
823
913
  css: "css",
824
914
  scss: "scss"
825
915
  };
826
- langOf(path9) {
827
- const m = /\.([a-z0-9]+)$/i.exec(path9 || "");
916
+ langOf(path10) {
917
+ const m = /\.([a-z0-9]+)$/i.exec(path10 || "");
828
918
  return m ? _TelegramBot.LANG_BY_EXT[m[1].toLowerCase()] ?? "" : "";
829
919
  }
830
920
  /** Convert markdown to Telegram HTML (escape first, then apply formatting) */
@@ -870,11 +960,11 @@ ${bodyLines.join("\n")}`;
870
960
  });
871
961
 
872
962
  // src/hub/codex-supervisor.ts
873
- import { spawn } from "child_process";
874
- import fs4 from "fs";
875
- import path4 from "path";
963
+ import { spawn as spawn2 } from "child_process";
964
+ import fs5 from "fs";
965
+ import path5 from "path";
876
966
  function resolveAdapterScript(baseDir) {
877
- return [path4.join(baseDir, "..", "codex", "main.js"), path4.join(baseDir, "codex", "main.js")].find((p) => fs4.existsSync(p));
967
+ return [path5.join(baseDir, "..", "codex", "main.js"), path5.join(baseDir, "codex", "main.js")].find((p) => fs5.existsSync(p));
878
968
  }
879
969
  var HEALTHY_RUN_MS, CodexSupervisor;
880
970
  var init_codex_supervisor = __esm({
@@ -883,7 +973,7 @@ var init_codex_supervisor = __esm({
883
973
  init_logger();
884
974
  HEALTHY_RUN_MS = 6e4;
885
975
  CodexSupervisor = class {
886
- constructor(script, spawnFn = spawn, minDelayMs = 2e3, maxDelayMs = 6e4, stopGraceMs = 3e3) {
976
+ constructor(script, spawnFn = spawn2, minDelayMs = 2e3, maxDelayMs = 6e4, stopGraceMs = 3e3) {
887
977
  this.script = script;
888
978
  this.spawnFn = spawnFn;
889
979
  this.minDelayMs = minDelayMs;
@@ -1009,11 +1099,16 @@ __export(server_exports, {
1009
1099
  HubServer: () => HubServer
1010
1100
  });
1011
1101
  import http from "http";
1012
- import fs5 from "fs";
1013
- import path5 from "path";
1102
+ import fs6 from "fs";
1103
+ import path6 from "path";
1014
1104
  import { fileURLToPath } from "url";
1015
- import { WebSocketServer, WebSocket } from "ws";
1105
+ import { WebSocketServer, WebSocket as WebSocket2 } from "ws";
1016
1106
  import { randomUUID as randomUUID3 } from "crypto";
1107
+ function validChoices(choices) {
1108
+ if (!Array.isArray(choices)) return void 0;
1109
+ const valid = choices.filter((c) => typeof c?.id === "string" && typeof c?.label === "string");
1110
+ return valid.length ? valid : void 0;
1111
+ }
1017
1112
  var __dirname, MAX_WS_PAYLOAD, HubServer;
1018
1113
  var init_server = __esm({
1019
1114
  "src/hub/server.ts"() {
@@ -1027,8 +1122,9 @@ var init_server = __esm({
1027
1122
  init_config();
1028
1123
  init_session_label();
1029
1124
  init_crash_guard();
1125
+ init_permission_key();
1030
1126
  init_auth();
1031
- __dirname = path5.dirname(fileURLToPath(import.meta.url));
1127
+ __dirname = path6.dirname(fileURLToPath(import.meta.url));
1032
1128
  MAX_WS_PAYLOAD = 16 * 1024 * 1024;
1033
1129
  HubServer = class {
1034
1130
  httpServer;
@@ -1048,6 +1144,8 @@ var init_server = __esm({
1048
1144
  channelAlive = /* @__PURE__ */ new Map();
1049
1145
  // sessionId -> alive flag
1050
1146
  socketOwners = /* @__PURE__ */ new WeakMap();
1147
+ // Codex approvals carry their own choices; a response is forwarded only in the mode of the request it answers.
1148
+ choiceRequests = /* @__PURE__ */ new Map();
1051
1149
  host;
1052
1150
  port;
1053
1151
  token;
@@ -1185,6 +1283,7 @@ var init_server = __esm({
1185
1283
  this.channelSockets.delete(sessionId2);
1186
1284
  this.localChannels.delete(sessionId2);
1187
1285
  this.channelAlive.delete(sessionId2);
1286
+ this.expireChoices(sessionId2);
1188
1287
  if (session) {
1189
1288
  this.broadcastToDashboards({ type: "session_disconnected", sessionId: sessionId2 });
1190
1289
  this.jsonResponse(res, 200, { ok: true });
@@ -1224,23 +1323,23 @@ var init_server = __esm({
1224
1323
  return;
1225
1324
  }
1226
1325
  const candidates = [
1227
- path5.join(__dirname, "..", "dashboard", "index.html"),
1326
+ path6.join(__dirname, "..", "dashboard", "index.html"),
1228
1327
  // from dist/hub/
1229
- path5.join(__dirname, "dashboard", "index.html"),
1328
+ path6.join(__dirname, "dashboard", "index.html"),
1230
1329
  // from dist/ (bundled index.js)
1231
- path5.join(__dirname, "..", "..", "src", "dashboard", "index.html"),
1330
+ path6.join(__dirname, "..", "..", "src", "dashboard", "index.html"),
1232
1331
  // from dist/hub/ -> src/
1233
- path5.join(__dirname, "..", "src", "dashboard", "index.html"),
1332
+ path6.join(__dirname, "..", "src", "dashboard", "index.html"),
1234
1333
  // from dist/ -> src/
1235
- path5.join(process.cwd(), "dist", "dashboard", "index.html"),
1334
+ path6.join(process.cwd(), "dist", "dashboard", "index.html"),
1236
1335
  // from cwd
1237
- path5.join(process.cwd(), "src", "dashboard", "index.html")
1336
+ path6.join(process.cwd(), "src", "dashboard", "index.html")
1238
1337
  // from cwd/src
1239
1338
  ];
1240
1339
  logger.debug(`Dashboard candidates: ${JSON.stringify(candidates)}`);
1241
1340
  for (const candidate of candidates) {
1242
- if (fs5.existsSync(candidate)) {
1243
- const html = fs5.readFileSync(candidate, "utf-8");
1341
+ if (fs6.existsSync(candidate)) {
1342
+ const html = fs6.readFileSync(candidate, "utf-8");
1244
1343
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
1245
1344
  res.end(html);
1246
1345
  return;
@@ -1275,7 +1374,7 @@ var init_server = __esm({
1275
1374
  return;
1276
1375
  }
1277
1376
  const ws = this.channelSockets.get(sessionId2);
1278
- if (!ws || ws.readyState !== WebSocket.OPEN) {
1377
+ if (!ws || ws.readyState !== WebSocket2.OPEN) {
1279
1378
  this.jsonResponse(res, 404, { error: "Session not connected" });
1280
1379
  return;
1281
1380
  }
@@ -1328,6 +1427,7 @@ var init_server = __esm({
1328
1427
  this.channelSockets.delete(sessionId2);
1329
1428
  this.localChannels.delete(sessionId2);
1330
1429
  this.channelAlive.delete(sessionId2);
1430
+ this.expireChoices(sessionId2);
1331
1431
  logger.info(`Channel disconnected: ${sessionId2}`);
1332
1432
  this.broadcastToDashboards({
1333
1433
  type: "session_disconnected",
@@ -1415,7 +1515,22 @@ var init_server = __esm({
1415
1515
  }
1416
1516
  case "permission_request": {
1417
1517
  this.sessions.updateActivity(msg.sessionId);
1518
+ const choices = validChoices(msg.choices);
1418
1519
  logger.info(`Permission request [${msg.requestId}] from ${msg.sessionId}: ${msg.toolName}`);
1520
+ if (choices) {
1521
+ this.choiceRequests.set(permissionKey(msg.sessionId, msg.requestId), {
1522
+ request: {
1523
+ sessionId: msg.sessionId,
1524
+ requestId: msg.requestId,
1525
+ toolName: msg.toolName,
1526
+ description: msg.description,
1527
+ inputPreview: msg.inputPreview,
1528
+ timestamp: msg.timestamp,
1529
+ choices
1530
+ },
1531
+ choiceIds: new Set(choices.map((c) => c.id))
1532
+ });
1533
+ }
1419
1534
  this.broadcastToDashboards({
1420
1535
  type: "permission_request",
1421
1536
  sessionId: msg.sessionId,
@@ -1423,15 +1538,23 @@ var init_server = __esm({
1423
1538
  toolName: msg.toolName,
1424
1539
  description: msg.description,
1425
1540
  inputPreview: msg.inputPreview,
1426
- timestamp: msg.timestamp
1541
+ timestamp: msg.timestamp,
1542
+ ...choices ? { choices } : {}
1427
1543
  });
1428
1544
  if (this.telegramBot) {
1429
- const session = this.sessions.get(msg.sessionId);
1430
- const label = this.getSessionLabel(session);
1431
- this.telegramBot.sendPermissionRequest(msg.sessionId, label, msg.requestId, msg.toolName, msg.description, msg.inputPreview);
1545
+ const label = this.getSessionLabel(this.sessions.get(msg.sessionId));
1546
+ if (choices) {
1547
+ void this.telegramBot.sendChoiceRequest(msg.sessionId, label, msg.requestId, msg.toolName, msg.description, msg.inputPreview, choices);
1548
+ } else {
1549
+ this.telegramBot.sendPermissionRequest(msg.sessionId, label, msg.requestId, msg.toolName, msg.description, msg.inputPreview);
1550
+ }
1432
1551
  }
1433
1552
  break;
1434
1553
  }
1554
+ case "permission_resolved": {
1555
+ this.resolveChoice(msg.sessionId, msg.requestId, msg.state === "expired" ? "expired" : "resolved");
1556
+ break;
1557
+ }
1435
1558
  }
1436
1559
  }
1437
1560
  // --- Dashboard WebSocket ---
@@ -1447,21 +1570,25 @@ var init_server = __esm({
1447
1570
  sessions: this.sessions.getAll()
1448
1571
  };
1449
1572
  ws.send(JSON.stringify(sessionsMsg));
1573
+ const pendingMsg = {
1574
+ type: "permission_pending",
1575
+ requests: [...this.choiceRequests.values()].map((c) => c.request)
1576
+ };
1577
+ ws.send(JSON.stringify(pendingMsg));
1450
1578
  ws.on("message", (data) => {
1451
1579
  try {
1452
1580
  const msg = JSON.parse(data.toString());
1453
1581
  if (msg.type === "message_to_session") {
1454
1582
  const channelWs = this.channelSockets.get(msg.sessionId);
1455
- if (channelWs?.readyState === WebSocket.OPEN) {
1583
+ if (channelWs?.readyState === WebSocket2.OPEN) {
1456
1584
  channelWs.send(JSON.stringify({ ...msg, source: "dashboard" }));
1457
1585
  }
1458
1586
  } else if (msg.type === "image_upload") {
1459
1587
  this.handleImageUpload(msg);
1460
1588
  } else if (msg.type === "permission_response") {
1461
- const channelWs = this.channelSockets.get(msg.sessionId);
1462
- if (channelWs?.readyState === WebSocket.OPEN) {
1463
- channelWs.send(JSON.stringify(msg));
1464
- logger.info(`Permission verdict [${msg.requestId}]: ${msg.behavior} -> session ${msg.sessionId}`);
1589
+ if (this.forwardPermissionResponse(msg)) {
1590
+ const verdict = msg.choiceId !== void 0 ? `choice ${msg.choiceId}` : msg.behavior;
1591
+ logger.info(`Permission verdict [${msg.requestId}]: ${verdict} -> session ${msg.sessionId}`);
1465
1592
  }
1466
1593
  }
1467
1594
  } catch {
@@ -1474,10 +1601,36 @@ var init_server = __esm({
1474
1601
  });
1475
1602
  }
1476
1603
  // --- Helpers ---
1604
+ forwardPermissionResponse(msg) {
1605
+ const pending = this.choiceRequests.get(permissionKey(msg.sessionId, msg.requestId));
1606
+ let out;
1607
+ if (pending) {
1608
+ if (typeof msg.choiceId !== "string" || !pending.choiceIds.has(msg.choiceId)) return false;
1609
+ out = { type: "permission_response", sessionId: msg.sessionId, requestId: msg.requestId, choiceId: msg.choiceId };
1610
+ } else {
1611
+ if (msg.behavior !== "allow" && msg.behavior !== "deny" || msg.choiceId !== void 0) return false;
1612
+ if (this.sessions.get(msg.sessionId)?.agentKind === "codex") return false;
1613
+ out = { type: "permission_response", sessionId: msg.sessionId, requestId: msg.requestId, behavior: msg.behavior };
1614
+ }
1615
+ const channelWs = this.channelSockets.get(msg.sessionId);
1616
+ if (channelWs?.readyState !== WebSocket2.OPEN) return false;
1617
+ channelWs.send(JSON.stringify(out));
1618
+ return true;
1619
+ }
1620
+ resolveChoice(sessionId2, requestId, state) {
1621
+ if (!this.choiceRequests.delete(permissionKey(sessionId2, requestId))) return;
1622
+ this.broadcastToDashboards({ type: "permission_resolved", sessionId: sessionId2, requestId, state });
1623
+ void this.telegramBot?.resolveChoiceRequest(sessionId2, requestId, state);
1624
+ }
1625
+ expireChoices(sessionId2) {
1626
+ for (const pending of [...this.choiceRequests.values()]) {
1627
+ if (pending.request.sessionId === sessionId2) this.resolveChoice(sessionId2, pending.request.requestId, "expired");
1628
+ }
1629
+ }
1477
1630
  broadcastToDashboards(msg) {
1478
1631
  const payload = JSON.stringify(msg);
1479
1632
  for (const ws of this.dashboardSockets) {
1480
- if (ws.readyState === WebSocket.OPEN) {
1633
+ if (ws.readyState === WebSocket2.OPEN) {
1481
1634
  ws.send(payload);
1482
1635
  }
1483
1636
  }
@@ -1489,7 +1642,7 @@ var init_server = __esm({
1489
1642
  return;
1490
1643
  }
1491
1644
  const channelWs = this.channelSockets.get(sessionId2);
1492
- if (!channelWs || channelWs.readyState !== WebSocket.OPEN) return;
1645
+ if (!channelWs || channelWs.readyState !== WebSocket2.OPEN) return;
1493
1646
  const allowedTypes = ["image/png", "image/jpeg", "image/gif", "image/webp"];
1494
1647
  if (!allowedTypes.includes(mimeType)) return;
1495
1648
  const base64Data = imageData.replace(/^data:image\/\w+;base64,/, "");
@@ -1498,11 +1651,11 @@ var init_server = __esm({
1498
1651
  logger.warn("Image upload rejected: exceeds 10MB");
1499
1652
  return;
1500
1653
  }
1501
- fs5.mkdirSync(UPLOADS_DIR, { recursive: true });
1654
+ fs6.mkdirSync(UPLOADS_DIR, { recursive: true });
1502
1655
  const ext = mimeType.split("/")[1] === "jpeg" ? "jpg" : mimeType.split("/")[1];
1503
1656
  const filename = `${randomUUID3()}.${ext}`;
1504
- const filePath = path5.join(UPLOADS_DIR, filename);
1505
- fs5.writeFileSync(filePath, buffer);
1657
+ const filePath = path6.join(UPLOADS_DIR, filename);
1658
+ fs6.writeFileSync(filePath, buffer);
1506
1659
  const forwardMsg = {
1507
1660
  type: "image_to_session",
1508
1661
  sessionId: sessionId2,
@@ -1515,7 +1668,7 @@ var init_server = __esm({
1515
1668
  logger.info(`Image saved and forwarded: ${filename} (${buffer.length} bytes)`);
1516
1669
  setTimeout(() => {
1517
1670
  try {
1518
- fs5.unlinkSync(filePath);
1671
+ fs6.unlinkSync(filePath);
1519
1672
  } catch {
1520
1673
  }
1521
1674
  }, 5 * 60 * 1e3);
@@ -1542,7 +1695,7 @@ var init_server = __esm({
1542
1695
  this.telegramBot.getSessions = () => this.sessions.getAll();
1543
1696
  this.telegramBot.onMessageToSession = (sessionId2, content) => {
1544
1697
  const channelWs = this.channelSockets.get(sessionId2);
1545
- if (channelWs?.readyState === WebSocket.OPEN) {
1698
+ if (channelWs?.readyState === WebSocket2.OPEN) {
1546
1699
  const msg = { type: "message_to_session", sessionId: sessionId2, content, source: "telegram" };
1547
1700
  channelWs.send(JSON.stringify(msg));
1548
1701
  logger.info(`Telegram message forwarded to session: ${sessionId2}`);
@@ -1550,21 +1703,23 @@ var init_server = __esm({
1550
1703
  };
1551
1704
  this.telegramBot.onImageToSession = (sessionId2, imagePath, mimeType, caption) => {
1552
1705
  const channelWs = this.channelSockets.get(sessionId2);
1553
- if (channelWs?.readyState === WebSocket.OPEN) {
1706
+ if (channelWs?.readyState === WebSocket2.OPEN) {
1554
1707
  const msg = { type: "image_to_session", sessionId: sessionId2, imagePath, mimeType, content: caption };
1555
1708
  channelWs.send(JSON.stringify(msg));
1556
1709
  logger.info(`Telegram photo forwarded to session: ${sessionId2}`);
1557
1710
  }
1558
1711
  };
1559
1712
  this.telegramBot.onPermissionVerdict = (sessionId2, requestId, behavior) => {
1560
- const channelWs = this.channelSockets.get(sessionId2);
1561
- if (channelWs?.readyState === WebSocket.OPEN) {
1562
- const msg = { type: "permission_response", sessionId: sessionId2, requestId, behavior };
1563
- channelWs.send(JSON.stringify(msg));
1713
+ if (this.forwardPermissionResponse({ sessionId: sessionId2, requestId, behavior })) {
1564
1714
  logger.info(`Telegram permission verdict [${requestId}]: ${behavior} -> session ${sessionId2}`);
1565
1715
  }
1566
1716
  this.broadcastToDashboards({ type: "permission_response", sessionId: sessionId2, requestId, behavior });
1567
1717
  };
1718
+ this.telegramBot.onChoiceVerdict = (sessionId2, requestId, choiceId) => {
1719
+ if (this.forwardPermissionResponse({ sessionId: sessionId2, requestId, choiceId })) {
1720
+ logger.info(`Telegram choice [${requestId}]: ${choiceId} -> session ${sessionId2}`);
1721
+ }
1722
+ };
1568
1723
  this.notifier.configure({ telegramBot: this.telegramBot });
1569
1724
  this.telegramBot.startPolling();
1570
1725
  logger.info("Telegram bot initialized");
@@ -1696,11 +1851,11 @@ var init_server = __esm({
1696
1851
  }
1697
1852
  cleanupUploads() {
1698
1853
  try {
1699
- if (!fs5.existsSync(UPLOADS_DIR)) return;
1700
- const files = fs5.readdirSync(UPLOADS_DIR);
1854
+ if (!fs6.existsSync(UPLOADS_DIR)) return;
1855
+ const files = fs6.readdirSync(UPLOADS_DIR);
1701
1856
  for (const file of files) {
1702
1857
  try {
1703
- fs5.unlinkSync(path5.join(UPLOADS_DIR, file));
1858
+ fs6.unlinkSync(path6.join(UPLOADS_DIR, file));
1704
1859
  } catch {
1705
1860
  }
1706
1861
  }
@@ -1773,7 +1928,7 @@ Content-Length: 0\r
1773
1928
  });
1774
1929
 
1775
1930
  // src/channel/hub-client.ts
1776
- import WebSocket2 from "ws";
1931
+ import WebSocket3 from "ws";
1777
1932
  var HubClient;
1778
1933
  var init_hub_client = __esm({
1779
1934
  "src/channel/hub-client.ts"() {
@@ -1802,7 +1957,7 @@ var init_hub_client = __esm({
1802
1957
  const url = `ws://${this.hubHost}:${this.hubPort}${WS_PATH_CHANNEL}${tokenQuery}`;
1803
1958
  logger.debug(`Connecting to hub at ${url}`);
1804
1959
  try {
1805
- const ws = new WebSocket2(url);
1960
+ const ws = new WebSocket3(url);
1806
1961
  this.ws = ws;
1807
1962
  ws.on("open", () => {
1808
1963
  if (this.ws !== ws) return;
@@ -1842,7 +1997,7 @@ var init_hub_client = __esm({
1842
1997
  }
1843
1998
  }
1844
1999
  send(msg) {
1845
- if (this.connected && this.ws?.readyState === WebSocket2.OPEN) {
2000
+ if (this.connected && this.ws?.readyState === WebSocket3.OPEN) {
1846
2001
  this.ws.send(JSON.stringify(msg));
1847
2002
  } else {
1848
2003
  if (this.queue.length < 100) {
@@ -1867,7 +2022,7 @@ var init_hub_client = __esm({
1867
2022
  this.connected = false;
1868
2023
  }
1869
2024
  reregister() {
1870
- if (this.connected && this.ws?.readyState === WebSocket2.OPEN) {
2025
+ if (this.connected && this.ws?.readyState === WebSocket3.OPEN) {
1871
2026
  this.ws.send(JSON.stringify(this.registration()));
1872
2027
  }
1873
2028
  }
@@ -1899,9 +2054,9 @@ var init_hub_client = __esm({
1899
2054
  });
1900
2055
 
1901
2056
  // src/channel/peer-name.ts
1902
- import fs6 from "fs";
2057
+ import fs7 from "fs";
1903
2058
  import os2 from "os";
1904
- import path6 from "path";
2059
+ import path7 from "path";
1905
2060
  function findPeerName(records, env) {
1906
2061
  const valid = records.filter((r) => typeof r === "object" && r !== null);
1907
2062
  const nameOf = (match) => {
@@ -1921,18 +2076,18 @@ function readPeerName(env = process.env) {
1921
2076
  sessionId: env.CLAUDE_CODE_SESSION_ID
1922
2077
  };
1923
2078
  if (!lookup.messagingSocket && !lookup.sessionId) return void 0;
1924
- const configDir = env.CLAUDE_CONFIG_DIR || path6.join(os2.homedir(), ".claude");
1925
- const dir = path6.join(configDir, "sessions");
2079
+ const configDir = env.CLAUDE_CONFIG_DIR || path7.join(os2.homedir(), ".claude");
2080
+ const dir = path7.join(configDir, "sessions");
1926
2081
  let files;
1927
2082
  try {
1928
- files = fs6.readdirSync(dir).filter((f) => f.endsWith(".json"));
2083
+ files = fs7.readdirSync(dir).filter((f) => f.endsWith(".json"));
1929
2084
  } catch {
1930
2085
  return void 0;
1931
2086
  }
1932
2087
  const records = [];
1933
2088
  for (const f of files) {
1934
2089
  try {
1935
- records.push(JSON.parse(fs6.readFileSync(path6.join(dir, f), "utf8")));
2090
+ records.push(JSON.parse(fs7.readFileSync(path7.join(dir, f), "utf8")));
1936
2091
  } catch {
1937
2092
  }
1938
2093
  }
@@ -1955,7 +2110,7 @@ import {
1955
2110
  } from "@modelcontextprotocol/sdk/types.js";
1956
2111
  import { z } from "zod";
1957
2112
  import { randomUUID as randomUUID4 } from "crypto";
1958
- import path7 from "path";
2113
+ import path8 from "path";
1959
2114
  async function main() {
1960
2115
  logger.info(`Starting MCP channel server (session: ${sessionId})`);
1961
2116
  hubClient.connect();
@@ -2024,7 +2179,7 @@ var init_server2 = __esm({
2024
2179
  init_hub_client();
2025
2180
  init_peer_name2();
2026
2181
  sessionId = randomUUID4();
2027
- sessionName = process.env.CLAUDE_ALARM_SESSION_NAME ?? path7.basename(process.cwd());
2182
+ sessionName = process.env.CLAUDE_ALARM_SESSION_NAME ?? path8.basename(process.cwd());
2028
2183
  peerName = readPeerName();
2029
2184
  server = new Server(
2030
2185
  {
@@ -2200,15 +2355,35 @@ NOTIFICATIONS:
2200
2355
 
2201
2356
  // src/cli.ts
2202
2357
  init_config();
2358
+ import { spawn as spawn3 } from "child_process";
2359
+ import fs8 from "fs";
2360
+ import path9 from "path";
2361
+ import readline from "readline";
2362
+ import { fileURLToPath as fileURLToPath2 } from "url";
2363
+
2364
+ // src/codex/transport.ts
2365
+ init_logger();
2366
+ import { spawn } from "child_process";
2367
+ import fs2 from "fs";
2368
+ import path3 from "path";
2369
+ import { Duplex } from "stream";
2370
+ import WebSocket from "ws";
2371
+ function findOnPath(command, platform = process.platform, env = process.env) {
2372
+ const dirs = (env.PATH ?? env.Path ?? "").split(path3.delimiter).filter(Boolean);
2373
+ for (const ext of platform === "win32" ? [".exe", ".cmd"] : [""]) {
2374
+ for (const dir of dirs) {
2375
+ const file = path3.join(dir, command + ext);
2376
+ if (fs2.existsSync(file)) return file;
2377
+ }
2378
+ }
2379
+ return void 0;
2380
+ }
2381
+
2382
+ // src/cli.ts
2203
2383
  init_constants();
2204
2384
  init_logger();
2205
2385
  init_crash_guard();
2206
- import { spawn as spawn2 } from "child_process";
2207
- import fs7 from "fs";
2208
- import path8 from "path";
2209
- import readline from "readline";
2210
- import { fileURLToPath as fileURLToPath2 } from "url";
2211
- var __dirname2 = path8.dirname(fileURLToPath2(import.meta.url));
2386
+ var __dirname2 = path9.dirname(fileURLToPath2(import.meta.url));
2212
2387
  function authHeaders(token) {
2213
2388
  return token ? { Authorization: `Bearer ${token}` } : {};
2214
2389
  }
@@ -2240,8 +2415,8 @@ Quick start:
2240
2415
  }
2241
2416
  async function checkForUpdates() {
2242
2417
  try {
2243
- const pkgPath = path8.join(__dirname2, "..", "package.json");
2244
- const pkg = JSON.parse(fs7.readFileSync(pkgPath, "utf-8"));
2418
+ const pkgPath = path9.join(__dirname2, "..", "package.json");
2419
+ const pkg = JSON.parse(fs8.readFileSync(pkgPath, "utf-8"));
2245
2420
  const currentVersion = pkg.version;
2246
2421
  const res = await fetch("https://registry.npmjs.org/@delt/claude-alarm/latest", {
2247
2422
  signal: AbortSignal.timeout(3e3)
@@ -2264,25 +2439,25 @@ async function hubStart(daemon) {
2264
2439
  const port = config2.hub.port ?? DEFAULT_HUB_PORT;
2265
2440
  const displayHost = host === "0.0.0.0" ? "127.0.0.1" : host;
2266
2441
  checkForUpdates();
2267
- if (fs7.existsSync(PID_FILE)) {
2268
- const pid = parseInt(fs7.readFileSync(PID_FILE, "utf-8").trim(), 10);
2442
+ if (fs8.existsSync(PID_FILE)) {
2443
+ const pid = parseInt(fs8.readFileSync(PID_FILE, "utf-8").trim(), 10);
2269
2444
  if (isProcessRunning(pid)) {
2270
2445
  console.log(`Hub is already running (PID: ${pid}). Dashboard: ${loginLink(displayHost, port, config2.hub.token)}`);
2271
2446
  return;
2272
2447
  }
2273
- fs7.unlinkSync(PID_FILE);
2448
+ fs8.unlinkSync(PID_FILE);
2274
2449
  }
2275
2450
  if (daemon) {
2276
2451
  ensureConfigDir();
2277
- const logFd = fs7.openSync(LOG_FILE, "a");
2278
- const hubScript = path8.join(__dirname2, "hub", "server.js");
2279
- const child = spawn2(process.execPath, [hubScript], {
2452
+ const logFd = fs8.openSync(LOG_FILE, "a");
2453
+ const hubScript = path9.join(__dirname2, "hub", "server.js");
2454
+ const child = spawn3(process.execPath, [hubScript], {
2280
2455
  detached: true,
2281
2456
  stdio: ["ignore", logFd, logFd],
2282
2457
  env: { ...process.env }
2283
2458
  });
2284
2459
  if (child.pid) {
2285
- fs7.writeFileSync(PID_FILE, String(child.pid), "utf-8");
2460
+ fs8.writeFileSync(PID_FILE, String(child.pid), "utf-8");
2286
2461
  child.unref();
2287
2462
  console.log(`Hub started as daemon (PID: ${child.pid})`);
2288
2463
  console.log(`Dashboard: ${loginLink(displayHost, port, config2.hub.token)}`);
@@ -2303,11 +2478,11 @@ async function hubStart(daemon) {
2303
2478
  const hub = new HubServer2(config2);
2304
2479
  await hub.start();
2305
2480
  ensureConfigDir();
2306
- fs7.writeFileSync(PID_FILE, String(process.pid), "utf-8");
2481
+ fs8.writeFileSync(PID_FILE, String(process.pid), "utf-8");
2307
2482
  const shutdown = async () => {
2308
2483
  console.log("\nShutting down...");
2309
2484
  await hub.stop();
2310
- if (fs7.existsSync(PID_FILE)) fs7.unlinkSync(PID_FILE);
2485
+ if (fs8.existsSync(PID_FILE)) fs8.unlinkSync(PID_FILE);
2311
2486
  process.exit(0);
2312
2487
  };
2313
2488
  process.on("SIGINT", shutdown);
@@ -2315,18 +2490,18 @@ async function hubStart(daemon) {
2315
2490
  }
2316
2491
  }
2317
2492
  function hubStop() {
2318
- if (!fs7.existsSync(PID_FILE)) {
2493
+ if (!fs8.existsSync(PID_FILE)) {
2319
2494
  console.log("Hub is not running (no PID file found)");
2320
2495
  return;
2321
2496
  }
2322
- const pid = parseInt(fs7.readFileSync(PID_FILE, "utf-8").trim(), 10);
2497
+ const pid = parseInt(fs8.readFileSync(PID_FILE, "utf-8").trim(), 10);
2323
2498
  try {
2324
2499
  process.kill(pid, "SIGTERM");
2325
2500
  console.log(`Hub stopped (PID: ${pid})`);
2326
2501
  } catch {
2327
2502
  console.log("Hub process not found (may have already stopped)");
2328
2503
  }
2329
- fs7.unlinkSync(PID_FILE);
2504
+ fs8.unlinkSync(PID_FILE);
2330
2505
  }
2331
2506
  async function hubStatus() {
2332
2507
  const config2 = loadConfig();
@@ -2334,8 +2509,8 @@ async function hubStatus() {
2334
2509
  const port = config2.hub.port ?? DEFAULT_HUB_PORT;
2335
2510
  const displayHost = host === "0.0.0.0" ? "127.0.0.1" : host;
2336
2511
  let pidInfo = "not running";
2337
- if (fs7.existsSync(PID_FILE)) {
2338
- const pid = parseInt(fs7.readFileSync(PID_FILE, "utf-8").trim(), 10);
2512
+ if (fs8.existsSync(PID_FILE)) {
2513
+ const pid = parseInt(fs8.readFileSync(PID_FILE, "utf-8").trim(), 10);
2339
2514
  if (isProcessRunning(pid)) {
2340
2515
  pidInfo = `running (PID: ${pid})`;
2341
2516
  } else {
@@ -2407,7 +2582,7 @@ function ask(question) {
2407
2582
  }
2408
2583
  async function init() {
2409
2584
  const dir = process.cwd();
2410
- const projectName = path8.basename(dir);
2585
+ const projectName = path9.basename(dir);
2411
2586
  console.log(`
2412
2587
  claude-alarm init for "${projectName}"
2413
2588
  `);
@@ -2427,11 +2602,11 @@ claude-alarm init for "${projectName}"
2427
2602
  if (port) env.CLAUDE_ALARM_HUB_PORT = port;
2428
2603
  if (token) env.CLAUDE_ALARM_HUB_TOKEN = token;
2429
2604
  }
2430
- const mcpPath = path8.join(dir, ".mcp.json");
2605
+ const mcpPath = path9.join(dir, ".mcp.json");
2431
2606
  let mcpConfig = {};
2432
- if (fs7.existsSync(mcpPath)) {
2607
+ if (fs8.existsSync(mcpPath)) {
2433
2608
  try {
2434
- mcpConfig = JSON.parse(fs7.readFileSync(mcpPath, "utf-8"));
2609
+ mcpConfig = JSON.parse(fs8.readFileSync(mcpPath, "utf-8"));
2435
2610
  } catch {
2436
2611
  mcpConfig = {};
2437
2612
  }
@@ -2442,7 +2617,7 @@ claude-alarm init for "${projectName}"
2442
2617
  args: ["-y", "@delt/claude-alarm", "serve"],
2443
2618
  env
2444
2619
  };
2445
- fs7.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2), "utf-8");
2620
+ fs8.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2), "utf-8");
2446
2621
  console.log(`
2447
2622
  \u2713 Created ${mcpPath}`);
2448
2623
  if (remote.toLowerCase() !== "y") {
@@ -2463,6 +2638,11 @@ claude-alarm init for "${projectName}"
2463
2638
  console.log(` claude-alarm hub start`);
2464
2639
  }
2465
2640
  console.log(` Dashboard: http://${displayHost}:${port}`);
2641
+ if (shouldOfferCodex(config2, findOnPath("codex") !== void 0)) {
2642
+ const answer = await ask("\nCodex is installed. Show Codex conversations on the dashboard too? (y/N): ");
2643
+ if (answer.toLowerCase() === "y") codexEnable(true);
2644
+ else setCodexEnabled(false);
2645
+ }
2466
2646
  }
2467
2647
  console.log(`
2468
2648
  Next step:`);
@@ -2488,8 +2668,8 @@ function isProcessRunning(pid) {
2488
2668
  }
2489
2669
  }
2490
2670
  function readCodexPid() {
2491
- if (!fs7.existsSync(CODEX_PID_FILE)) return void 0;
2492
- const pid = parseInt(fs7.readFileSync(CODEX_PID_FILE, "utf-8").trim(), 10);
2671
+ if (!fs8.existsSync(CODEX_PID_FILE)) return void 0;
2672
+ const pid = parseInt(fs8.readFileSync(CODEX_PID_FILE, "utf-8").trim(), 10);
2493
2673
  return Number.isNaN(pid) ? void 0 : pid;
2494
2674
  }
2495
2675
  function codexEnable(enabled) {
@@ -2503,8 +2683,8 @@ function codexStart() {
2503
2683
  return;
2504
2684
  }
2505
2685
  ensureConfigDir();
2506
- const logFd = fs7.openSync(CODEX_LOG_FILE, "a");
2507
- const child = spawn2(process.execPath, [path8.join(__dirname2, "codex", "main.js")], {
2686
+ const logFd = fs8.openSync(CODEX_LOG_FILE, "a");
2687
+ const child = spawn3(process.execPath, [path9.join(__dirname2, "codex", "main.js")], {
2508
2688
  detached: true,
2509
2689
  stdio: ["ignore", logFd, logFd],
2510
2690
  env: { ...process.env }
@@ -2525,7 +2705,7 @@ function codexStop() {
2525
2705
  console.log("Codex adapter process not found (may have already stopped)");
2526
2706
  }
2527
2707
  try {
2528
- fs7.unlinkSync(CODEX_PID_FILE);
2708
+ fs8.unlinkSync(CODEX_PID_FILE);
2529
2709
  } catch {
2530
2710
  }
2531
2711
  }