@delt/claude-alarm 0.9.1 → 1.0.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.
@@ -2,8 +2,8 @@
2
2
 
3
3
  // src/hub/server.ts
4
4
  import http from "http";
5
- import fs4 from "fs";
6
- import path4 from "path";
5
+ import fs5 from "fs";
6
+ import path5 from "path";
7
7
  import { fileURLToPath } from "url";
8
8
  import { WebSocketServer, WebSocket } from "ws";
9
9
 
@@ -38,6 +38,8 @@ var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
38
38
  var PID_FILE = path.join(CONFIG_DIR, "hub.pid");
39
39
  var LOG_FILE = path.join(CONFIG_DIR, "hub.log");
40
40
  var UPLOADS_DIR = path.join(CONFIG_DIR, "uploads");
41
+ var CODEX_PID_FILE = path.join(CONFIG_DIR, "codex.pid");
42
+ var CODEX_LOG_FILE = path.join(CONFIG_DIR, "codex.log");
41
43
  var WS_PATH_CHANNEL = "/ws/channel";
42
44
  var WS_PATH_DASHBOARD = "/ws/dashboard";
43
45
 
@@ -51,9 +53,9 @@ function isValidPeerName(name) {
51
53
  var SessionManager = class {
52
54
  sessions = /* @__PURE__ */ new Map();
53
55
  register(session) {
54
- const baseName = session.cwd?.replace(/^.*[/\\]/, "") || session.name;
56
+ const baseName = this.baseName(session);
55
57
  const existing = Array.from(this.sessions.values()).filter(
56
- (s) => s.id !== session.id && (s.cwd?.replace(/^.*[/\\]/, "") || s.name) === baseName
58
+ (s) => s.id !== session.id && this.baseName(s) === baseName
57
59
  );
58
60
  if (existing.length > 0) {
59
61
  const usedNums = existing.map((s) => {
@@ -107,6 +109,9 @@ var SessionManager = class {
107
109
  count() {
108
110
  return this.sessions.size;
109
111
  }
112
+ baseName(session) {
113
+ return session.title || session.cwd?.replace(/^.*[/\\]/, "") || session.name;
114
+ }
110
115
  };
111
116
 
112
117
  // src/hub/notifier.ts
@@ -127,7 +132,7 @@ var Notifier = class {
127
132
  async notify(title, message, level = "info") {
128
133
  await this.notifyWithSession(void 0, void 0, title, message, level);
129
134
  }
130
- async notifyWithSession(sessionId, sessionLabel, title, message, level = "info") {
135
+ async notifyWithSession(sessionId, sessionLabel2, title, message, level = "info") {
131
136
  const promises = [];
132
137
  if (this.desktopEnabled) {
133
138
  promises.push(this.sendDesktop(title, message, level));
@@ -135,8 +140,8 @@ var Notifier = class {
135
140
  for (const webhook of this.webhooks) {
136
141
  promises.push(this.sendWebhook(webhook, title, message, level));
137
142
  }
138
- if (this.telegramBot && sessionId && sessionLabel) {
139
- promises.push(this.telegramBot.sendNotification(sessionId, sessionLabel, title, message));
143
+ if (this.telegramBot && sessionId && sessionLabel2) {
144
+ promises.push(this.telegramBot.sendNotification(sessionId, sessionLabel2, title, message));
140
145
  }
141
146
  await Promise.allSettled(promises);
142
147
  }
@@ -222,6 +227,14 @@ var Notifier = class {
222
227
  import fs from "fs";
223
228
  import path2 from "path";
224
229
  import { randomUUID } from "crypto";
230
+
231
+ // src/shared/session-label.ts
232
+ function sessionLabel(session) {
233
+ const base = session.displayName || session.cwd?.replace(/^.*[/\\]/, "") || session.name;
234
+ return session.agentKind === "codex" ? `Codex \xB7 ${base}` : base;
235
+ }
236
+
237
+ // src/hub/telegram.ts
225
238
  var TELEGRAM_API = "https://api.telegram.org/bot";
226
239
  var TelegramBot = class _TelegramBot {
227
240
  config;
@@ -360,15 +373,14 @@ ${this.mdToHtml(message)}`;
360
373
  const pending = this.pendingMessages.get(msg.chat.id);
361
374
  if (pending) {
362
375
  this.pendingMessages.delete(msg.chat.id);
363
- const sessions2 = this.getSessions?.() ?? [];
364
- const idx = parseInt(selectMatch[1], 10) - 1;
365
- if (idx >= 0 && idx < sessions2.length) {
376
+ const session = this.pendingSession(pending, parseInt(selectMatch[1], 10) - 1);
377
+ if (session) {
366
378
  if (pending.photoFileId) {
367
- await this.deliverPhotoToSessionByFileId(sessions2[idx].id, pending.photoFileId, pending.caption);
379
+ await this.deliverPhotoToSessionByFileId(session.id, pending.photoFileId, pending.caption);
368
380
  } else if (pending.text) {
369
- this.deliverToSession(sessions2[idx].id, pending.text);
381
+ this.deliverToSession(session.id, pending.text);
370
382
  }
371
- this.sendMessage(`Sent to [${this.getLabel(sessions2[idx])}]`);
383
+ this.sendMessage(`Sent to [${this.getLabel(session)}]`);
372
384
  } else {
373
385
  this.sendMessage("Invalid session number.");
374
386
  }
@@ -389,11 +401,12 @@ ${this.mdToHtml(message)}`;
389
401
  }
390
402
  return;
391
403
  }
404
+ const sessionIds = sessions.map((s) => s.id);
392
405
  if (hasPhoto) {
393
406
  const largest = msg.photo[msg.photo.length - 1];
394
- this.pendingMessages.set(msg.chat.id, { photoFileId: largest.file_id, caption: text });
407
+ this.pendingMessages.set(msg.chat.id, { photoFileId: largest.file_id, caption: text, sessionIds });
395
408
  } else {
396
- this.pendingMessages.set(msg.chat.id, { text });
409
+ this.pendingMessages.set(msg.chat.id, { text, sessionIds });
397
410
  }
398
411
  const buttons = sessions.map((s, i) => ({
399
412
  text: this.getLabel(s),
@@ -450,11 +463,16 @@ ${this.mdToHtml(message)}`;
450
463
  logger.warn(`Telegram photo download failed: ${err.message}`);
451
464
  }
452
465
  }
466
+ // Buttons are numbered against the list shown when they were sent; Codex sessions come and go, so the live list may have shifted.
467
+ pendingSession(pending, idx) {
468
+ const id = pending.sessionIds[idx];
469
+ return id ? this.getSessions?.().find((s) => s.id === id) : void 0;
470
+ }
453
471
  getLabel(session) {
454
- return session.displayName || session.cwd?.replace(/^.*[/\\]/, "") || session.name;
472
+ return sessionLabel(session);
455
473
  }
456
474
  /** Send a permission request with inline buttons */
457
- async sendPermissionRequest(sessionId, sessionLabel, requestId, toolName, description, inputPreview) {
475
+ async sendPermissionRequest(sessionId, sessionLabel2, requestId, toolName, description, inputPreview) {
458
476
  const isNotify = toolName.endsWith("__notify") || toolName === "notify";
459
477
  let preview = inputPreview;
460
478
  let kind = "code";
@@ -507,7 +525,7 @@ ${this.mdToHtml(message)}`;
507
525
  const mcpMatch = toolName.match(/__([^_]+)$/);
508
526
  if (mcpMatch) displayTool = mcpMatch[1].charAt(0).toUpperCase() + mcpMatch[1].slice(1);
509
527
  }
510
- const text = `\u26A0\uFE0F <b>Permission Request</b> \u2014 ${this.escHtml(sessionLabel)}
528
+ const text = `\u26A0\uFE0F <b>Permission Request</b> \u2014 ${this.escHtml(sessionLabel2)}
511
529
 
512
530
  \u{1F527} <b>${this.escHtml(displayTool)}</b>
513
531
  ${previewHtml}${truncNote}`;
@@ -547,22 +565,18 @@ ${label}`);
547
565
  const parts = query.data.split(":");
548
566
  if (parts.length < 3) return;
549
567
  const [, idxStr, chatIdStr] = parts;
550
- const idx = parseInt(idxStr, 10);
551
568
  const chatId = parseInt(chatIdStr, 10);
552
- const sessions = this.getSessions?.() ?? [];
553
- if (idx < 0 || idx >= sessions.length) {
569
+ const pending = this.pendingMessages.get(chatId);
570
+ const session = pending ? this.pendingSession(pending, parseInt(idxStr, 10)) : void 0;
571
+ if (!pending || !session) {
554
572
  await this.answerCallbackQuery(query.id, "Session not found");
555
573
  return;
556
574
  }
557
- const session = sessions[idx];
558
- const pending = this.pendingMessages.get(chatId);
559
575
  this.pendingMessages.delete(chatId);
560
- if (pending) {
561
- if (pending.photoFileId) {
562
- await this.deliverPhotoToSessionByFileId(session.id, pending.photoFileId, pending.caption);
563
- } else if (pending.text) {
564
- this.deliverToSession(session.id, pending.text);
565
- }
576
+ if (pending.photoFileId) {
577
+ await this.deliverPhotoToSessionByFileId(session.id, pending.photoFileId, pending.caption);
578
+ } else if (pending.text) {
579
+ this.deliverToSession(session.id, pending.text);
566
580
  }
567
581
  await this.answerCallbackQuery(query.id, `Sent to ${this.getLabel(session)}`);
568
582
  if (query.message) {
@@ -624,8 +638,8 @@ ${label}`);
624
638
  css: "css",
625
639
  scss: "scss"
626
640
  };
627
- langOf(path5) {
628
- const m = /\.([a-z0-9]+)$/i.exec(path5 || "");
641
+ langOf(path6) {
642
+ const m = /\.([a-z0-9]+)$/i.exec(path6 || "");
629
643
  return m ? _TelegramBot.LANG_BY_EXT[m[1].toLowerCase()] ?? "" : "";
630
644
  }
631
645
  /** Convert markdown to Telegram HTML (escape first, then apply formatting) */
@@ -668,9 +682,73 @@ ${bodyLines.join("\n")}`;
668
682
  }
669
683
  };
670
684
 
671
- // src/shared/config.ts
685
+ // src/hub/codex-supervisor.ts
686
+ import { spawn } from "child_process";
672
687
  import fs2 from "fs";
673
688
  import path3 from "path";
689
+ var HEALTHY_RUN_MS = 6e4;
690
+ function resolveAdapterScript(baseDir) {
691
+ return [path3.join(baseDir, "..", "codex", "main.js"), path3.join(baseDir, "codex", "main.js")].find((p) => fs2.existsSync(p));
692
+ }
693
+ var CodexSupervisor = class {
694
+ constructor(script, spawnFn = spawn, minDelayMs = 2e3, maxDelayMs = 6e4, stopGraceMs = 3e3) {
695
+ this.script = script;
696
+ this.spawnFn = spawnFn;
697
+ this.minDelayMs = minDelayMs;
698
+ this.maxDelayMs = maxDelayMs;
699
+ this.stopGraceMs = stopGraceMs;
700
+ this.delay = minDelayMs;
701
+ }
702
+ child;
703
+ timer;
704
+ stopped = true;
705
+ delay;
706
+ start() {
707
+ this.stopped = false;
708
+ this.launch();
709
+ }
710
+ stop() {
711
+ this.stopped = true;
712
+ if (this.timer) clearTimeout(this.timer);
713
+ this.timer = void 0;
714
+ const child = this.child;
715
+ this.child = void 0;
716
+ if (!child) return;
717
+ child.stdin?.end();
718
+ const force = setTimeout(() => child.kill(), this.stopGraceMs);
719
+ force.unref();
720
+ child.once("exit", () => clearTimeout(force));
721
+ }
722
+ launch() {
723
+ const startedAt = Date.now();
724
+ const child = this.spawnFn(process.execPath, [this.script, "--watch-stdin"], {
725
+ detached: true,
726
+ stdio: ["pipe", "pipe", "pipe"],
727
+ windowsHide: true
728
+ });
729
+ this.child = child;
730
+ child.stdout?.on("data", (d) => process.stdout.write(d));
731
+ child.stderr?.on("data", (d) => process.stderr.write(d));
732
+ child.stdin?.on("error", () => {
733
+ });
734
+ child.on("error", (err) => logger.warn(`Codex adapter failed to start: ${err.message}`));
735
+ child.on("exit", (code, signal) => {
736
+ if (this.child === child) this.child = void 0;
737
+ if (this.stopped || code === 0) return;
738
+ if (Date.now() - startedAt > HEALTHY_RUN_MS) this.delay = this.minDelayMs;
739
+ logger.warn(`Codex adapter exited (${signal ?? code}); restarting in ${this.delay}ms`);
740
+ this.timer = setTimeout(() => {
741
+ this.timer = void 0;
742
+ if (!this.stopped) this.launch();
743
+ }, this.delay);
744
+ this.delay = Math.min(this.delay * 2, this.maxDelayMs);
745
+ });
746
+ }
747
+ };
748
+
749
+ // src/shared/config.ts
750
+ import fs3 from "fs";
751
+ import path4 from "path";
674
752
  import { randomUUID as randomUUID2 } from "crypto";
675
753
  var DEFAULT_CONFIG = {
676
754
  hub: {
@@ -684,18 +762,18 @@ var DEFAULT_CONFIG = {
684
762
  webhooks: []
685
763
  };
686
764
  function ensureConfigDir() {
687
- if (!fs2.existsSync(CONFIG_DIR)) {
688
- fs2.mkdirSync(CONFIG_DIR, { recursive: true });
765
+ if (!fs3.existsSync(CONFIG_DIR)) {
766
+ fs3.mkdirSync(CONFIG_DIR, { recursive: true });
689
767
  }
690
768
  }
691
769
  function loadConfig() {
692
770
  ensureConfigDir();
693
771
  let config;
694
- if (!fs2.existsSync(CONFIG_FILE)) {
772
+ if (!fs3.existsSync(CONFIG_FILE)) {
695
773
  config = { ...DEFAULT_CONFIG, hub: { ...DEFAULT_CONFIG.hub } };
696
774
  } else {
697
775
  try {
698
- const raw = fs2.readFileSync(CONFIG_FILE, "utf-8");
776
+ const raw = fs3.readFileSync(CONFIG_FILE, "utf-8");
699
777
  const parsed = JSON.parse(raw);
700
778
  config = { ...DEFAULT_CONFIG, ...parsed, hub: { ...DEFAULT_CONFIG.hub, ...parsed.hub }, ...parsed.telegram ? { telegram: parsed.telegram } : {} };
701
779
  } catch {
@@ -710,17 +788,17 @@ function loadConfig() {
710
788
  }
711
789
  function saveConfig(config) {
712
790
  ensureConfigDir();
713
- fs2.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { encoding: "utf-8", mode: 384 });
791
+ fs3.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { encoding: "utf-8", mode: 384 });
714
792
  }
715
793
 
716
794
  // src/shared/crash-guard.ts
717
- import fs3 from "fs";
795
+ import fs4 from "fs";
718
796
  var BURST_WINDOW_MS = 6e4;
719
797
  var BURST_LIMIT = 10;
720
798
  function appendToLog(text) {
721
799
  try {
722
- fs3.mkdirSync(CONFIG_DIR, { recursive: true });
723
- fs3.appendFileSync(LOG_FILE, text, "utf-8");
800
+ fs4.mkdirSync(CONFIG_DIR, { recursive: true });
801
+ fs4.appendFileSync(LOG_FILE, text, "utf-8");
724
802
  } catch {
725
803
  }
726
804
  }
@@ -819,7 +897,7 @@ function isSecureRequest(req) {
819
897
  }
820
898
 
821
899
  // src/hub/server.ts
822
- var __dirname = path4.dirname(fileURLToPath(import.meta.url));
900
+ var __dirname = path5.dirname(fileURLToPath(import.meta.url));
823
901
  var MAX_WS_PAYLOAD = 16 * 1024 * 1024;
824
902
  var HubServer = class {
825
903
  httpServer;
@@ -842,10 +920,13 @@ var HubServer = class {
842
920
  host;
843
921
  port;
844
922
  token;
923
+ codexEnabled;
924
+ codexSupervisor;
845
925
  constructor(config) {
846
926
  this.host = config?.hub?.host ?? DEFAULT_HUB_HOST;
847
927
  this.port = config?.hub?.port ?? DEFAULT_HUB_PORT;
848
928
  this.token = config?.hub?.token;
929
+ this.codexEnabled = config?.codex?.enabled === true;
849
930
  if (config?.notifications) {
850
931
  this.notifier.configure({
851
932
  desktop: config.notifications.desktop
@@ -903,12 +984,15 @@ var HubServer = class {
903
984
  this.httpServer.on("error", (err) => logger.error(`HTTP server error: ${err.message}`));
904
985
  const displayHost = this.host === "0.0.0.0" ? "127.0.0.1" : this.host;
905
986
  logger.info(`Hub server listening on http://${displayHost}:${this.port}`);
987
+ this.startCodexAdapter();
906
988
  resolve();
907
989
  });
908
990
  });
909
991
  }
910
992
  stop() {
911
993
  return new Promise((resolve) => {
994
+ this.codexSupervisor?.stop();
995
+ this.codexSupervisor = void 0;
912
996
  if (this.heartbeatInterval) clearInterval(this.heartbeatInterval);
913
997
  if (this.telegramBot) this.telegramBot.stopPolling();
914
998
  for (const ws of this.channelSockets.values()) ws.terminate();
@@ -1009,23 +1093,23 @@ var HubServer = class {
1009
1093
  return;
1010
1094
  }
1011
1095
  const candidates = [
1012
- path4.join(__dirname, "..", "dashboard", "index.html"),
1096
+ path5.join(__dirname, "..", "dashboard", "index.html"),
1013
1097
  // from dist/hub/
1014
- path4.join(__dirname, "dashboard", "index.html"),
1098
+ path5.join(__dirname, "dashboard", "index.html"),
1015
1099
  // from dist/ (bundled index.js)
1016
- path4.join(__dirname, "..", "..", "src", "dashboard", "index.html"),
1100
+ path5.join(__dirname, "..", "..", "src", "dashboard", "index.html"),
1017
1101
  // from dist/hub/ -> src/
1018
- path4.join(__dirname, "..", "src", "dashboard", "index.html"),
1102
+ path5.join(__dirname, "..", "src", "dashboard", "index.html"),
1019
1103
  // from dist/ -> src/
1020
- path4.join(process.cwd(), "dist", "dashboard", "index.html"),
1104
+ path5.join(process.cwd(), "dist", "dashboard", "index.html"),
1021
1105
  // from cwd
1022
- path4.join(process.cwd(), "src", "dashboard", "index.html")
1106
+ path5.join(process.cwd(), "src", "dashboard", "index.html")
1023
1107
  // from cwd/src
1024
1108
  ];
1025
1109
  logger.debug(`Dashboard candidates: ${JSON.stringify(candidates)}`);
1026
1110
  for (const candidate of candidates) {
1027
- if (fs4.existsSync(candidate)) {
1028
- const html = fs4.readFileSync(candidate, "utf-8");
1111
+ if (fs5.existsSync(candidate)) {
1112
+ const html = fs5.readFileSync(candidate, "utf-8");
1029
1113
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
1030
1114
  res.end(html);
1031
1115
  return;
@@ -1064,7 +1148,7 @@ var HubServer = class {
1064
1148
  this.jsonResponse(res, 404, { error: "Session not connected" });
1065
1149
  return;
1066
1150
  }
1067
- const msg = { type: "message_to_session", sessionId, content };
1151
+ const msg = { type: "message_to_session", sessionId, content, source: "api" };
1068
1152
  ws.send(JSON.stringify(msg));
1069
1153
  this.jsonResponse(res, 200, { ok: true });
1070
1154
  }
@@ -1238,7 +1322,7 @@ var HubServer = class {
1238
1322
  if (msg.type === "message_to_session") {
1239
1323
  const channelWs = this.channelSockets.get(msg.sessionId);
1240
1324
  if (channelWs?.readyState === WebSocket.OPEN) {
1241
- channelWs.send(JSON.stringify(msg));
1325
+ channelWs.send(JSON.stringify({ ...msg, source: "dashboard" }));
1242
1326
  }
1243
1327
  } else if (msg.type === "image_upload") {
1244
1328
  this.handleImageUpload(msg);
@@ -1283,11 +1367,11 @@ var HubServer = class {
1283
1367
  logger.warn("Image upload rejected: exceeds 10MB");
1284
1368
  return;
1285
1369
  }
1286
- fs4.mkdirSync(UPLOADS_DIR, { recursive: true });
1370
+ fs5.mkdirSync(UPLOADS_DIR, { recursive: true });
1287
1371
  const ext = mimeType.split("/")[1] === "jpeg" ? "jpg" : mimeType.split("/")[1];
1288
1372
  const filename = `${randomUUID3()}.${ext}`;
1289
- const filePath = path4.join(UPLOADS_DIR, filename);
1290
- fs4.writeFileSync(filePath, buffer);
1373
+ const filePath = path5.join(UPLOADS_DIR, filename);
1374
+ fs5.writeFileSync(filePath, buffer);
1291
1375
  const forwardMsg = {
1292
1376
  type: "image_to_session",
1293
1377
  sessionId,
@@ -1300,7 +1384,7 @@ var HubServer = class {
1300
1384
  logger.info(`Image saved and forwarded: ${filename} (${buffer.length} bytes)`);
1301
1385
  setTimeout(() => {
1302
1386
  try {
1303
- fs4.unlinkSync(filePath);
1387
+ fs5.unlinkSync(filePath);
1304
1388
  } catch {
1305
1389
  }
1306
1390
  }, 5 * 60 * 1e3);
@@ -1328,7 +1412,7 @@ var HubServer = class {
1328
1412
  this.telegramBot.onMessageToSession = (sessionId, content) => {
1329
1413
  const channelWs = this.channelSockets.get(sessionId);
1330
1414
  if (channelWs?.readyState === WebSocket.OPEN) {
1331
- const msg = { type: "message_to_session", sessionId, content };
1415
+ const msg = { type: "message_to_session", sessionId, content, source: "telegram" };
1332
1416
  channelWs.send(JSON.stringify(msg));
1333
1417
  logger.info(`Telegram message forwarded to session: ${sessionId}`);
1334
1418
  }
@@ -1455,6 +1539,17 @@ var HubServer = class {
1455
1539
  this.jsonResponse(res, 500, { error: err.message });
1456
1540
  }
1457
1541
  }
1542
+ startCodexAdapter() {
1543
+ if (!this.codexEnabled) return;
1544
+ const script = resolveAdapterScript(__dirname);
1545
+ if (!script) {
1546
+ logger.warn("Codex adapter is enabled but codex/main.js was not found next to the hub");
1547
+ return;
1548
+ }
1549
+ this.codexSupervisor = new CodexSupervisor(script);
1550
+ this.codexSupervisor.start();
1551
+ logger.info("Codex adapter started");
1552
+ }
1458
1553
  startHeartbeat() {
1459
1554
  this.heartbeatInterval = setInterval(() => {
1460
1555
  for (const [sessionId, ws] of this.channelSockets) {
@@ -1470,11 +1565,11 @@ var HubServer = class {
1470
1565
  }
1471
1566
  cleanupUploads() {
1472
1567
  try {
1473
- if (!fs4.existsSync(UPLOADS_DIR)) return;
1474
- const files = fs4.readdirSync(UPLOADS_DIR);
1568
+ if (!fs5.existsSync(UPLOADS_DIR)) return;
1569
+ const files = fs5.readdirSync(UPLOADS_DIR);
1475
1570
  for (const file of files) {
1476
1571
  try {
1477
- fs4.unlinkSync(path4.join(UPLOADS_DIR, file));
1572
+ fs5.unlinkSync(path5.join(UPLOADS_DIR, file));
1478
1573
  } catch {
1479
1574
  }
1480
1575
  }
@@ -1483,8 +1578,7 @@ var HubServer = class {
1483
1578
  }
1484
1579
  }
1485
1580
  getSessionLabel(session) {
1486
- if (!session) return "unknown";
1487
- return session.displayName || session.cwd?.replace(/^.*[/\\]/, "") || session.name;
1581
+ return session ? sessionLabel(session) : "unknown";
1488
1582
  }
1489
1583
  jsonResponse(res, status, body) {
1490
1584
  res.writeHead(status, { "Content-Type": "application/json" });