@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.
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,15 @@ 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
+ }
78
+ function shouldOfferCodex(config2, installed) {
79
+ return installed && config2.codex === void 0;
80
+ }
70
81
  function setupMcpConfig(targetDir) {
71
82
  const dir = targetDir ?? process.cwd();
72
83
  const mcpPath = path2.join(dir, ".mcp.json");
@@ -135,11 +146,11 @@ var init_logger = __esm({
135
146
  });
136
147
 
137
148
  // src/shared/crash-guard.ts
138
- import fs2 from "fs";
149
+ import fs3 from "fs";
139
150
  function appendToLog(text) {
140
151
  try {
141
- fs2.mkdirSync(CONFIG_DIR, { recursive: true });
142
- fs2.appendFileSync(LOG_FILE, text, "utf-8");
152
+ fs3.mkdirSync(CONFIG_DIR, { recursive: true });
153
+ fs3.appendFileSync(LOG_FILE, text, "utf-8");
143
154
  } catch {
144
155
  }
145
156
  }
@@ -211,9 +222,9 @@ var init_session_manager = __esm({
211
222
  SessionManager = class {
212
223
  sessions = /* @__PURE__ */ new Map();
213
224
  register(session) {
214
- const baseName = session.cwd?.replace(/^.*[/\\]/, "") || session.name;
225
+ const baseName = this.baseName(session);
215
226
  const existing = Array.from(this.sessions.values()).filter(
216
- (s) => s.id !== session.id && (s.cwd?.replace(/^.*[/\\]/, "") || s.name) === baseName
227
+ (s) => s.id !== session.id && this.baseName(s) === baseName
217
228
  );
218
229
  if (existing.length > 0) {
219
230
  const usedNums = existing.map((s) => {
@@ -267,6 +278,9 @@ var init_session_manager = __esm({
267
278
  count() {
268
279
  return this.sessions.size;
269
280
  }
281
+ baseName(session) {
282
+ return session.title || session.cwd?.replace(/^.*[/\\]/, "") || session.name;
283
+ }
270
284
  };
271
285
  }
272
286
  });
@@ -294,7 +308,7 @@ var init_notifier = __esm({
294
308
  async notify(title, message, level = "info") {
295
309
  await this.notifyWithSession(void 0, void 0, title, message, level);
296
310
  }
297
- async notifyWithSession(sessionId2, sessionLabel, title, message, level = "info") {
311
+ async notifyWithSession(sessionId2, sessionLabel2, title, message, level = "info") {
298
312
  const promises = [];
299
313
  if (this.desktopEnabled) {
300
314
  promises.push(this.sendDesktop(title, message, level));
@@ -302,8 +316,8 @@ var init_notifier = __esm({
302
316
  for (const webhook of this.webhooks) {
303
317
  promises.push(this.sendWebhook(webhook, title, message, level));
304
318
  }
305
- if (this.telegramBot && sessionId2 && sessionLabel) {
306
- promises.push(this.telegramBot.sendNotification(sessionId2, sessionLabel, title, message));
319
+ if (this.telegramBot && sessionId2 && sessionLabel2) {
320
+ promises.push(this.telegramBot.sendNotification(sessionId2, sessionLabel2, title, message));
307
321
  }
308
322
  await Promise.allSettled(promises);
309
323
  }
@@ -387,9 +401,20 @@ var init_notifier = __esm({
387
401
  }
388
402
  });
389
403
 
404
+ // src/shared/session-label.ts
405
+ function sessionLabel(session) {
406
+ const base = session.displayName || session.cwd?.replace(/^.*[/\\]/, "") || session.name;
407
+ return session.agentKind === "codex" ? `Codex \xB7 ${base}` : base;
408
+ }
409
+ var init_session_label = __esm({
410
+ "src/shared/session-label.ts"() {
411
+ "use strict";
412
+ }
413
+ });
414
+
390
415
  // src/hub/telegram.ts
391
- import fs3 from "fs";
392
- import path3 from "path";
416
+ import fs4 from "fs";
417
+ import path4 from "path";
393
418
  import { randomUUID as randomUUID2 } from "crypto";
394
419
  var TELEGRAM_API, TelegramBot;
395
420
  var init_telegram = __esm({
@@ -397,6 +422,7 @@ var init_telegram = __esm({
397
422
  "use strict";
398
423
  init_logger();
399
424
  init_constants();
425
+ init_session_label();
400
426
  TELEGRAM_API = "https://api.telegram.org/bot";
401
427
  TelegramBot = class _TelegramBot {
402
428
  config;
@@ -535,15 +561,14 @@ ${this.mdToHtml(message)}`;
535
561
  const pending = this.pendingMessages.get(msg.chat.id);
536
562
  if (pending) {
537
563
  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) {
564
+ const session = this.pendingSession(pending, parseInt(selectMatch[1], 10) - 1);
565
+ if (session) {
541
566
  if (pending.photoFileId) {
542
- await this.deliverPhotoToSessionByFileId(sessions2[idx].id, pending.photoFileId, pending.caption);
567
+ await this.deliverPhotoToSessionByFileId(session.id, pending.photoFileId, pending.caption);
543
568
  } else if (pending.text) {
544
- this.deliverToSession(sessions2[idx].id, pending.text);
569
+ this.deliverToSession(session.id, pending.text);
545
570
  }
546
- this.sendMessage(`Sent to [${this.getLabel(sessions2[idx])}]`);
571
+ this.sendMessage(`Sent to [${this.getLabel(session)}]`);
547
572
  } else {
548
573
  this.sendMessage("Invalid session number.");
549
574
  }
@@ -564,11 +589,12 @@ ${this.mdToHtml(message)}`;
564
589
  }
565
590
  return;
566
591
  }
592
+ const sessionIds = sessions.map((s) => s.id);
567
593
  if (hasPhoto) {
568
594
  const largest = msg.photo[msg.photo.length - 1];
569
- this.pendingMessages.set(msg.chat.id, { photoFileId: largest.file_id, caption: text });
595
+ this.pendingMessages.set(msg.chat.id, { photoFileId: largest.file_id, caption: text, sessionIds });
570
596
  } else {
571
- this.pendingMessages.set(msg.chat.id, { text });
597
+ this.pendingMessages.set(msg.chat.id, { text, sessionIds });
572
598
  }
573
599
  const buttons = sessions.map((s, i) => ({
574
600
  text: this.getLabel(s),
@@ -607,17 +633,17 @@ ${this.mdToHtml(message)}`;
607
633
  const buffer = Buffer.from(await imgRes.arrayBuffer());
608
634
  const ext = fileData.result.file_path.split(".").pop() || "jpg";
609
635
  const mimeType = ext === "png" ? "image/png" : ext === "gif" ? "image/gif" : ext === "webp" ? "image/webp" : "image/jpeg";
610
- fs3.mkdirSync(UPLOADS_DIR, { recursive: true });
636
+ fs4.mkdirSync(UPLOADS_DIR, { recursive: true });
611
637
  const filename = `${randomUUID2()}.${ext}`;
612
- const filePath = path3.join(UPLOADS_DIR, filename);
613
- fs3.writeFileSync(filePath, buffer);
638
+ const filePath = path4.join(UPLOADS_DIR, filename);
639
+ fs4.writeFileSync(filePath, buffer);
614
640
  logger.info(`Telegram photo saved: ${filename} (${buffer.length} bytes)`);
615
641
  if (this.onImageToSession) {
616
642
  this.onImageToSession(sessionId2, filePath, mimeType, caption);
617
643
  }
618
644
  setTimeout(() => {
619
645
  try {
620
- fs3.unlinkSync(filePath);
646
+ fs4.unlinkSync(filePath);
621
647
  } catch {
622
648
  }
623
649
  }, 5 * 60 * 1e3);
@@ -625,11 +651,16 @@ ${this.mdToHtml(message)}`;
625
651
  logger.warn(`Telegram photo download failed: ${err.message}`);
626
652
  }
627
653
  }
654
+ // Buttons are numbered against the list shown when they were sent; Codex sessions come and go, so the live list may have shifted.
655
+ pendingSession(pending, idx) {
656
+ const id = pending.sessionIds[idx];
657
+ return id ? this.getSessions?.().find((s) => s.id === id) : void 0;
658
+ }
628
659
  getLabel(session) {
629
- return session.displayName || session.cwd?.replace(/^.*[/\\]/, "") || session.name;
660
+ return sessionLabel(session);
630
661
  }
631
662
  /** Send a permission request with inline buttons */
632
- async sendPermissionRequest(sessionId2, sessionLabel, requestId, toolName, description, inputPreview) {
663
+ async sendPermissionRequest(sessionId2, sessionLabel2, requestId, toolName, description, inputPreview) {
633
664
  const isNotify = toolName.endsWith("__notify") || toolName === "notify";
634
665
  let preview = inputPreview;
635
666
  let kind = "code";
@@ -682,7 +713,7 @@ ${this.mdToHtml(message)}`;
682
713
  const mcpMatch = toolName.match(/__([^_]+)$/);
683
714
  if (mcpMatch) displayTool = mcpMatch[1].charAt(0).toUpperCase() + mcpMatch[1].slice(1);
684
715
  }
685
- const text = `\u26A0\uFE0F <b>Permission Request</b> \u2014 ${this.escHtml(sessionLabel)}
716
+ const text = `\u26A0\uFE0F <b>Permission Request</b> \u2014 ${this.escHtml(sessionLabel2)}
686
717
 
687
718
  \u{1F527} <b>${this.escHtml(displayTool)}</b>
688
719
  ${previewHtml}${truncNote}`;
@@ -722,22 +753,18 @@ ${label}`);
722
753
  const parts = query.data.split(":");
723
754
  if (parts.length < 3) return;
724
755
  const [, idxStr, chatIdStr] = parts;
725
- const idx = parseInt(idxStr, 10);
726
756
  const chatId = parseInt(chatIdStr, 10);
727
- const sessions = this.getSessions?.() ?? [];
728
- if (idx < 0 || idx >= sessions.length) {
757
+ const pending = this.pendingMessages.get(chatId);
758
+ const session = pending ? this.pendingSession(pending, parseInt(idxStr, 10)) : void 0;
759
+ if (!pending || !session) {
729
760
  await this.answerCallbackQuery(query.id, "Session not found");
730
761
  return;
731
762
  }
732
- const session = sessions[idx];
733
- const pending = this.pendingMessages.get(chatId);
734
763
  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
- }
764
+ if (pending.photoFileId) {
765
+ await this.deliverPhotoToSessionByFileId(session.id, pending.photoFileId, pending.caption);
766
+ } else if (pending.text) {
767
+ this.deliverToSession(session.id, pending.text);
741
768
  }
742
769
  await this.answerCallbackQuery(query.id, `Sent to ${this.getLabel(session)}`);
743
770
  if (query.message) {
@@ -799,8 +826,8 @@ ${label}`);
799
826
  css: "css",
800
827
  scss: "scss"
801
828
  };
802
- langOf(path8) {
803
- const m = /\.([a-z0-9]+)$/i.exec(path8 || "");
829
+ langOf(path10) {
830
+ const m = /\.([a-z0-9]+)$/i.exec(path10 || "");
804
831
  return m ? _TelegramBot.LANG_BY_EXT[m[1].toLowerCase()] ?? "" : "";
805
832
  }
806
833
  /** Convert markdown to Telegram HTML (escape first, then apply formatting) */
@@ -845,6 +872,77 @@ ${bodyLines.join("\n")}`;
845
872
  }
846
873
  });
847
874
 
875
+ // src/hub/codex-supervisor.ts
876
+ import { spawn as spawn2 } from "child_process";
877
+ import fs5 from "fs";
878
+ import path5 from "path";
879
+ function resolveAdapterScript(baseDir) {
880
+ return [path5.join(baseDir, "..", "codex", "main.js"), path5.join(baseDir, "codex", "main.js")].find((p) => fs5.existsSync(p));
881
+ }
882
+ var HEALTHY_RUN_MS, CodexSupervisor;
883
+ var init_codex_supervisor = __esm({
884
+ "src/hub/codex-supervisor.ts"() {
885
+ "use strict";
886
+ init_logger();
887
+ HEALTHY_RUN_MS = 6e4;
888
+ CodexSupervisor = class {
889
+ constructor(script, spawnFn = spawn2, minDelayMs = 2e3, maxDelayMs = 6e4, stopGraceMs = 3e3) {
890
+ this.script = script;
891
+ this.spawnFn = spawnFn;
892
+ this.minDelayMs = minDelayMs;
893
+ this.maxDelayMs = maxDelayMs;
894
+ this.stopGraceMs = stopGraceMs;
895
+ this.delay = minDelayMs;
896
+ }
897
+ child;
898
+ timer;
899
+ stopped = true;
900
+ delay;
901
+ start() {
902
+ this.stopped = false;
903
+ this.launch();
904
+ }
905
+ stop() {
906
+ this.stopped = true;
907
+ if (this.timer) clearTimeout(this.timer);
908
+ this.timer = void 0;
909
+ const child = this.child;
910
+ this.child = void 0;
911
+ if (!child) return;
912
+ child.stdin?.end();
913
+ const force = setTimeout(() => child.kill(), this.stopGraceMs);
914
+ force.unref();
915
+ child.once("exit", () => clearTimeout(force));
916
+ }
917
+ launch() {
918
+ const startedAt = Date.now();
919
+ const child = this.spawnFn(process.execPath, [this.script, "--watch-stdin"], {
920
+ detached: true,
921
+ stdio: ["pipe", "pipe", "pipe"],
922
+ windowsHide: true
923
+ });
924
+ this.child = child;
925
+ child.stdout?.on("data", (d) => process.stdout.write(d));
926
+ child.stderr?.on("data", (d) => process.stderr.write(d));
927
+ child.stdin?.on("error", () => {
928
+ });
929
+ child.on("error", (err) => logger.warn(`Codex adapter failed to start: ${err.message}`));
930
+ child.on("exit", (code, signal) => {
931
+ if (this.child === child) this.child = void 0;
932
+ if (this.stopped || code === 0) return;
933
+ if (Date.now() - startedAt > HEALTHY_RUN_MS) this.delay = this.minDelayMs;
934
+ logger.warn(`Codex adapter exited (${signal ?? code}); restarting in ${this.delay}ms`);
935
+ this.timer = setTimeout(() => {
936
+ this.timer = void 0;
937
+ if (!this.stopped) this.launch();
938
+ }, this.delay);
939
+ this.delay = Math.min(this.delay * 2, this.maxDelayMs);
940
+ });
941
+ }
942
+ };
943
+ }
944
+ });
945
+
848
946
  // src/hub/auth.ts
849
947
  import crypto from "crypto";
850
948
  function safeEqual(a, b) {
@@ -914,10 +1012,10 @@ __export(server_exports, {
914
1012
  HubServer: () => HubServer
915
1013
  });
916
1014
  import http from "http";
917
- import fs4 from "fs";
918
- import path4 from "path";
1015
+ import fs6 from "fs";
1016
+ import path6 from "path";
919
1017
  import { fileURLToPath } from "url";
920
- import { WebSocketServer, WebSocket } from "ws";
1018
+ import { WebSocketServer, WebSocket as WebSocket2 } from "ws";
921
1019
  import { randomUUID as randomUUID3 } from "crypto";
922
1020
  var __dirname, MAX_WS_PAYLOAD, HubServer;
923
1021
  var init_server = __esm({
@@ -928,10 +1026,12 @@ var init_server = __esm({
928
1026
  init_session_manager();
929
1027
  init_notifier();
930
1028
  init_telegram();
1029
+ init_codex_supervisor();
931
1030
  init_config();
1031
+ init_session_label();
932
1032
  init_crash_guard();
933
1033
  init_auth();
934
- __dirname = path4.dirname(fileURLToPath(import.meta.url));
1034
+ __dirname = path6.dirname(fileURLToPath(import.meta.url));
935
1035
  MAX_WS_PAYLOAD = 16 * 1024 * 1024;
936
1036
  HubServer = class {
937
1037
  httpServer;
@@ -954,10 +1054,13 @@ var init_server = __esm({
954
1054
  host;
955
1055
  port;
956
1056
  token;
1057
+ codexEnabled;
1058
+ codexSupervisor;
957
1059
  constructor(config2) {
958
1060
  this.host = config2?.hub?.host ?? DEFAULT_HUB_HOST;
959
1061
  this.port = config2?.hub?.port ?? DEFAULT_HUB_PORT;
960
1062
  this.token = config2?.hub?.token;
1063
+ this.codexEnabled = config2?.codex?.enabled === true;
961
1064
  if (config2?.notifications) {
962
1065
  this.notifier.configure({
963
1066
  desktop: config2.notifications.desktop
@@ -1015,12 +1118,15 @@ var init_server = __esm({
1015
1118
  this.httpServer.on("error", (err) => logger.error(`HTTP server error: ${err.message}`));
1016
1119
  const displayHost = this.host === "0.0.0.0" ? "127.0.0.1" : this.host;
1017
1120
  logger.info(`Hub server listening on http://${displayHost}:${this.port}`);
1121
+ this.startCodexAdapter();
1018
1122
  resolve();
1019
1123
  });
1020
1124
  });
1021
1125
  }
1022
1126
  stop() {
1023
1127
  return new Promise((resolve) => {
1128
+ this.codexSupervisor?.stop();
1129
+ this.codexSupervisor = void 0;
1024
1130
  if (this.heartbeatInterval) clearInterval(this.heartbeatInterval);
1025
1131
  if (this.telegramBot) this.telegramBot.stopPolling();
1026
1132
  for (const ws of this.channelSockets.values()) ws.terminate();
@@ -1121,23 +1227,23 @@ var init_server = __esm({
1121
1227
  return;
1122
1228
  }
1123
1229
  const candidates = [
1124
- path4.join(__dirname, "..", "dashboard", "index.html"),
1230
+ path6.join(__dirname, "..", "dashboard", "index.html"),
1125
1231
  // from dist/hub/
1126
- path4.join(__dirname, "dashboard", "index.html"),
1232
+ path6.join(__dirname, "dashboard", "index.html"),
1127
1233
  // from dist/ (bundled index.js)
1128
- path4.join(__dirname, "..", "..", "src", "dashboard", "index.html"),
1234
+ path6.join(__dirname, "..", "..", "src", "dashboard", "index.html"),
1129
1235
  // from dist/hub/ -> src/
1130
- path4.join(__dirname, "..", "src", "dashboard", "index.html"),
1236
+ path6.join(__dirname, "..", "src", "dashboard", "index.html"),
1131
1237
  // from dist/ -> src/
1132
- path4.join(process.cwd(), "dist", "dashboard", "index.html"),
1238
+ path6.join(process.cwd(), "dist", "dashboard", "index.html"),
1133
1239
  // from cwd
1134
- path4.join(process.cwd(), "src", "dashboard", "index.html")
1240
+ path6.join(process.cwd(), "src", "dashboard", "index.html")
1135
1241
  // from cwd/src
1136
1242
  ];
1137
1243
  logger.debug(`Dashboard candidates: ${JSON.stringify(candidates)}`);
1138
1244
  for (const candidate of candidates) {
1139
- if (fs4.existsSync(candidate)) {
1140
- const html = fs4.readFileSync(candidate, "utf-8");
1245
+ if (fs6.existsSync(candidate)) {
1246
+ const html = fs6.readFileSync(candidate, "utf-8");
1141
1247
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
1142
1248
  res.end(html);
1143
1249
  return;
@@ -1172,11 +1278,11 @@ var init_server = __esm({
1172
1278
  return;
1173
1279
  }
1174
1280
  const ws = this.channelSockets.get(sessionId2);
1175
- if (!ws || ws.readyState !== WebSocket.OPEN) {
1281
+ if (!ws || ws.readyState !== WebSocket2.OPEN) {
1176
1282
  this.jsonResponse(res, 404, { error: "Session not connected" });
1177
1283
  return;
1178
1284
  }
1179
- const msg = { type: "message_to_session", sessionId: sessionId2, content };
1285
+ const msg = { type: "message_to_session", sessionId: sessionId2, content, source: "api" };
1180
1286
  ws.send(JSON.stringify(msg));
1181
1287
  this.jsonResponse(res, 200, { ok: true });
1182
1288
  }
@@ -1349,14 +1455,14 @@ var init_server = __esm({
1349
1455
  const msg = JSON.parse(data.toString());
1350
1456
  if (msg.type === "message_to_session") {
1351
1457
  const channelWs = this.channelSockets.get(msg.sessionId);
1352
- if (channelWs?.readyState === WebSocket.OPEN) {
1353
- channelWs.send(JSON.stringify(msg));
1458
+ if (channelWs?.readyState === WebSocket2.OPEN) {
1459
+ channelWs.send(JSON.stringify({ ...msg, source: "dashboard" }));
1354
1460
  }
1355
1461
  } else if (msg.type === "image_upload") {
1356
1462
  this.handleImageUpload(msg);
1357
1463
  } else if (msg.type === "permission_response") {
1358
1464
  const channelWs = this.channelSockets.get(msg.sessionId);
1359
- if (channelWs?.readyState === WebSocket.OPEN) {
1465
+ if (channelWs?.readyState === WebSocket2.OPEN) {
1360
1466
  channelWs.send(JSON.stringify(msg));
1361
1467
  logger.info(`Permission verdict [${msg.requestId}]: ${msg.behavior} -> session ${msg.sessionId}`);
1362
1468
  }
@@ -1374,7 +1480,7 @@ var init_server = __esm({
1374
1480
  broadcastToDashboards(msg) {
1375
1481
  const payload = JSON.stringify(msg);
1376
1482
  for (const ws of this.dashboardSockets) {
1377
- if (ws.readyState === WebSocket.OPEN) {
1483
+ if (ws.readyState === WebSocket2.OPEN) {
1378
1484
  ws.send(payload);
1379
1485
  }
1380
1486
  }
@@ -1386,7 +1492,7 @@ var init_server = __esm({
1386
1492
  return;
1387
1493
  }
1388
1494
  const channelWs = this.channelSockets.get(sessionId2);
1389
- if (!channelWs || channelWs.readyState !== WebSocket.OPEN) return;
1495
+ if (!channelWs || channelWs.readyState !== WebSocket2.OPEN) return;
1390
1496
  const allowedTypes = ["image/png", "image/jpeg", "image/gif", "image/webp"];
1391
1497
  if (!allowedTypes.includes(mimeType)) return;
1392
1498
  const base64Data = imageData.replace(/^data:image\/\w+;base64,/, "");
@@ -1395,11 +1501,11 @@ var init_server = __esm({
1395
1501
  logger.warn("Image upload rejected: exceeds 10MB");
1396
1502
  return;
1397
1503
  }
1398
- fs4.mkdirSync(UPLOADS_DIR, { recursive: true });
1504
+ fs6.mkdirSync(UPLOADS_DIR, { recursive: true });
1399
1505
  const ext = mimeType.split("/")[1] === "jpeg" ? "jpg" : mimeType.split("/")[1];
1400
1506
  const filename = `${randomUUID3()}.${ext}`;
1401
- const filePath = path4.join(UPLOADS_DIR, filename);
1402
- fs4.writeFileSync(filePath, buffer);
1507
+ const filePath = path6.join(UPLOADS_DIR, filename);
1508
+ fs6.writeFileSync(filePath, buffer);
1403
1509
  const forwardMsg = {
1404
1510
  type: "image_to_session",
1405
1511
  sessionId: sessionId2,
@@ -1412,7 +1518,7 @@ var init_server = __esm({
1412
1518
  logger.info(`Image saved and forwarded: ${filename} (${buffer.length} bytes)`);
1413
1519
  setTimeout(() => {
1414
1520
  try {
1415
- fs4.unlinkSync(filePath);
1521
+ fs6.unlinkSync(filePath);
1416
1522
  } catch {
1417
1523
  }
1418
1524
  }, 5 * 60 * 1e3);
@@ -1439,15 +1545,15 @@ var init_server = __esm({
1439
1545
  this.telegramBot.getSessions = () => this.sessions.getAll();
1440
1546
  this.telegramBot.onMessageToSession = (sessionId2, content) => {
1441
1547
  const channelWs = this.channelSockets.get(sessionId2);
1442
- if (channelWs?.readyState === WebSocket.OPEN) {
1443
- const msg = { type: "message_to_session", sessionId: sessionId2, content };
1548
+ if (channelWs?.readyState === WebSocket2.OPEN) {
1549
+ const msg = { type: "message_to_session", sessionId: sessionId2, content, source: "telegram" };
1444
1550
  channelWs.send(JSON.stringify(msg));
1445
1551
  logger.info(`Telegram message forwarded to session: ${sessionId2}`);
1446
1552
  }
1447
1553
  };
1448
1554
  this.telegramBot.onImageToSession = (sessionId2, imagePath, mimeType, caption) => {
1449
1555
  const channelWs = this.channelSockets.get(sessionId2);
1450
- if (channelWs?.readyState === WebSocket.OPEN) {
1556
+ if (channelWs?.readyState === WebSocket2.OPEN) {
1451
1557
  const msg = { type: "image_to_session", sessionId: sessionId2, imagePath, mimeType, content: caption };
1452
1558
  channelWs.send(JSON.stringify(msg));
1453
1559
  logger.info(`Telegram photo forwarded to session: ${sessionId2}`);
@@ -1455,7 +1561,7 @@ var init_server = __esm({
1455
1561
  };
1456
1562
  this.telegramBot.onPermissionVerdict = (sessionId2, requestId, behavior) => {
1457
1563
  const channelWs = this.channelSockets.get(sessionId2);
1458
- if (channelWs?.readyState === WebSocket.OPEN) {
1564
+ if (channelWs?.readyState === WebSocket2.OPEN) {
1459
1565
  const msg = { type: "permission_response", sessionId: sessionId2, requestId, behavior };
1460
1566
  channelWs.send(JSON.stringify(msg));
1461
1567
  logger.info(`Telegram permission verdict [${requestId}]: ${behavior} -> session ${sessionId2}`);
@@ -1567,6 +1673,17 @@ var init_server = __esm({
1567
1673
  this.jsonResponse(res, 500, { error: err.message });
1568
1674
  }
1569
1675
  }
1676
+ startCodexAdapter() {
1677
+ if (!this.codexEnabled) return;
1678
+ const script = resolveAdapterScript(__dirname);
1679
+ if (!script) {
1680
+ logger.warn("Codex adapter is enabled but codex/main.js was not found next to the hub");
1681
+ return;
1682
+ }
1683
+ this.codexSupervisor = new CodexSupervisor(script);
1684
+ this.codexSupervisor.start();
1685
+ logger.info("Codex adapter started");
1686
+ }
1570
1687
  startHeartbeat() {
1571
1688
  this.heartbeatInterval = setInterval(() => {
1572
1689
  for (const [sessionId2, ws] of this.channelSockets) {
@@ -1582,11 +1699,11 @@ var init_server = __esm({
1582
1699
  }
1583
1700
  cleanupUploads() {
1584
1701
  try {
1585
- if (!fs4.existsSync(UPLOADS_DIR)) return;
1586
- const files = fs4.readdirSync(UPLOADS_DIR);
1702
+ if (!fs6.existsSync(UPLOADS_DIR)) return;
1703
+ const files = fs6.readdirSync(UPLOADS_DIR);
1587
1704
  for (const file of files) {
1588
1705
  try {
1589
- fs4.unlinkSync(path4.join(UPLOADS_DIR, file));
1706
+ fs6.unlinkSync(path6.join(UPLOADS_DIR, file));
1590
1707
  } catch {
1591
1708
  }
1592
1709
  }
@@ -1595,8 +1712,7 @@ var init_server = __esm({
1595
1712
  }
1596
1713
  }
1597
1714
  getSessionLabel(session) {
1598
- if (!session) return "unknown";
1599
- return session.displayName || session.cwd?.replace(/^.*[/\\]/, "") || session.name;
1715
+ return session ? sessionLabel(session) : "unknown";
1600
1716
  }
1601
1717
  jsonResponse(res, status, body) {
1602
1718
  res.writeHead(status, { "Content-Type": "application/json" });
@@ -1660,7 +1776,7 @@ Content-Length: 0\r
1660
1776
  });
1661
1777
 
1662
1778
  // src/channel/hub-client.ts
1663
- import WebSocket2 from "ws";
1779
+ import WebSocket3 from "ws";
1664
1780
  var HubClient;
1665
1781
  var init_hub_client = __esm({
1666
1782
  "src/channel/hub-client.ts"() {
@@ -1668,48 +1784,41 @@ var init_hub_client = __esm({
1668
1784
  init_logger();
1669
1785
  init_constants();
1670
1786
  HubClient = class {
1671
- constructor(sessionId2, sessionName2, hubHost2 = DEFAULT_HUB_HOST, hubPort2 = DEFAULT_HUB_PORT, token, getPeerName = () => void 0) {
1787
+ constructor(sessionId2, sessionName2, hubHost2 = DEFAULT_HUB_HOST, hubPort2 = DEFAULT_HUB_PORT, token, getPeerName = () => void 0, getRegistration = () => ({})) {
1672
1788
  this.sessionId = sessionId2;
1673
1789
  this.sessionName = sessionName2;
1674
1790
  this.hubHost = hubHost2;
1675
1791
  this.hubPort = hubPort2;
1676
1792
  this.token = token;
1677
1793
  this.getPeerName = getPeerName;
1794
+ this.getRegistration = getRegistration;
1678
1795
  }
1679
1796
  ws = null;
1680
1797
  reconnectTimer = null;
1681
1798
  messageHandlers = [];
1682
1799
  queue = [];
1683
1800
  connected = false;
1801
+ closed = false;
1684
1802
  connect() {
1803
+ this.closed = false;
1685
1804
  const tokenQuery = this.token ? `?token=${encodeURIComponent(this.token)}` : "";
1686
1805
  const url = `ws://${this.hubHost}:${this.hubPort}${WS_PATH_CHANNEL}${tokenQuery}`;
1687
1806
  logger.debug(`Connecting to hub at ${url}`);
1688
1807
  try {
1689
- this.ws = new WebSocket2(url);
1690
- this.ws.on("open", () => {
1808
+ const ws = new WebSocket3(url);
1809
+ this.ws = ws;
1810
+ ws.on("open", () => {
1811
+ if (this.ws !== ws) return;
1691
1812
  logger.info("Connected to hub");
1692
1813
  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));
1814
+ this.ws.send(JSON.stringify(this.registration()));
1707
1815
  for (const msg of this.queue) {
1708
1816
  this.ws.send(JSON.stringify(msg));
1709
1817
  }
1710
1818
  this.queue = [];
1711
1819
  });
1712
- this.ws.on("message", (data) => {
1820
+ ws.on("message", (data) => {
1821
+ if (this.ws !== ws) return;
1713
1822
  try {
1714
1823
  const msg = JSON.parse(data.toString());
1715
1824
  for (const handler of this.messageHandlers) {
@@ -1719,12 +1828,14 @@ var init_hub_client = __esm({
1719
1828
  logger.warn("Failed to parse hub message:", err);
1720
1829
  }
1721
1830
  });
1722
- this.ws.on("close", () => {
1831
+ ws.on("close", () => {
1832
+ if (this.ws !== ws) return;
1723
1833
  logger.info("Disconnected from hub");
1724
1834
  this.connected = false;
1725
- this.scheduleReconnect();
1835
+ if (!this.closed) this.scheduleReconnect();
1726
1836
  });
1727
- this.ws.on("error", (err) => {
1837
+ ws.on("error", (err) => {
1838
+ if (this.ws !== ws) return;
1728
1839
  logger.debug(`Hub connection error: ${err.message}`);
1729
1840
  this.connected = false;
1730
1841
  });
@@ -1734,7 +1845,7 @@ var init_hub_client = __esm({
1734
1845
  }
1735
1846
  }
1736
1847
  send(msg) {
1737
- if (this.connected && this.ws?.readyState === WebSocket2.OPEN) {
1848
+ if (this.connected && this.ws?.readyState === WebSocket3.OPEN) {
1738
1849
  this.ws.send(JSON.stringify(msg));
1739
1850
  } else {
1740
1851
  if (this.queue.length < 100) {
@@ -1747,6 +1858,7 @@ var init_hub_client = __esm({
1747
1858
  this.messageHandlers.push(handler);
1748
1859
  }
1749
1860
  disconnect() {
1861
+ this.closed = true;
1750
1862
  if (this.reconnectTimer) {
1751
1863
  clearTimeout(this.reconnectTimer);
1752
1864
  this.reconnectTimer = null;
@@ -1757,8 +1869,29 @@ var init_hub_client = __esm({
1757
1869
  }
1758
1870
  this.connected = false;
1759
1871
  }
1872
+ reregister() {
1873
+ if (this.connected && this.ws?.readyState === WebSocket3.OPEN) {
1874
+ this.ws.send(JSON.stringify(this.registration()));
1875
+ }
1876
+ }
1877
+ registration() {
1878
+ return {
1879
+ type: "register",
1880
+ session: {
1881
+ id: this.sessionId,
1882
+ name: this.sessionName,
1883
+ status: "idle",
1884
+ connectedAt: Date.now(),
1885
+ lastActivity: Date.now(),
1886
+ cwd: process.cwd(),
1887
+ channelEnabled: true,
1888
+ peerName: this.getPeerName(),
1889
+ ...this.getRegistration()
1890
+ }
1891
+ };
1892
+ }
1760
1893
  scheduleReconnect() {
1761
- if (this.reconnectTimer) return;
1894
+ if (this.reconnectTimer || this.closed) return;
1762
1895
  this.reconnectTimer = setTimeout(() => {
1763
1896
  this.reconnectTimer = null;
1764
1897
  this.connect();
@@ -1769,9 +1902,9 @@ var init_hub_client = __esm({
1769
1902
  });
1770
1903
 
1771
1904
  // src/channel/peer-name.ts
1772
- import fs5 from "fs";
1905
+ import fs7 from "fs";
1773
1906
  import os2 from "os";
1774
- import path5 from "path";
1907
+ import path7 from "path";
1775
1908
  function findPeerName(records, env) {
1776
1909
  const valid = records.filter((r) => typeof r === "object" && r !== null);
1777
1910
  const nameOf = (match) => {
@@ -1791,18 +1924,18 @@ function readPeerName(env = process.env) {
1791
1924
  sessionId: env.CLAUDE_CODE_SESSION_ID
1792
1925
  };
1793
1926
  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");
1927
+ const configDir = env.CLAUDE_CONFIG_DIR || path7.join(os2.homedir(), ".claude");
1928
+ const dir = path7.join(configDir, "sessions");
1796
1929
  let files;
1797
1930
  try {
1798
- files = fs5.readdirSync(dir).filter((f) => f.endsWith(".json"));
1931
+ files = fs7.readdirSync(dir).filter((f) => f.endsWith(".json"));
1799
1932
  } catch {
1800
1933
  return void 0;
1801
1934
  }
1802
1935
  const records = [];
1803
1936
  for (const f of files) {
1804
1937
  try {
1805
- records.push(JSON.parse(fs5.readFileSync(path5.join(dir, f), "utf8")));
1938
+ records.push(JSON.parse(fs7.readFileSync(path7.join(dir, f), "utf8")));
1806
1939
  } catch {
1807
1940
  }
1808
1941
  }
@@ -1825,7 +1958,7 @@ import {
1825
1958
  } from "@modelcontextprotocol/sdk/types.js";
1826
1959
  import { z } from "zod";
1827
1960
  import { randomUUID as randomUUID4 } from "crypto";
1828
- import path6 from "path";
1961
+ import path8 from "path";
1829
1962
  async function main() {
1830
1963
  logger.info(`Starting MCP channel server (session: ${sessionId})`);
1831
1964
  hubClient.connect();
@@ -1894,7 +2027,7 @@ var init_server2 = __esm({
1894
2027
  init_hub_client();
1895
2028
  init_peer_name2();
1896
2029
  sessionId = randomUUID4();
1897
- sessionName = process.env.CLAUDE_ALARM_SESSION_NAME ?? path6.basename(process.cwd());
2030
+ sessionName = process.env.CLAUDE_ALARM_SESSION_NAME ?? path8.basename(process.cwd());
1898
2031
  peerName = readPeerName();
1899
2032
  server = new Server(
1900
2033
  {
@@ -2070,15 +2203,35 @@ NOTIFICATIONS:
2070
2203
 
2071
2204
  // src/cli.ts
2072
2205
  init_config();
2206
+ import { spawn as spawn3 } from "child_process";
2207
+ import fs8 from "fs";
2208
+ import path9 from "path";
2209
+ import readline from "readline";
2210
+ import { fileURLToPath as fileURLToPath2 } from "url";
2211
+
2212
+ // src/codex/transport.ts
2213
+ init_logger();
2214
+ import { spawn } from "child_process";
2215
+ import fs2 from "fs";
2216
+ import path3 from "path";
2217
+ import { Duplex } from "stream";
2218
+ import WebSocket from "ws";
2219
+ function findOnPath(command, platform = process.platform, env = process.env) {
2220
+ const dirs = (env.PATH ?? env.Path ?? "").split(path3.delimiter).filter(Boolean);
2221
+ for (const ext of platform === "win32" ? [".exe", ".cmd"] : [""]) {
2222
+ for (const dir of dirs) {
2223
+ const file = path3.join(dir, command + ext);
2224
+ if (fs2.existsSync(file)) return file;
2225
+ }
2226
+ }
2227
+ return void 0;
2228
+ }
2229
+
2230
+ // src/cli.ts
2073
2231
  init_constants();
2074
2232
  init_logger();
2075
2233
  init_crash_guard();
2076
- import { spawn } from "child_process";
2077
- import fs6 from "fs";
2078
- import path7 from "path";
2079
- import readline from "readline";
2080
- import { fileURLToPath as fileURLToPath2 } from "url";
2081
- var __dirname2 = path7.dirname(fileURLToPath2(import.meta.url));
2234
+ var __dirname2 = path9.dirname(fileURLToPath2(import.meta.url));
2082
2235
  function authHeaders(token) {
2083
2236
  return token ? { Authorization: `Bearer ${token}` } : {};
2084
2237
  }
@@ -2097,6 +2250,11 @@ Usage:
2097
2250
  claude-alarm setup [dir] Add claude-alarm to .mcp.json
2098
2251
  claude-alarm test Send a test notification
2099
2252
  claude-alarm token Show current auth token
2253
+ claude-alarm codex enable Start the Codex adapter together with the hub
2254
+ claude-alarm codex disable Stop starting the Codex adapter with the hub
2255
+ claude-alarm codex start Run the Codex adapter on its own (e.g. Codex on another PC)
2256
+ claude-alarm codex stop Stop a running Codex adapter
2257
+ claude-alarm codex status Show Codex adapter status
2100
2258
  claude-alarm help Show this help
2101
2259
 
2102
2260
  Quick start:
@@ -2105,8 +2263,8 @@ Quick start:
2105
2263
  }
2106
2264
  async function checkForUpdates() {
2107
2265
  try {
2108
- const pkgPath = path7.join(__dirname2, "..", "package.json");
2109
- const pkg = JSON.parse(fs6.readFileSync(pkgPath, "utf-8"));
2266
+ const pkgPath = path9.join(__dirname2, "..", "package.json");
2267
+ const pkg = JSON.parse(fs8.readFileSync(pkgPath, "utf-8"));
2110
2268
  const currentVersion = pkg.version;
2111
2269
  const res = await fetch("https://registry.npmjs.org/@delt/claude-alarm/latest", {
2112
2270
  signal: AbortSignal.timeout(3e3)
@@ -2129,25 +2287,25 @@ async function hubStart(daemon) {
2129
2287
  const port = config2.hub.port ?? DEFAULT_HUB_PORT;
2130
2288
  const displayHost = host === "0.0.0.0" ? "127.0.0.1" : host;
2131
2289
  checkForUpdates();
2132
- if (fs6.existsSync(PID_FILE)) {
2133
- const pid = parseInt(fs6.readFileSync(PID_FILE, "utf-8").trim(), 10);
2290
+ if (fs8.existsSync(PID_FILE)) {
2291
+ const pid = parseInt(fs8.readFileSync(PID_FILE, "utf-8").trim(), 10);
2134
2292
  if (isProcessRunning(pid)) {
2135
2293
  console.log(`Hub is already running (PID: ${pid}). Dashboard: ${loginLink(displayHost, port, config2.hub.token)}`);
2136
2294
  return;
2137
2295
  }
2138
- fs6.unlinkSync(PID_FILE);
2296
+ fs8.unlinkSync(PID_FILE);
2139
2297
  }
2140
2298
  if (daemon) {
2141
2299
  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], {
2300
+ const logFd = fs8.openSync(LOG_FILE, "a");
2301
+ const hubScript = path9.join(__dirname2, "hub", "server.js");
2302
+ const child = spawn3(process.execPath, [hubScript], {
2145
2303
  detached: true,
2146
2304
  stdio: ["ignore", logFd, logFd],
2147
2305
  env: { ...process.env }
2148
2306
  });
2149
2307
  if (child.pid) {
2150
- fs6.writeFileSync(PID_FILE, String(child.pid), "utf-8");
2308
+ fs8.writeFileSync(PID_FILE, String(child.pid), "utf-8");
2151
2309
  child.unref();
2152
2310
  console.log(`Hub started as daemon (PID: ${child.pid})`);
2153
2311
  console.log(`Dashboard: ${loginLink(displayHost, port, config2.hub.token)}`);
@@ -2168,11 +2326,11 @@ async function hubStart(daemon) {
2168
2326
  const hub = new HubServer2(config2);
2169
2327
  await hub.start();
2170
2328
  ensureConfigDir();
2171
- fs6.writeFileSync(PID_FILE, String(process.pid), "utf-8");
2329
+ fs8.writeFileSync(PID_FILE, String(process.pid), "utf-8");
2172
2330
  const shutdown = async () => {
2173
2331
  console.log("\nShutting down...");
2174
2332
  await hub.stop();
2175
- if (fs6.existsSync(PID_FILE)) fs6.unlinkSync(PID_FILE);
2333
+ if (fs8.existsSync(PID_FILE)) fs8.unlinkSync(PID_FILE);
2176
2334
  process.exit(0);
2177
2335
  };
2178
2336
  process.on("SIGINT", shutdown);
@@ -2180,18 +2338,18 @@ async function hubStart(daemon) {
2180
2338
  }
2181
2339
  }
2182
2340
  function hubStop() {
2183
- if (!fs6.existsSync(PID_FILE)) {
2341
+ if (!fs8.existsSync(PID_FILE)) {
2184
2342
  console.log("Hub is not running (no PID file found)");
2185
2343
  return;
2186
2344
  }
2187
- const pid = parseInt(fs6.readFileSync(PID_FILE, "utf-8").trim(), 10);
2345
+ const pid = parseInt(fs8.readFileSync(PID_FILE, "utf-8").trim(), 10);
2188
2346
  try {
2189
2347
  process.kill(pid, "SIGTERM");
2190
2348
  console.log(`Hub stopped (PID: ${pid})`);
2191
2349
  } catch {
2192
2350
  console.log("Hub process not found (may have already stopped)");
2193
2351
  }
2194
- fs6.unlinkSync(PID_FILE);
2352
+ fs8.unlinkSync(PID_FILE);
2195
2353
  }
2196
2354
  async function hubStatus() {
2197
2355
  const config2 = loadConfig();
@@ -2199,8 +2357,8 @@ async function hubStatus() {
2199
2357
  const port = config2.hub.port ?? DEFAULT_HUB_PORT;
2200
2358
  const displayHost = host === "0.0.0.0" ? "127.0.0.1" : host;
2201
2359
  let pidInfo = "not running";
2202
- if (fs6.existsSync(PID_FILE)) {
2203
- const pid = parseInt(fs6.readFileSync(PID_FILE, "utf-8").trim(), 10);
2360
+ if (fs8.existsSync(PID_FILE)) {
2361
+ const pid = parseInt(fs8.readFileSync(PID_FILE, "utf-8").trim(), 10);
2204
2362
  if (isProcessRunning(pid)) {
2205
2363
  pidInfo = `running (PID: ${pid})`;
2206
2364
  } else {
@@ -2272,7 +2430,7 @@ function ask(question) {
2272
2430
  }
2273
2431
  async function init() {
2274
2432
  const dir = process.cwd();
2275
- const projectName = path7.basename(dir);
2433
+ const projectName = path9.basename(dir);
2276
2434
  console.log(`
2277
2435
  claude-alarm init for "${projectName}"
2278
2436
  `);
@@ -2292,11 +2450,11 @@ claude-alarm init for "${projectName}"
2292
2450
  if (port) env.CLAUDE_ALARM_HUB_PORT = port;
2293
2451
  if (token) env.CLAUDE_ALARM_HUB_TOKEN = token;
2294
2452
  }
2295
- const mcpPath = path7.join(dir, ".mcp.json");
2453
+ const mcpPath = path9.join(dir, ".mcp.json");
2296
2454
  let mcpConfig = {};
2297
- if (fs6.existsSync(mcpPath)) {
2455
+ if (fs8.existsSync(mcpPath)) {
2298
2456
  try {
2299
- mcpConfig = JSON.parse(fs6.readFileSync(mcpPath, "utf-8"));
2457
+ mcpConfig = JSON.parse(fs8.readFileSync(mcpPath, "utf-8"));
2300
2458
  } catch {
2301
2459
  mcpConfig = {};
2302
2460
  }
@@ -2307,7 +2465,7 @@ claude-alarm init for "${projectName}"
2307
2465
  args: ["-y", "@delt/claude-alarm", "serve"],
2308
2466
  env
2309
2467
  };
2310
- fs6.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2), "utf-8");
2468
+ fs8.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2), "utf-8");
2311
2469
  console.log(`
2312
2470
  \u2713 Created ${mcpPath}`);
2313
2471
  if (remote.toLowerCase() !== "y") {
@@ -2328,6 +2486,11 @@ claude-alarm init for "${projectName}"
2328
2486
  console.log(` claude-alarm hub start`);
2329
2487
  }
2330
2488
  console.log(` Dashboard: http://${displayHost}:${port}`);
2489
+ if (shouldOfferCodex(config2, findOnPath("codex") !== void 0)) {
2490
+ const answer = await ask("\nCodex is installed. Show Codex conversations on the dashboard too? (y/N): ");
2491
+ if (answer.toLowerCase() === "y") codexEnable(true);
2492
+ else setCodexEnabled(false);
2493
+ }
2331
2494
  }
2332
2495
  console.log(`
2333
2496
  Next step:`);
@@ -2352,6 +2515,54 @@ function isProcessRunning(pid) {
2352
2515
  return false;
2353
2516
  }
2354
2517
  }
2518
+ function readCodexPid() {
2519
+ if (!fs8.existsSync(CODEX_PID_FILE)) return void 0;
2520
+ const pid = parseInt(fs8.readFileSync(CODEX_PID_FILE, "utf-8").trim(), 10);
2521
+ return Number.isNaN(pid) ? void 0 : pid;
2522
+ }
2523
+ function codexEnable(enabled) {
2524
+ setCodexEnabled(enabled);
2525
+ 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.");
2526
+ }
2527
+ function codexStart() {
2528
+ const pid = readCodexPid();
2529
+ if (pid !== void 0 && isProcessRunning(pid)) {
2530
+ console.log(`Codex adapter is already running (PID: ${pid})`);
2531
+ return;
2532
+ }
2533
+ ensureConfigDir();
2534
+ const logFd = fs8.openSync(CODEX_LOG_FILE, "a");
2535
+ const child = spawn3(process.execPath, [path9.join(__dirname2, "codex", "main.js")], {
2536
+ detached: true,
2537
+ stdio: ["ignore", logFd, logFd],
2538
+ env: { ...process.env }
2539
+ });
2540
+ child.unref();
2541
+ console.log(`Codex adapter started (PID: ${child.pid}). Logs: ${CODEX_LOG_FILE}`);
2542
+ }
2543
+ function codexStop() {
2544
+ const pid = readCodexPid();
2545
+ if (pid === void 0) {
2546
+ console.log("Codex adapter is not running");
2547
+ return;
2548
+ }
2549
+ try {
2550
+ process.kill(pid, "SIGTERM");
2551
+ console.log(`Codex adapter stopped (PID: ${pid})`);
2552
+ } catch {
2553
+ console.log("Codex adapter process not found (may have already stopped)");
2554
+ }
2555
+ try {
2556
+ fs8.unlinkSync(CODEX_PID_FILE);
2557
+ } catch {
2558
+ }
2559
+ }
2560
+ function codexStatus() {
2561
+ const pid = readCodexPid();
2562
+ const state = pid === void 0 ? "not running" : isProcessRunning(pid) ? `running (PID: ${pid})` : "not running (stale PID file)";
2563
+ console.log(`Codex adapter: ${state}`);
2564
+ console.log(`Start with hub: ${loadConfig().codex?.enabled ? "enabled" : "disabled"}`);
2565
+ }
2355
2566
  async function main2() {
2356
2567
  const args = process.argv.slice(2);
2357
2568
  const cmd = args[0];
@@ -2383,6 +2594,19 @@ async function main2() {
2383
2594
  }
2384
2595
  return;
2385
2596
  }
2597
+ if (cmd === "codex") {
2598
+ if (sub === "enable") codexEnable(true);
2599
+ else if (sub === "disable") codexEnable(false);
2600
+ else if (sub === "start") codexStart();
2601
+ else if (sub === "stop") codexStop();
2602
+ else if (sub === "status") codexStatus();
2603
+ else {
2604
+ console.error(`Unknown codex command: ${sub}`);
2605
+ printUsage();
2606
+ process.exit(1);
2607
+ }
2608
+ return;
2609
+ }
2386
2610
  if (cmd === "setup") {
2387
2611
  setup(args[1]);
2388
2612
  return;