@delt/claude-alarm 0.9.1 → 1.0.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
@@ -12,7 +12,7 @@ var __export = (target, all) => {
12
12
  // src/shared/constants.ts
13
13
  import path from "path";
14
14
  import os from "os";
15
- var DEFAULT_HUB_HOST, DEFAULT_HUB_PORT, CONFIG_DIR, CONFIG_FILE, PID_FILE, LOG_FILE, UPLOADS_DIR, WS_PATH_CHANNEL, WS_PATH_DASHBOARD, CHANNEL_SERVER_NAME, CHANNEL_SERVER_VERSION;
15
+ var DEFAULT_HUB_HOST, DEFAULT_HUB_PORT, CONFIG_DIR, CONFIG_FILE, PID_FILE, LOG_FILE, UPLOADS_DIR, CODEX_PID_FILE, CODEX_LOG_FILE, WS_PATH_CHANNEL, WS_PATH_DASHBOARD, CHANNEL_SERVER_NAME, CHANNEL_SERVER_VERSION;
16
16
  var init_constants = __esm({
17
17
  "src/shared/constants.ts"() {
18
18
  "use strict";
@@ -23,6 +23,8 @@ var init_constants = __esm({
23
23
  PID_FILE = path.join(CONFIG_DIR, "hub.pid");
24
24
  LOG_FILE = path.join(CONFIG_DIR, "hub.log");
25
25
  UPLOADS_DIR = path.join(CONFIG_DIR, "uploads");
26
+ CODEX_PID_FILE = path.join(CONFIG_DIR, "codex.pid");
27
+ CODEX_LOG_FILE = path.join(CONFIG_DIR, "codex.log");
26
28
  WS_PATH_CHANNEL = "/ws/channel";
27
29
  WS_PATH_DASHBOARD = "/ws/dashboard";
28
30
  CHANNEL_SERVER_NAME = "claude-alarm";
@@ -67,6 +69,12 @@ function saveConfig(config2) {
67
69
  ensureConfigDir();
68
70
  fs.writeFileSync(CONFIG_FILE, JSON.stringify(config2, null, 2), { encoding: "utf-8", mode: 384 });
69
71
  }
72
+ function setCodexEnabled(enabled) {
73
+ const config2 = loadConfig();
74
+ config2.codex = { ...config2.codex, enabled };
75
+ saveConfig(config2);
76
+ return config2;
77
+ }
70
78
  function setupMcpConfig(targetDir) {
71
79
  const dir = targetDir ?? process.cwd();
72
80
  const mcpPath = path2.join(dir, ".mcp.json");
@@ -211,9 +219,9 @@ var init_session_manager = __esm({
211
219
  SessionManager = class {
212
220
  sessions = /* @__PURE__ */ new Map();
213
221
  register(session) {
214
- const baseName = session.cwd?.replace(/^.*[/\\]/, "") || session.name;
222
+ const baseName = this.baseName(session);
215
223
  const existing = Array.from(this.sessions.values()).filter(
216
- (s) => s.id !== session.id && (s.cwd?.replace(/^.*[/\\]/, "") || s.name) === baseName
224
+ (s) => s.id !== session.id && this.baseName(s) === baseName
217
225
  );
218
226
  if (existing.length > 0) {
219
227
  const usedNums = existing.map((s) => {
@@ -267,6 +275,9 @@ var init_session_manager = __esm({
267
275
  count() {
268
276
  return this.sessions.size;
269
277
  }
278
+ baseName(session) {
279
+ return session.title || session.cwd?.replace(/^.*[/\\]/, "") || session.name;
280
+ }
270
281
  };
271
282
  }
272
283
  });
@@ -294,7 +305,7 @@ var init_notifier = __esm({
294
305
  async notify(title, message, level = "info") {
295
306
  await this.notifyWithSession(void 0, void 0, title, message, level);
296
307
  }
297
- async notifyWithSession(sessionId2, sessionLabel, title, message, level = "info") {
308
+ async notifyWithSession(sessionId2, sessionLabel2, title, message, level = "info") {
298
309
  const promises = [];
299
310
  if (this.desktopEnabled) {
300
311
  promises.push(this.sendDesktop(title, message, level));
@@ -302,8 +313,8 @@ var init_notifier = __esm({
302
313
  for (const webhook of this.webhooks) {
303
314
  promises.push(this.sendWebhook(webhook, title, message, level));
304
315
  }
305
- if (this.telegramBot && sessionId2 && sessionLabel) {
306
- promises.push(this.telegramBot.sendNotification(sessionId2, sessionLabel, title, message));
316
+ if (this.telegramBot && sessionId2 && sessionLabel2) {
317
+ promises.push(this.telegramBot.sendNotification(sessionId2, sessionLabel2, title, message));
307
318
  }
308
319
  await Promise.allSettled(promises);
309
320
  }
@@ -387,6 +398,17 @@ var init_notifier = __esm({
387
398
  }
388
399
  });
389
400
 
401
+ // src/shared/session-label.ts
402
+ function sessionLabel(session) {
403
+ const base = session.displayName || session.cwd?.replace(/^.*[/\\]/, "") || session.name;
404
+ return session.agentKind === "codex" ? `Codex \xB7 ${base}` : base;
405
+ }
406
+ var init_session_label = __esm({
407
+ "src/shared/session-label.ts"() {
408
+ "use strict";
409
+ }
410
+ });
411
+
390
412
  // src/hub/telegram.ts
391
413
  import fs3 from "fs";
392
414
  import path3 from "path";
@@ -397,6 +419,7 @@ var init_telegram = __esm({
397
419
  "use strict";
398
420
  init_logger();
399
421
  init_constants();
422
+ init_session_label();
400
423
  TELEGRAM_API = "https://api.telegram.org/bot";
401
424
  TelegramBot = class _TelegramBot {
402
425
  config;
@@ -535,15 +558,14 @@ ${this.mdToHtml(message)}`;
535
558
  const pending = this.pendingMessages.get(msg.chat.id);
536
559
  if (pending) {
537
560
  this.pendingMessages.delete(msg.chat.id);
538
- const sessions2 = this.getSessions?.() ?? [];
539
- const idx = parseInt(selectMatch[1], 10) - 1;
540
- if (idx >= 0 && idx < sessions2.length) {
561
+ const session = this.pendingSession(pending, parseInt(selectMatch[1], 10) - 1);
562
+ if (session) {
541
563
  if (pending.photoFileId) {
542
- await this.deliverPhotoToSessionByFileId(sessions2[idx].id, pending.photoFileId, pending.caption);
564
+ await this.deliverPhotoToSessionByFileId(session.id, pending.photoFileId, pending.caption);
543
565
  } else if (pending.text) {
544
- this.deliverToSession(sessions2[idx].id, pending.text);
566
+ this.deliverToSession(session.id, pending.text);
545
567
  }
546
- this.sendMessage(`Sent to [${this.getLabel(sessions2[idx])}]`);
568
+ this.sendMessage(`Sent to [${this.getLabel(session)}]`);
547
569
  } else {
548
570
  this.sendMessage("Invalid session number.");
549
571
  }
@@ -564,11 +586,12 @@ ${this.mdToHtml(message)}`;
564
586
  }
565
587
  return;
566
588
  }
589
+ const sessionIds = sessions.map((s) => s.id);
567
590
  if (hasPhoto) {
568
591
  const largest = msg.photo[msg.photo.length - 1];
569
- this.pendingMessages.set(msg.chat.id, { photoFileId: largest.file_id, caption: text });
592
+ this.pendingMessages.set(msg.chat.id, { photoFileId: largest.file_id, caption: text, sessionIds });
570
593
  } else {
571
- this.pendingMessages.set(msg.chat.id, { text });
594
+ this.pendingMessages.set(msg.chat.id, { text, sessionIds });
572
595
  }
573
596
  const buttons = sessions.map((s, i) => ({
574
597
  text: this.getLabel(s),
@@ -625,11 +648,16 @@ ${this.mdToHtml(message)}`;
625
648
  logger.warn(`Telegram photo download failed: ${err.message}`);
626
649
  }
627
650
  }
651
+ // Buttons are numbered against the list shown when they were sent; Codex sessions come and go, so the live list may have shifted.
652
+ pendingSession(pending, idx) {
653
+ const id = pending.sessionIds[idx];
654
+ return id ? this.getSessions?.().find((s) => s.id === id) : void 0;
655
+ }
628
656
  getLabel(session) {
629
- return session.displayName || session.cwd?.replace(/^.*[/\\]/, "") || session.name;
657
+ return sessionLabel(session);
630
658
  }
631
659
  /** Send a permission request with inline buttons */
632
- async sendPermissionRequest(sessionId2, sessionLabel, requestId, toolName, description, inputPreview) {
660
+ async sendPermissionRequest(sessionId2, sessionLabel2, requestId, toolName, description, inputPreview) {
633
661
  const isNotify = toolName.endsWith("__notify") || toolName === "notify";
634
662
  let preview = inputPreview;
635
663
  let kind = "code";
@@ -682,7 +710,7 @@ ${this.mdToHtml(message)}`;
682
710
  const mcpMatch = toolName.match(/__([^_]+)$/);
683
711
  if (mcpMatch) displayTool = mcpMatch[1].charAt(0).toUpperCase() + mcpMatch[1].slice(1);
684
712
  }
685
- const text = `\u26A0\uFE0F <b>Permission Request</b> \u2014 ${this.escHtml(sessionLabel)}
713
+ const text = `\u26A0\uFE0F <b>Permission Request</b> \u2014 ${this.escHtml(sessionLabel2)}
686
714
 
687
715
  \u{1F527} <b>${this.escHtml(displayTool)}</b>
688
716
  ${previewHtml}${truncNote}`;
@@ -722,22 +750,18 @@ ${label}`);
722
750
  const parts = query.data.split(":");
723
751
  if (parts.length < 3) return;
724
752
  const [, idxStr, chatIdStr] = parts;
725
- const idx = parseInt(idxStr, 10);
726
753
  const chatId = parseInt(chatIdStr, 10);
727
- const sessions = this.getSessions?.() ?? [];
728
- if (idx < 0 || idx >= sessions.length) {
754
+ const pending = this.pendingMessages.get(chatId);
755
+ const session = pending ? this.pendingSession(pending, parseInt(idxStr, 10)) : void 0;
756
+ if (!pending || !session) {
729
757
  await this.answerCallbackQuery(query.id, "Session not found");
730
758
  return;
731
759
  }
732
- const session = sessions[idx];
733
- const pending = this.pendingMessages.get(chatId);
734
760
  this.pendingMessages.delete(chatId);
735
- if (pending) {
736
- if (pending.photoFileId) {
737
- await this.deliverPhotoToSessionByFileId(session.id, pending.photoFileId, pending.caption);
738
- } else if (pending.text) {
739
- this.deliverToSession(session.id, pending.text);
740
- }
761
+ if (pending.photoFileId) {
762
+ await this.deliverPhotoToSessionByFileId(session.id, pending.photoFileId, pending.caption);
763
+ } else if (pending.text) {
764
+ this.deliverToSession(session.id, pending.text);
741
765
  }
742
766
  await this.answerCallbackQuery(query.id, `Sent to ${this.getLabel(session)}`);
743
767
  if (query.message) {
@@ -799,8 +823,8 @@ ${label}`);
799
823
  css: "css",
800
824
  scss: "scss"
801
825
  };
802
- langOf(path8) {
803
- const m = /\.([a-z0-9]+)$/i.exec(path8 || "");
826
+ langOf(path9) {
827
+ const m = /\.([a-z0-9]+)$/i.exec(path9 || "");
804
828
  return m ? _TelegramBot.LANG_BY_EXT[m[1].toLowerCase()] ?? "" : "";
805
829
  }
806
830
  /** Convert markdown to Telegram HTML (escape first, then apply formatting) */
@@ -845,6 +869,77 @@ ${bodyLines.join("\n")}`;
845
869
  }
846
870
  });
847
871
 
872
+ // src/hub/codex-supervisor.ts
873
+ import { spawn } from "child_process";
874
+ import fs4 from "fs";
875
+ import path4 from "path";
876
+ function resolveAdapterScript(baseDir) {
877
+ return [path4.join(baseDir, "..", "codex", "main.js"), path4.join(baseDir, "codex", "main.js")].find((p) => fs4.existsSync(p));
878
+ }
879
+ var HEALTHY_RUN_MS, CodexSupervisor;
880
+ var init_codex_supervisor = __esm({
881
+ "src/hub/codex-supervisor.ts"() {
882
+ "use strict";
883
+ init_logger();
884
+ HEALTHY_RUN_MS = 6e4;
885
+ CodexSupervisor = class {
886
+ constructor(script, spawnFn = spawn, minDelayMs = 2e3, maxDelayMs = 6e4, stopGraceMs = 3e3) {
887
+ this.script = script;
888
+ this.spawnFn = spawnFn;
889
+ this.minDelayMs = minDelayMs;
890
+ this.maxDelayMs = maxDelayMs;
891
+ this.stopGraceMs = stopGraceMs;
892
+ this.delay = minDelayMs;
893
+ }
894
+ child;
895
+ timer;
896
+ stopped = true;
897
+ delay;
898
+ start() {
899
+ this.stopped = false;
900
+ this.launch();
901
+ }
902
+ stop() {
903
+ this.stopped = true;
904
+ if (this.timer) clearTimeout(this.timer);
905
+ this.timer = void 0;
906
+ const child = this.child;
907
+ this.child = void 0;
908
+ if (!child) return;
909
+ child.stdin?.end();
910
+ const force = setTimeout(() => child.kill(), this.stopGraceMs);
911
+ force.unref();
912
+ child.once("exit", () => clearTimeout(force));
913
+ }
914
+ launch() {
915
+ const startedAt = Date.now();
916
+ const child = this.spawnFn(process.execPath, [this.script, "--watch-stdin"], {
917
+ detached: true,
918
+ stdio: ["pipe", "pipe", "pipe"],
919
+ windowsHide: true
920
+ });
921
+ this.child = child;
922
+ child.stdout?.on("data", (d) => process.stdout.write(d));
923
+ child.stderr?.on("data", (d) => process.stderr.write(d));
924
+ child.stdin?.on("error", () => {
925
+ });
926
+ child.on("error", (err) => logger.warn(`Codex adapter failed to start: ${err.message}`));
927
+ child.on("exit", (code, signal) => {
928
+ if (this.child === child) this.child = void 0;
929
+ if (this.stopped || code === 0) return;
930
+ if (Date.now() - startedAt > HEALTHY_RUN_MS) this.delay = this.minDelayMs;
931
+ logger.warn(`Codex adapter exited (${signal ?? code}); restarting in ${this.delay}ms`);
932
+ this.timer = setTimeout(() => {
933
+ this.timer = void 0;
934
+ if (!this.stopped) this.launch();
935
+ }, this.delay);
936
+ this.delay = Math.min(this.delay * 2, this.maxDelayMs);
937
+ });
938
+ }
939
+ };
940
+ }
941
+ });
942
+
848
943
  // src/hub/auth.ts
849
944
  import crypto from "crypto";
850
945
  function safeEqual(a, b) {
@@ -914,8 +1009,8 @@ __export(server_exports, {
914
1009
  HubServer: () => HubServer
915
1010
  });
916
1011
  import http from "http";
917
- import fs4 from "fs";
918
- import path4 from "path";
1012
+ import fs5 from "fs";
1013
+ import path5 from "path";
919
1014
  import { fileURLToPath } from "url";
920
1015
  import { WebSocketServer, WebSocket } from "ws";
921
1016
  import { randomUUID as randomUUID3 } from "crypto";
@@ -928,10 +1023,12 @@ var init_server = __esm({
928
1023
  init_session_manager();
929
1024
  init_notifier();
930
1025
  init_telegram();
1026
+ init_codex_supervisor();
931
1027
  init_config();
1028
+ init_session_label();
932
1029
  init_crash_guard();
933
1030
  init_auth();
934
- __dirname = path4.dirname(fileURLToPath(import.meta.url));
1031
+ __dirname = path5.dirname(fileURLToPath(import.meta.url));
935
1032
  MAX_WS_PAYLOAD = 16 * 1024 * 1024;
936
1033
  HubServer = class {
937
1034
  httpServer;
@@ -954,10 +1051,13 @@ var init_server = __esm({
954
1051
  host;
955
1052
  port;
956
1053
  token;
1054
+ codexEnabled;
1055
+ codexSupervisor;
957
1056
  constructor(config2) {
958
1057
  this.host = config2?.hub?.host ?? DEFAULT_HUB_HOST;
959
1058
  this.port = config2?.hub?.port ?? DEFAULT_HUB_PORT;
960
1059
  this.token = config2?.hub?.token;
1060
+ this.codexEnabled = config2?.codex?.enabled === true;
961
1061
  if (config2?.notifications) {
962
1062
  this.notifier.configure({
963
1063
  desktop: config2.notifications.desktop
@@ -1015,12 +1115,15 @@ var init_server = __esm({
1015
1115
  this.httpServer.on("error", (err) => logger.error(`HTTP server error: ${err.message}`));
1016
1116
  const displayHost = this.host === "0.0.0.0" ? "127.0.0.1" : this.host;
1017
1117
  logger.info(`Hub server listening on http://${displayHost}:${this.port}`);
1118
+ this.startCodexAdapter();
1018
1119
  resolve();
1019
1120
  });
1020
1121
  });
1021
1122
  }
1022
1123
  stop() {
1023
1124
  return new Promise((resolve) => {
1125
+ this.codexSupervisor?.stop();
1126
+ this.codexSupervisor = void 0;
1024
1127
  if (this.heartbeatInterval) clearInterval(this.heartbeatInterval);
1025
1128
  if (this.telegramBot) this.telegramBot.stopPolling();
1026
1129
  for (const ws of this.channelSockets.values()) ws.terminate();
@@ -1121,23 +1224,23 @@ var init_server = __esm({
1121
1224
  return;
1122
1225
  }
1123
1226
  const candidates = [
1124
- path4.join(__dirname, "..", "dashboard", "index.html"),
1227
+ path5.join(__dirname, "..", "dashboard", "index.html"),
1125
1228
  // from dist/hub/
1126
- path4.join(__dirname, "dashboard", "index.html"),
1229
+ path5.join(__dirname, "dashboard", "index.html"),
1127
1230
  // from dist/ (bundled index.js)
1128
- path4.join(__dirname, "..", "..", "src", "dashboard", "index.html"),
1231
+ path5.join(__dirname, "..", "..", "src", "dashboard", "index.html"),
1129
1232
  // from dist/hub/ -> src/
1130
- path4.join(__dirname, "..", "src", "dashboard", "index.html"),
1233
+ path5.join(__dirname, "..", "src", "dashboard", "index.html"),
1131
1234
  // from dist/ -> src/
1132
- path4.join(process.cwd(), "dist", "dashboard", "index.html"),
1235
+ path5.join(process.cwd(), "dist", "dashboard", "index.html"),
1133
1236
  // from cwd
1134
- path4.join(process.cwd(), "src", "dashboard", "index.html")
1237
+ path5.join(process.cwd(), "src", "dashboard", "index.html")
1135
1238
  // from cwd/src
1136
1239
  ];
1137
1240
  logger.debug(`Dashboard candidates: ${JSON.stringify(candidates)}`);
1138
1241
  for (const candidate of candidates) {
1139
- if (fs4.existsSync(candidate)) {
1140
- const html = fs4.readFileSync(candidate, "utf-8");
1242
+ if (fs5.existsSync(candidate)) {
1243
+ const html = fs5.readFileSync(candidate, "utf-8");
1141
1244
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
1142
1245
  res.end(html);
1143
1246
  return;
@@ -1176,7 +1279,7 @@ var init_server = __esm({
1176
1279
  this.jsonResponse(res, 404, { error: "Session not connected" });
1177
1280
  return;
1178
1281
  }
1179
- const msg = { type: "message_to_session", sessionId: sessionId2, content };
1282
+ const msg = { type: "message_to_session", sessionId: sessionId2, content, source: "api" };
1180
1283
  ws.send(JSON.stringify(msg));
1181
1284
  this.jsonResponse(res, 200, { ok: true });
1182
1285
  }
@@ -1350,7 +1453,7 @@ var init_server = __esm({
1350
1453
  if (msg.type === "message_to_session") {
1351
1454
  const channelWs = this.channelSockets.get(msg.sessionId);
1352
1455
  if (channelWs?.readyState === WebSocket.OPEN) {
1353
- channelWs.send(JSON.stringify(msg));
1456
+ channelWs.send(JSON.stringify({ ...msg, source: "dashboard" }));
1354
1457
  }
1355
1458
  } else if (msg.type === "image_upload") {
1356
1459
  this.handleImageUpload(msg);
@@ -1395,11 +1498,11 @@ var init_server = __esm({
1395
1498
  logger.warn("Image upload rejected: exceeds 10MB");
1396
1499
  return;
1397
1500
  }
1398
- fs4.mkdirSync(UPLOADS_DIR, { recursive: true });
1501
+ fs5.mkdirSync(UPLOADS_DIR, { recursive: true });
1399
1502
  const ext = mimeType.split("/")[1] === "jpeg" ? "jpg" : mimeType.split("/")[1];
1400
1503
  const filename = `${randomUUID3()}.${ext}`;
1401
- const filePath = path4.join(UPLOADS_DIR, filename);
1402
- fs4.writeFileSync(filePath, buffer);
1504
+ const filePath = path5.join(UPLOADS_DIR, filename);
1505
+ fs5.writeFileSync(filePath, buffer);
1403
1506
  const forwardMsg = {
1404
1507
  type: "image_to_session",
1405
1508
  sessionId: sessionId2,
@@ -1412,7 +1515,7 @@ var init_server = __esm({
1412
1515
  logger.info(`Image saved and forwarded: ${filename} (${buffer.length} bytes)`);
1413
1516
  setTimeout(() => {
1414
1517
  try {
1415
- fs4.unlinkSync(filePath);
1518
+ fs5.unlinkSync(filePath);
1416
1519
  } catch {
1417
1520
  }
1418
1521
  }, 5 * 60 * 1e3);
@@ -1440,7 +1543,7 @@ var init_server = __esm({
1440
1543
  this.telegramBot.onMessageToSession = (sessionId2, content) => {
1441
1544
  const channelWs = this.channelSockets.get(sessionId2);
1442
1545
  if (channelWs?.readyState === WebSocket.OPEN) {
1443
- const msg = { type: "message_to_session", sessionId: sessionId2, content };
1546
+ const msg = { type: "message_to_session", sessionId: sessionId2, content, source: "telegram" };
1444
1547
  channelWs.send(JSON.stringify(msg));
1445
1548
  logger.info(`Telegram message forwarded to session: ${sessionId2}`);
1446
1549
  }
@@ -1567,6 +1670,17 @@ var init_server = __esm({
1567
1670
  this.jsonResponse(res, 500, { error: err.message });
1568
1671
  }
1569
1672
  }
1673
+ startCodexAdapter() {
1674
+ if (!this.codexEnabled) return;
1675
+ const script = resolveAdapterScript(__dirname);
1676
+ if (!script) {
1677
+ logger.warn("Codex adapter is enabled but codex/main.js was not found next to the hub");
1678
+ return;
1679
+ }
1680
+ this.codexSupervisor = new CodexSupervisor(script);
1681
+ this.codexSupervisor.start();
1682
+ logger.info("Codex adapter started");
1683
+ }
1570
1684
  startHeartbeat() {
1571
1685
  this.heartbeatInterval = setInterval(() => {
1572
1686
  for (const [sessionId2, ws] of this.channelSockets) {
@@ -1582,11 +1696,11 @@ var init_server = __esm({
1582
1696
  }
1583
1697
  cleanupUploads() {
1584
1698
  try {
1585
- if (!fs4.existsSync(UPLOADS_DIR)) return;
1586
- const files = fs4.readdirSync(UPLOADS_DIR);
1699
+ if (!fs5.existsSync(UPLOADS_DIR)) return;
1700
+ const files = fs5.readdirSync(UPLOADS_DIR);
1587
1701
  for (const file of files) {
1588
1702
  try {
1589
- fs4.unlinkSync(path4.join(UPLOADS_DIR, file));
1703
+ fs5.unlinkSync(path5.join(UPLOADS_DIR, file));
1590
1704
  } catch {
1591
1705
  }
1592
1706
  }
@@ -1595,8 +1709,7 @@ var init_server = __esm({
1595
1709
  }
1596
1710
  }
1597
1711
  getSessionLabel(session) {
1598
- if (!session) return "unknown";
1599
- return session.displayName || session.cwd?.replace(/^.*[/\\]/, "") || session.name;
1712
+ return session ? sessionLabel(session) : "unknown";
1600
1713
  }
1601
1714
  jsonResponse(res, status, body) {
1602
1715
  res.writeHead(status, { "Content-Type": "application/json" });
@@ -1668,48 +1781,41 @@ var init_hub_client = __esm({
1668
1781
  init_logger();
1669
1782
  init_constants();
1670
1783
  HubClient = class {
1671
- constructor(sessionId2, sessionName2, hubHost2 = DEFAULT_HUB_HOST, hubPort2 = DEFAULT_HUB_PORT, token, getPeerName = () => void 0) {
1784
+ constructor(sessionId2, sessionName2, hubHost2 = DEFAULT_HUB_HOST, hubPort2 = DEFAULT_HUB_PORT, token, getPeerName = () => void 0, getRegistration = () => ({})) {
1672
1785
  this.sessionId = sessionId2;
1673
1786
  this.sessionName = sessionName2;
1674
1787
  this.hubHost = hubHost2;
1675
1788
  this.hubPort = hubPort2;
1676
1789
  this.token = token;
1677
1790
  this.getPeerName = getPeerName;
1791
+ this.getRegistration = getRegistration;
1678
1792
  }
1679
1793
  ws = null;
1680
1794
  reconnectTimer = null;
1681
1795
  messageHandlers = [];
1682
1796
  queue = [];
1683
1797
  connected = false;
1798
+ closed = false;
1684
1799
  connect() {
1800
+ this.closed = false;
1685
1801
  const tokenQuery = this.token ? `?token=${encodeURIComponent(this.token)}` : "";
1686
1802
  const url = `ws://${this.hubHost}:${this.hubPort}${WS_PATH_CHANNEL}${tokenQuery}`;
1687
1803
  logger.debug(`Connecting to hub at ${url}`);
1688
1804
  try {
1689
- this.ws = new WebSocket2(url);
1690
- this.ws.on("open", () => {
1805
+ const ws = new WebSocket2(url);
1806
+ this.ws = ws;
1807
+ ws.on("open", () => {
1808
+ if (this.ws !== ws) return;
1691
1809
  logger.info("Connected to hub");
1692
1810
  this.connected = true;
1693
- const registration = {
1694
- type: "register",
1695
- session: {
1696
- id: this.sessionId,
1697
- name: this.sessionName,
1698
- status: "idle",
1699
- connectedAt: Date.now(),
1700
- lastActivity: Date.now(),
1701
- cwd: process.cwd(),
1702
- channelEnabled: true,
1703
- peerName: this.getPeerName()
1704
- }
1705
- };
1706
- this.ws.send(JSON.stringify(registration));
1811
+ this.ws.send(JSON.stringify(this.registration()));
1707
1812
  for (const msg of this.queue) {
1708
1813
  this.ws.send(JSON.stringify(msg));
1709
1814
  }
1710
1815
  this.queue = [];
1711
1816
  });
1712
- this.ws.on("message", (data) => {
1817
+ ws.on("message", (data) => {
1818
+ if (this.ws !== ws) return;
1713
1819
  try {
1714
1820
  const msg = JSON.parse(data.toString());
1715
1821
  for (const handler of this.messageHandlers) {
@@ -1719,12 +1825,14 @@ var init_hub_client = __esm({
1719
1825
  logger.warn("Failed to parse hub message:", err);
1720
1826
  }
1721
1827
  });
1722
- this.ws.on("close", () => {
1828
+ ws.on("close", () => {
1829
+ if (this.ws !== ws) return;
1723
1830
  logger.info("Disconnected from hub");
1724
1831
  this.connected = false;
1725
- this.scheduleReconnect();
1832
+ if (!this.closed) this.scheduleReconnect();
1726
1833
  });
1727
- this.ws.on("error", (err) => {
1834
+ ws.on("error", (err) => {
1835
+ if (this.ws !== ws) return;
1728
1836
  logger.debug(`Hub connection error: ${err.message}`);
1729
1837
  this.connected = false;
1730
1838
  });
@@ -1747,6 +1855,7 @@ var init_hub_client = __esm({
1747
1855
  this.messageHandlers.push(handler);
1748
1856
  }
1749
1857
  disconnect() {
1858
+ this.closed = true;
1750
1859
  if (this.reconnectTimer) {
1751
1860
  clearTimeout(this.reconnectTimer);
1752
1861
  this.reconnectTimer = null;
@@ -1757,8 +1866,29 @@ var init_hub_client = __esm({
1757
1866
  }
1758
1867
  this.connected = false;
1759
1868
  }
1869
+ reregister() {
1870
+ if (this.connected && this.ws?.readyState === WebSocket2.OPEN) {
1871
+ this.ws.send(JSON.stringify(this.registration()));
1872
+ }
1873
+ }
1874
+ registration() {
1875
+ return {
1876
+ type: "register",
1877
+ session: {
1878
+ id: this.sessionId,
1879
+ name: this.sessionName,
1880
+ status: "idle",
1881
+ connectedAt: Date.now(),
1882
+ lastActivity: Date.now(),
1883
+ cwd: process.cwd(),
1884
+ channelEnabled: true,
1885
+ peerName: this.getPeerName(),
1886
+ ...this.getRegistration()
1887
+ }
1888
+ };
1889
+ }
1760
1890
  scheduleReconnect() {
1761
- if (this.reconnectTimer) return;
1891
+ if (this.reconnectTimer || this.closed) return;
1762
1892
  this.reconnectTimer = setTimeout(() => {
1763
1893
  this.reconnectTimer = null;
1764
1894
  this.connect();
@@ -1769,9 +1899,9 @@ var init_hub_client = __esm({
1769
1899
  });
1770
1900
 
1771
1901
  // src/channel/peer-name.ts
1772
- import fs5 from "fs";
1902
+ import fs6 from "fs";
1773
1903
  import os2 from "os";
1774
- import path5 from "path";
1904
+ import path6 from "path";
1775
1905
  function findPeerName(records, env) {
1776
1906
  const valid = records.filter((r) => typeof r === "object" && r !== null);
1777
1907
  const nameOf = (match) => {
@@ -1791,18 +1921,18 @@ function readPeerName(env = process.env) {
1791
1921
  sessionId: env.CLAUDE_CODE_SESSION_ID
1792
1922
  };
1793
1923
  if (!lookup.messagingSocket && !lookup.sessionId) return void 0;
1794
- const configDir = env.CLAUDE_CONFIG_DIR || path5.join(os2.homedir(), ".claude");
1795
- const dir = path5.join(configDir, "sessions");
1924
+ const configDir = env.CLAUDE_CONFIG_DIR || path6.join(os2.homedir(), ".claude");
1925
+ const dir = path6.join(configDir, "sessions");
1796
1926
  let files;
1797
1927
  try {
1798
- files = fs5.readdirSync(dir).filter((f) => f.endsWith(".json"));
1928
+ files = fs6.readdirSync(dir).filter((f) => f.endsWith(".json"));
1799
1929
  } catch {
1800
1930
  return void 0;
1801
1931
  }
1802
1932
  const records = [];
1803
1933
  for (const f of files) {
1804
1934
  try {
1805
- records.push(JSON.parse(fs5.readFileSync(path5.join(dir, f), "utf8")));
1935
+ records.push(JSON.parse(fs6.readFileSync(path6.join(dir, f), "utf8")));
1806
1936
  } catch {
1807
1937
  }
1808
1938
  }
@@ -1825,7 +1955,7 @@ import {
1825
1955
  } from "@modelcontextprotocol/sdk/types.js";
1826
1956
  import { z } from "zod";
1827
1957
  import { randomUUID as randomUUID4 } from "crypto";
1828
- import path6 from "path";
1958
+ import path7 from "path";
1829
1959
  async function main() {
1830
1960
  logger.info(`Starting MCP channel server (session: ${sessionId})`);
1831
1961
  hubClient.connect();
@@ -1894,7 +2024,7 @@ var init_server2 = __esm({
1894
2024
  init_hub_client();
1895
2025
  init_peer_name2();
1896
2026
  sessionId = randomUUID4();
1897
- sessionName = process.env.CLAUDE_ALARM_SESSION_NAME ?? path6.basename(process.cwd());
2027
+ sessionName = process.env.CLAUDE_ALARM_SESSION_NAME ?? path7.basename(process.cwd());
1898
2028
  peerName = readPeerName();
1899
2029
  server = new Server(
1900
2030
  {
@@ -2073,12 +2203,12 @@ init_config();
2073
2203
  init_constants();
2074
2204
  init_logger();
2075
2205
  init_crash_guard();
2076
- import { spawn } from "child_process";
2077
- import fs6 from "fs";
2078
- import path7 from "path";
2206
+ import { spawn as spawn2 } from "child_process";
2207
+ import fs7 from "fs";
2208
+ import path8 from "path";
2079
2209
  import readline from "readline";
2080
2210
  import { fileURLToPath as fileURLToPath2 } from "url";
2081
- var __dirname2 = path7.dirname(fileURLToPath2(import.meta.url));
2211
+ var __dirname2 = path8.dirname(fileURLToPath2(import.meta.url));
2082
2212
  function authHeaders(token) {
2083
2213
  return token ? { Authorization: `Bearer ${token}` } : {};
2084
2214
  }
@@ -2097,6 +2227,11 @@ Usage:
2097
2227
  claude-alarm setup [dir] Add claude-alarm to .mcp.json
2098
2228
  claude-alarm test Send a test notification
2099
2229
  claude-alarm token Show current auth token
2230
+ claude-alarm codex enable Start the Codex adapter together with the hub
2231
+ claude-alarm codex disable Stop starting the Codex adapter with the hub
2232
+ claude-alarm codex start Run the Codex adapter on its own (e.g. Codex on another PC)
2233
+ claude-alarm codex stop Stop a running Codex adapter
2234
+ claude-alarm codex status Show Codex adapter status
2100
2235
  claude-alarm help Show this help
2101
2236
 
2102
2237
  Quick start:
@@ -2105,8 +2240,8 @@ Quick start:
2105
2240
  }
2106
2241
  async function checkForUpdates() {
2107
2242
  try {
2108
- const pkgPath = path7.join(__dirname2, "..", "package.json");
2109
- const pkg = JSON.parse(fs6.readFileSync(pkgPath, "utf-8"));
2243
+ const pkgPath = path8.join(__dirname2, "..", "package.json");
2244
+ const pkg = JSON.parse(fs7.readFileSync(pkgPath, "utf-8"));
2110
2245
  const currentVersion = pkg.version;
2111
2246
  const res = await fetch("https://registry.npmjs.org/@delt/claude-alarm/latest", {
2112
2247
  signal: AbortSignal.timeout(3e3)
@@ -2129,25 +2264,25 @@ async function hubStart(daemon) {
2129
2264
  const port = config2.hub.port ?? DEFAULT_HUB_PORT;
2130
2265
  const displayHost = host === "0.0.0.0" ? "127.0.0.1" : host;
2131
2266
  checkForUpdates();
2132
- if (fs6.existsSync(PID_FILE)) {
2133
- const pid = parseInt(fs6.readFileSync(PID_FILE, "utf-8").trim(), 10);
2267
+ if (fs7.existsSync(PID_FILE)) {
2268
+ const pid = parseInt(fs7.readFileSync(PID_FILE, "utf-8").trim(), 10);
2134
2269
  if (isProcessRunning(pid)) {
2135
2270
  console.log(`Hub is already running (PID: ${pid}). Dashboard: ${loginLink(displayHost, port, config2.hub.token)}`);
2136
2271
  return;
2137
2272
  }
2138
- fs6.unlinkSync(PID_FILE);
2273
+ fs7.unlinkSync(PID_FILE);
2139
2274
  }
2140
2275
  if (daemon) {
2141
2276
  ensureConfigDir();
2142
- const logFd = fs6.openSync(LOG_FILE, "a");
2143
- const hubScript = path7.join(__dirname2, "hub", "server.js");
2144
- const child = spawn(process.execPath, [hubScript], {
2277
+ const logFd = fs7.openSync(LOG_FILE, "a");
2278
+ const hubScript = path8.join(__dirname2, "hub", "server.js");
2279
+ const child = spawn2(process.execPath, [hubScript], {
2145
2280
  detached: true,
2146
2281
  stdio: ["ignore", logFd, logFd],
2147
2282
  env: { ...process.env }
2148
2283
  });
2149
2284
  if (child.pid) {
2150
- fs6.writeFileSync(PID_FILE, String(child.pid), "utf-8");
2285
+ fs7.writeFileSync(PID_FILE, String(child.pid), "utf-8");
2151
2286
  child.unref();
2152
2287
  console.log(`Hub started as daemon (PID: ${child.pid})`);
2153
2288
  console.log(`Dashboard: ${loginLink(displayHost, port, config2.hub.token)}`);
@@ -2168,11 +2303,11 @@ async function hubStart(daemon) {
2168
2303
  const hub = new HubServer2(config2);
2169
2304
  await hub.start();
2170
2305
  ensureConfigDir();
2171
- fs6.writeFileSync(PID_FILE, String(process.pid), "utf-8");
2306
+ fs7.writeFileSync(PID_FILE, String(process.pid), "utf-8");
2172
2307
  const shutdown = async () => {
2173
2308
  console.log("\nShutting down...");
2174
2309
  await hub.stop();
2175
- if (fs6.existsSync(PID_FILE)) fs6.unlinkSync(PID_FILE);
2310
+ if (fs7.existsSync(PID_FILE)) fs7.unlinkSync(PID_FILE);
2176
2311
  process.exit(0);
2177
2312
  };
2178
2313
  process.on("SIGINT", shutdown);
@@ -2180,18 +2315,18 @@ async function hubStart(daemon) {
2180
2315
  }
2181
2316
  }
2182
2317
  function hubStop() {
2183
- if (!fs6.existsSync(PID_FILE)) {
2318
+ if (!fs7.existsSync(PID_FILE)) {
2184
2319
  console.log("Hub is not running (no PID file found)");
2185
2320
  return;
2186
2321
  }
2187
- const pid = parseInt(fs6.readFileSync(PID_FILE, "utf-8").trim(), 10);
2322
+ const pid = parseInt(fs7.readFileSync(PID_FILE, "utf-8").trim(), 10);
2188
2323
  try {
2189
2324
  process.kill(pid, "SIGTERM");
2190
2325
  console.log(`Hub stopped (PID: ${pid})`);
2191
2326
  } catch {
2192
2327
  console.log("Hub process not found (may have already stopped)");
2193
2328
  }
2194
- fs6.unlinkSync(PID_FILE);
2329
+ fs7.unlinkSync(PID_FILE);
2195
2330
  }
2196
2331
  async function hubStatus() {
2197
2332
  const config2 = loadConfig();
@@ -2199,8 +2334,8 @@ async function hubStatus() {
2199
2334
  const port = config2.hub.port ?? DEFAULT_HUB_PORT;
2200
2335
  const displayHost = host === "0.0.0.0" ? "127.0.0.1" : host;
2201
2336
  let pidInfo = "not running";
2202
- if (fs6.existsSync(PID_FILE)) {
2203
- const pid = parseInt(fs6.readFileSync(PID_FILE, "utf-8").trim(), 10);
2337
+ if (fs7.existsSync(PID_FILE)) {
2338
+ const pid = parseInt(fs7.readFileSync(PID_FILE, "utf-8").trim(), 10);
2204
2339
  if (isProcessRunning(pid)) {
2205
2340
  pidInfo = `running (PID: ${pid})`;
2206
2341
  } else {
@@ -2272,7 +2407,7 @@ function ask(question) {
2272
2407
  }
2273
2408
  async function init() {
2274
2409
  const dir = process.cwd();
2275
- const projectName = path7.basename(dir);
2410
+ const projectName = path8.basename(dir);
2276
2411
  console.log(`
2277
2412
  claude-alarm init for "${projectName}"
2278
2413
  `);
@@ -2292,11 +2427,11 @@ claude-alarm init for "${projectName}"
2292
2427
  if (port) env.CLAUDE_ALARM_HUB_PORT = port;
2293
2428
  if (token) env.CLAUDE_ALARM_HUB_TOKEN = token;
2294
2429
  }
2295
- const mcpPath = path7.join(dir, ".mcp.json");
2430
+ const mcpPath = path8.join(dir, ".mcp.json");
2296
2431
  let mcpConfig = {};
2297
- if (fs6.existsSync(mcpPath)) {
2432
+ if (fs7.existsSync(mcpPath)) {
2298
2433
  try {
2299
- mcpConfig = JSON.parse(fs6.readFileSync(mcpPath, "utf-8"));
2434
+ mcpConfig = JSON.parse(fs7.readFileSync(mcpPath, "utf-8"));
2300
2435
  } catch {
2301
2436
  mcpConfig = {};
2302
2437
  }
@@ -2307,7 +2442,7 @@ claude-alarm init for "${projectName}"
2307
2442
  args: ["-y", "@delt/claude-alarm", "serve"],
2308
2443
  env
2309
2444
  };
2310
- fs6.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2), "utf-8");
2445
+ fs7.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2), "utf-8");
2311
2446
  console.log(`
2312
2447
  \u2713 Created ${mcpPath}`);
2313
2448
  if (remote.toLowerCase() !== "y") {
@@ -2352,6 +2487,54 @@ function isProcessRunning(pid) {
2352
2487
  return false;
2353
2488
  }
2354
2489
  }
2490
+ 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);
2493
+ return Number.isNaN(pid) ? void 0 : pid;
2494
+ }
2495
+ function codexEnable(enabled) {
2496
+ setCodexEnabled(enabled);
2497
+ console.log(enabled ? "Codex adapter enabled. Restart the hub to apply: claude-alarm hub stop, then claude-alarm hub start" : "Codex adapter disabled. Restart the hub to apply.");
2498
+ }
2499
+ function codexStart() {
2500
+ const pid = readCodexPid();
2501
+ if (pid !== void 0 && isProcessRunning(pid)) {
2502
+ console.log(`Codex adapter is already running (PID: ${pid})`);
2503
+ return;
2504
+ }
2505
+ ensureConfigDir();
2506
+ const logFd = fs7.openSync(CODEX_LOG_FILE, "a");
2507
+ const child = spawn2(process.execPath, [path8.join(__dirname2, "codex", "main.js")], {
2508
+ detached: true,
2509
+ stdio: ["ignore", logFd, logFd],
2510
+ env: { ...process.env }
2511
+ });
2512
+ child.unref();
2513
+ console.log(`Codex adapter started (PID: ${child.pid}). Logs: ${CODEX_LOG_FILE}`);
2514
+ }
2515
+ function codexStop() {
2516
+ const pid = readCodexPid();
2517
+ if (pid === void 0) {
2518
+ console.log("Codex adapter is not running");
2519
+ return;
2520
+ }
2521
+ try {
2522
+ process.kill(pid, "SIGTERM");
2523
+ console.log(`Codex adapter stopped (PID: ${pid})`);
2524
+ } catch {
2525
+ console.log("Codex adapter process not found (may have already stopped)");
2526
+ }
2527
+ try {
2528
+ fs7.unlinkSync(CODEX_PID_FILE);
2529
+ } catch {
2530
+ }
2531
+ }
2532
+ function codexStatus() {
2533
+ const pid = readCodexPid();
2534
+ const state = pid === void 0 ? "not running" : isProcessRunning(pid) ? `running (PID: ${pid})` : "not running (stale PID file)";
2535
+ console.log(`Codex adapter: ${state}`);
2536
+ console.log(`Start with hub: ${loadConfig().codex?.enabled ? "enabled" : "disabled"}`);
2537
+ }
2355
2538
  async function main2() {
2356
2539
  const args = process.argv.slice(2);
2357
2540
  const cmd = args[0];
@@ -2383,6 +2566,19 @@ async function main2() {
2383
2566
  }
2384
2567
  return;
2385
2568
  }
2569
+ if (cmd === "codex") {
2570
+ if (sub === "enable") codexEnable(true);
2571
+ else if (sub === "disable") codexEnable(false);
2572
+ else if (sub === "start") codexStart();
2573
+ else if (sub === "stop") codexStop();
2574
+ else if (sub === "status") codexStatus();
2575
+ else {
2576
+ console.error(`Unknown codex command: ${sub}`);
2577
+ printUsage();
2578
+ process.exit(1);
2579
+ }
2580
+ return;
2581
+ }
2386
2582
  if (cmd === "setup") {
2387
2583
  setup(args[1]);
2388
2584
  return;