@delt/claude-alarm 1.1.1 → 1.3.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, CODEX_PID_FILE, CODEX_LOG_FILE, 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, WS_PATH_CODEX, CHANNEL_SERVER_NAME, CHANNEL_SERVER_VERSION;
16
16
  var init_constants = __esm({
17
17
  "src/shared/constants.ts"() {
18
18
  "use strict";
@@ -27,6 +27,7 @@ var init_constants = __esm({
27
27
  CODEX_LOG_FILE = path.join(CONFIG_DIR, "codex.log");
28
28
  WS_PATH_CHANNEL = "/ws/channel";
29
29
  WS_PATH_DASHBOARD = "/ws/dashboard";
30
+ WS_PATH_CODEX = "/ws/codex";
30
31
  CHANNEL_SERVER_NAME = "claude-alarm";
31
32
  CHANNEL_SERVER_VERSION = "0.1.0";
32
33
  }
@@ -201,6 +202,19 @@ var init_crash_guard = __esm({
201
202
  }
202
203
  });
203
204
 
205
+ // src/shared/hub-url.ts
206
+ import net from "net";
207
+ function hubUrlHost(host) {
208
+ if (host === "" || host === "0.0.0.0") return "127.0.0.1";
209
+ if (host === "::") return "[::1]";
210
+ return net.isIPv6(host) ? `[${host}]` : host;
211
+ }
212
+ var init_hub_url = __esm({
213
+ "src/shared/hub-url.ts"() {
214
+ "use strict";
215
+ }
216
+ });
217
+
204
218
  // src/shared/peer-name.ts
205
219
  function isValidPeerName(name) {
206
220
  return typeof name === "string" && name.trim() !== "" && SAFE_PEER_NAME.test(name);
@@ -351,6 +365,7 @@ var init_notifier = __esm({
351
365
  execFile(
352
366
  "powershell",
353
367
  ["-Command", '(Get-ItemProperty -Path "HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\PushNotifications" -Name ToastEnabled -ErrorAction SilentlyContinue).ToastEnabled'],
368
+ { windowsHide: true },
354
369
  (err, stdout) => {
355
370
  if (err) {
356
371
  resolve(true);
@@ -368,7 +383,7 @@ var init_notifier = __esm({
368
383
  logger.warn("Windows notifications are disabled. Opening notification settings...");
369
384
  logger.warn("Please enable notifications for this app, then try again.");
370
385
  if (process.platform === "win32") {
371
- execFile("powershell", ["-Command", "Start-Process ms-settings:notifications"]);
386
+ execFile("powershell", ["-Command", "Start-Process ms-settings:notifications"], { windowsHide: true });
372
387
  }
373
388
  setTimeout(() => {
374
389
  this.notificationSettingsOpened = false;
@@ -424,10 +439,13 @@ var init_permission_key = __esm({
424
439
  });
425
440
 
426
441
  // src/hub/telegram.ts
427
- import fs4 from "fs";
428
- import path4 from "path";
442
+ import fs5 from "fs";
443
+ import path6 from "path";
429
444
  import { randomUUID as randomUUID2 } from "crypto";
430
- var TELEGRAM_API, MAX_CHOICE_MESSAGES, TelegramBot;
445
+ function visibleLength(html) {
446
+ return html.replace(/<[^>]*>/g, "").replace(/&(?:amp|lt|gt);/g, "&").length;
447
+ }
448
+ var TELEGRAM_API, MAX_CHOICE_MESSAGES, MAX_SELECTIONS, MAX_VISIBLE_CHARS, TRUNCATED, MAX_PHOTO_BYTES, TelegramBot;
431
449
  var init_telegram = __esm({
432
450
  "src/hub/telegram.ts"() {
433
451
  "use strict";
@@ -437,6 +455,10 @@ var init_telegram = __esm({
437
455
  init_permission_key();
438
456
  TELEGRAM_API = "https://api.telegram.org/bot";
439
457
  MAX_CHOICE_MESSAGES = 200;
458
+ MAX_SELECTIONS = 20;
459
+ MAX_VISIBLE_CHARS = 4e3;
460
+ TRUNCATED = "\u2026(truncated)";
461
+ MAX_PHOTO_BYTES = 10 * 1024 * 1024;
440
462
  TelegramBot = class _TelegramBot {
441
463
  config;
442
464
  offset = 0;
@@ -457,9 +479,9 @@ var init_telegram = __esm({
457
479
  choiceMessages = /* @__PURE__ */ new Map();
458
480
  // Callback: get current sessions list
459
481
  getSessions;
460
- // Pending messages for session selection
461
- pendingMessages = /* @__PURE__ */ new Map();
462
- // chatId -> pending
482
+ latestSelection;
483
+ // Keyed by prompt, not by chat: buttons from an older prompt must still send that prompt's message.
484
+ selections = /* @__PURE__ */ new Map();
463
485
  constructor(config2) {
464
486
  this.config = config2;
465
487
  }
@@ -468,8 +490,7 @@ var init_telegram = __esm({
468
490
  }
469
491
  /** Send a notification message to Telegram */
470
492
  async sendNotification(sessionId2, _sessionLabel, title, message) {
471
- const text = `<b>${this.escHtml(title)}</b>
472
- ${this.mdToHtml(message)}`;
493
+ const text = this.fitNotification(title, message);
473
494
  const result = await this.sendMessage(text);
474
495
  if (result?.message_id) {
475
496
  this.messageSessionMap.set(result.message_id, sessionId2);
@@ -481,6 +502,25 @@ ${this.mdToHtml(message)}`;
481
502
  }
482
503
  }
483
504
  }
505
+ fitNotification(title, message) {
506
+ const render = (body) => `<b>${this.escHtml(title)}</b>
507
+ ${this.mdToHtml(body)}`;
508
+ const full = render(message);
509
+ if (visibleLength(full) <= MAX_VISIBLE_CHARS) return full;
510
+ const cut = (n) => {
511
+ const end = n > 0 && /[\uD800-\uDBFF]/.test(message[n - 1]) ? n - 1 : n;
512
+ return render(`${message.slice(0, end)}
513
+ ${TRUNCATED}`);
514
+ };
515
+ let lo = 0;
516
+ let hi = message.length;
517
+ while (lo < hi) {
518
+ const mid = Math.ceil((lo + hi) / 2);
519
+ if (visibleLength(cut(mid)) <= MAX_VISIBLE_CHARS) lo = mid;
520
+ else hi = mid - 1;
521
+ }
522
+ return cut(lo);
523
+ }
484
524
  /** Send a text message to the configured chat */
485
525
  async sendMessage(text, replyToMessageId, replyMarkup) {
486
526
  try {
@@ -562,6 +602,13 @@ ${this.mdToHtml(message)}`;
562
602
  const hasPhoto = msg.photo && msg.photo.length > 0;
563
603
  const text = (msg.text || msg.caption || "").trim();
564
604
  if (!text && !hasPhoto) return;
605
+ if (hasPhoto) {
606
+ const size = msg.photo[msg.photo.length - 1].file_size;
607
+ if (size !== void 0 && size > MAX_PHOTO_BYTES) {
608
+ this.photoNotDelivered("it is larger than 10 MB");
609
+ return;
610
+ }
611
+ }
565
612
  if (msg.reply_to_message) {
566
613
  const sessionId2 = this.messageSessionMap.get(msg.reply_to_message.message_id);
567
614
  if (sessionId2) {
@@ -575,18 +622,20 @@ ${this.mdToHtml(message)}`;
575
622
  }
576
623
  if (text) {
577
624
  const selectMatch = text.match(/^\/s_(\d+)$/);
578
- if (selectMatch) {
579
- const pending = this.pendingMessages.get(msg.chat.id);
625
+ const latest = this.latestSelection;
626
+ if (selectMatch && latest !== void 0) {
627
+ const pending = this.selections.get(latest);
580
628
  if (pending) {
581
- this.pendingMessages.delete(msg.chat.id);
629
+ this.selections.delete(latest);
582
630
  const session = this.pendingSession(pending, parseInt(selectMatch[1], 10) - 1);
583
631
  if (session) {
632
+ let delivered = true;
584
633
  if (pending.photoFileId) {
585
- await this.deliverPhotoToSessionByFileId(session.id, pending.photoFileId, pending.caption);
634
+ delivered = await this.deliverPhotoToSessionByFileId(session.id, pending.photoFileId, pending.caption);
586
635
  } else if (pending.text) {
587
636
  this.deliverToSession(session.id, pending.text);
588
637
  }
589
- this.sendMessage(`Sent to [${this.getLabel(session)}]`);
638
+ if (delivered) this.sendMessage(`Sent to [${this.getLabel(session)}]`);
590
639
  } else {
591
640
  this.sendMessage("Invalid session number.");
592
641
  }
@@ -607,16 +656,19 @@ ${this.mdToHtml(message)}`;
607
656
  }
608
657
  return;
609
658
  }
659
+ const selectionId = randomUUID2().replace(/-/g, "").slice(0, 8);
660
+ this.latestSelection = selectionId;
610
661
  const sessionIds = sessions.map((s) => s.id);
611
662
  if (hasPhoto) {
612
663
  const largest = msg.photo[msg.photo.length - 1];
613
- this.pendingMessages.set(msg.chat.id, { photoFileId: largest.file_id, caption: text, sessionIds });
664
+ this.selections.set(selectionId, { photoFileId: largest.file_id, caption: text, sessionIds });
614
665
  } else {
615
- this.pendingMessages.set(msg.chat.id, { text, sessionIds });
666
+ this.selections.set(selectionId, { text, sessionIds });
616
667
  }
668
+ while (this.selections.size > MAX_SELECTIONS) this.selections.delete(this.selections.keys().next().value);
617
669
  const buttons = sessions.map((s, i) => ({
618
670
  text: this.getLabel(s),
619
- callback_data: `sess:${i}:${msg.chat.id}`
671
+ callback_data: `sel:${selectionId}:${i}`
620
672
  }));
621
673
  const rows = [];
622
674
  for (let i = 0; i < buttons.length; i += 2) {
@@ -631,44 +683,54 @@ ${this.mdToHtml(message)}`;
631
683
  }
632
684
  async deliverPhotoToSession(sessionId2, photos, caption) {
633
685
  const largest = photos[photos.length - 1];
634
- await this.deliverPhotoToSessionByFileId(sessionId2, largest.file_id, caption);
686
+ return this.deliverPhotoToSessionByFileId(sessionId2, largest.file_id, caption);
635
687
  }
636
688
  async deliverPhotoToSessionByFileId(sessionId2, fileId, caption) {
637
689
  try {
638
690
  const fileRes = await fetch(`${this.apiUrl}/getFile?file_id=${fileId}`);
639
- if (!fileRes.ok) {
640
- logger.warn("Failed to get Telegram file info");
641
- return;
691
+ const fileData = fileRes.ok ? await fileRes.json() : void 0;
692
+ if (!fileData?.ok || !fileData.result) {
693
+ this.photoNotDelivered("Telegram did not return the file");
694
+ return false;
642
695
  }
643
- const fileData = await fileRes.json();
644
- if (!fileData.ok) return;
645
696
  const downloadUrl = `https://api.telegram.org/file/bot${this.config.botToken}/${fileData.result.file_path}`;
646
697
  const imgRes = await fetch(downloadUrl);
647
698
  if (!imgRes.ok) {
648
- logger.warn("Failed to download Telegram photo");
649
- return;
699
+ this.photoNotDelivered("the download failed");
700
+ return false;
650
701
  }
651
702
  const buffer = Buffer.from(await imgRes.arrayBuffer());
703
+ if (buffer.length > MAX_PHOTO_BYTES) {
704
+ this.photoNotDelivered("it is larger than 10 MB");
705
+ return false;
706
+ }
652
707
  const ext = fileData.result.file_path.split(".").pop() || "jpg";
653
708
  const mimeType = ext === "png" ? "image/png" : ext === "gif" ? "image/gif" : ext === "webp" ? "image/webp" : "image/jpeg";
654
- fs4.mkdirSync(UPLOADS_DIR, { recursive: true });
709
+ fs5.mkdirSync(UPLOADS_DIR, { recursive: true });
655
710
  const filename = `${randomUUID2()}.${ext}`;
656
- const filePath = path4.join(UPLOADS_DIR, filename);
657
- fs4.writeFileSync(filePath, buffer);
711
+ const filePath = path6.join(UPLOADS_DIR, filename);
712
+ fs5.writeFileSync(filePath, buffer);
658
713
  logger.info(`Telegram photo saved: ${filename} (${buffer.length} bytes)`);
659
714
  if (this.onImageToSession) {
660
715
  this.onImageToSession(sessionId2, filePath, mimeType, caption);
661
716
  }
662
717
  setTimeout(() => {
663
718
  try {
664
- fs4.unlinkSync(filePath);
719
+ fs5.unlinkSync(filePath);
665
720
  } catch {
666
721
  }
667
- }, 5 * 60 * 1e3);
722
+ }, 5 * 60 * 1e3).unref();
723
+ return this.onImageToSession !== void 0;
668
724
  } catch (err) {
669
725
  logger.warn(`Telegram photo download failed: ${err.message}`);
726
+ this.photoNotDelivered("the download failed");
727
+ return false;
670
728
  }
671
729
  }
730
+ photoNotDelivered(reason) {
731
+ logger.warn(`Telegram photo not delivered: ${reason}`);
732
+ void this.sendMessage(`Photo not delivered: ${reason}`);
733
+ }
672
734
  // Buttons are numbered against the list shown when they were sent; Codex sessions come and go, so the live list may have shifted.
673
735
  pendingSession(pending, idx) {
674
736
  const id = pending.sessionIds[idx];
@@ -782,7 +844,7 @@ ${this.escHtml(description)}` : "") + (slice ? `
782
844
  async handleChoiceCallback(query) {
783
845
  const choice = this.choiceTokens.get(query.data.slice("pc:".length));
784
846
  if (!choice) {
785
- await this.answerCallbackQuery(query.id, "Expired");
847
+ await this.expire(query);
786
848
  return;
787
849
  }
788
850
  this.dropChoiceTokens(choice.sessionId, choice.requestId);
@@ -811,10 +873,14 @@ ${this.escHtml(description)}` : "") + (slice ? `
811
873
  async handleCallbackQuery(query) {
812
874
  if (!query.data) return;
813
875
  if (String(query.message?.chat.id) !== String(this.config.chatId)) return;
814
- if (query.data.startsWith("sess:")) {
876
+ if (query.data.startsWith("sel:")) {
815
877
  await this.handleSessionSelectCallback(query);
816
878
  return;
817
879
  }
880
+ if (query.data.startsWith("sess:")) {
881
+ await this.expire(query);
882
+ return;
883
+ }
818
884
  if (query.data.startsWith("pc:")) {
819
885
  await this.handleChoiceCallback(query);
820
886
  return;
@@ -837,19 +903,25 @@ ${label}`);
837
903
  }
838
904
  }
839
905
  async handleSessionSelectCallback(query) {
840
- const parts = query.data.split(":");
841
- if (parts.length < 3) return;
842
- const [, idxStr, chatIdStr] = parts;
843
- const chatId = parseInt(chatIdStr, 10);
844
- const pending = this.pendingMessages.get(chatId);
845
- const session = pending ? this.pendingSession(pending, parseInt(idxStr, 10)) : void 0;
846
- if (!pending || !session) {
906
+ const [, selectionId, idxStr] = query.data.split(":");
907
+ const pending = this.selections.get(selectionId);
908
+ if (!pending) {
909
+ await this.expire(query);
910
+ return;
911
+ }
912
+ const session = this.pendingSession(pending, parseInt(idxStr, 10));
913
+ if (!session) {
847
914
  await this.answerCallbackQuery(query.id, "Session not found");
848
915
  return;
849
916
  }
850
- this.pendingMessages.delete(chatId);
917
+ this.selections.delete(selectionId);
851
918
  if (pending.photoFileId) {
852
- await this.deliverPhotoToSessionByFileId(session.id, pending.photoFileId, pending.caption);
919
+ const delivered = await this.deliverPhotoToSessionByFileId(session.id, pending.photoFileId, pending.caption);
920
+ if (!delivered) {
921
+ await this.answerCallbackQuery(query.id, "Photo not delivered");
922
+ if (query.message) await this.removeButtons(query.message.chat.id, query.message.message_id);
923
+ return;
924
+ }
853
925
  } else if (pending.text) {
854
926
  this.deliverToSession(session.id, pending.text);
855
927
  }
@@ -869,6 +941,21 @@ ${label}`);
869
941
  logger.warn(`Telegram answerCallbackQuery error: ${err.message}`);
870
942
  }
871
943
  }
944
+ async expire(query) {
945
+ await this.answerCallbackQuery(query.id, "Expired");
946
+ if (query.message) await this.removeButtons(query.message.chat.id, query.message.message_id);
947
+ }
948
+ async removeButtons(chatId, messageId) {
949
+ try {
950
+ await fetch(`${this.apiUrl}/editMessageReplyMarkup`, {
951
+ method: "POST",
952
+ headers: { "Content-Type": "application/json" },
953
+ body: JSON.stringify({ chat_id: chatId, message_id: messageId, reply_markup: { inline_keyboard: [] } })
954
+ });
955
+ } catch (err) {
956
+ logger.warn(`Telegram editMessageReplyMarkup error: ${err.message}`);
957
+ }
958
+ }
872
959
  async editMessageText(chatId, messageId, text) {
873
960
  try {
874
961
  await fetch(`${this.apiUrl}/editMessageText`, {
@@ -913,8 +1000,8 @@ ${label}`);
913
1000
  css: "css",
914
1001
  scss: "scss"
915
1002
  };
916
- langOf(path10) {
917
- const m = /\.([a-z0-9]+)$/i.exec(path10 || "");
1003
+ langOf(path12) {
1004
+ const m = /\.([a-z0-9]+)$/i.exec(path12 || "");
918
1005
  return m ? _TelegramBot.LANG_BY_EXT[m[1].toLowerCase()] ?? "" : "";
919
1006
  }
920
1007
  /** Convert markdown to Telegram HTML (escape first, then apply formatting) */
@@ -961,10 +1048,15 @@ ${bodyLines.join("\n")}`;
961
1048
 
962
1049
  // src/hub/codex-supervisor.ts
963
1050
  import { spawn as spawn2 } from "child_process";
964
- import fs5 from "fs";
965
- import path5 from "path";
1051
+ import fs6 from "fs";
1052
+ import path7 from "path";
966
1053
  function resolveAdapterScript(baseDir) {
967
- return [path5.join(baseDir, "..", "codex", "main.js"), path5.join(baseDir, "codex", "main.js")].find((p) => fs5.existsSync(p));
1054
+ return [path7.join(baseDir, "..", "codex", "main.js"), path7.join(baseDir, "codex", "main.js")].find((p) => fs6.existsSync(p));
1055
+ }
1056
+ function adapterEnv(base, hub) {
1057
+ const env = { ...base, CLAUDE_ALARM_HUB_HOST: hub.host, CLAUDE_ALARM_HUB_PORT: String(hub.port) };
1058
+ delete env.CLAUDE_ALARM_HUB_TOKEN;
1059
+ return env;
968
1060
  }
969
1061
  var HEALTHY_RUN_MS, CodexSupervisor;
970
1062
  var init_codex_supervisor = __esm({
@@ -973,18 +1065,24 @@ var init_codex_supervisor = __esm({
973
1065
  init_logger();
974
1066
  HEALTHY_RUN_MS = 6e4;
975
1067
  CodexSupervisor = class {
976
- constructor(script, spawnFn = spawn2, minDelayMs = 2e3, maxDelayMs = 6e4, stopGraceMs = 3e3) {
1068
+ constructor(script, opts = {}) {
977
1069
  this.script = script;
978
- this.spawnFn = spawnFn;
979
- this.minDelayMs = minDelayMs;
980
- this.maxDelayMs = maxDelayMs;
981
- this.stopGraceMs = stopGraceMs;
982
- this.delay = minDelayMs;
1070
+ this.spawnFn = opts.spawnFn ?? spawn2;
1071
+ this.minDelayMs = opts.minDelayMs ?? 2e3;
1072
+ this.maxDelayMs = opts.maxDelayMs ?? 6e4;
1073
+ this.stopGraceMs = opts.stopGraceMs ?? 3e3;
1074
+ this.env = opts.env;
1075
+ this.delay = this.minDelayMs;
983
1076
  }
984
1077
  child;
985
1078
  timer;
986
1079
  stopped = true;
987
1080
  delay;
1081
+ spawnFn;
1082
+ minDelayMs;
1083
+ maxDelayMs;
1084
+ stopGraceMs;
1085
+ env;
988
1086
  start() {
989
1087
  this.stopped = false;
990
1088
  this.launch();
@@ -1006,7 +1104,8 @@ var init_codex_supervisor = __esm({
1006
1104
  const child = this.spawnFn(process.execPath, [this.script, "--watch-stdin"], {
1007
1105
  detached: true,
1008
1106
  stdio: ["pipe", "pipe", "pipe"],
1009
- windowsHide: true
1107
+ windowsHide: true,
1108
+ ...this.env ? { env: this.env } : {}
1010
1109
  });
1011
1110
  this.child = child;
1012
1111
  child.stdout?.on("data", (d) => process.stdout.write(d));
@@ -1030,16 +1129,28 @@ var init_codex_supervisor = __esm({
1030
1129
  }
1031
1130
  });
1032
1131
 
1132
+ // src/shared/entry.ts
1133
+ function isEntryScript(argv1, suffixes) {
1134
+ if (!argv1) return false;
1135
+ const normalized = argv1.replace(/\\/g, "/");
1136
+ return suffixes.some((suffix) => normalized.endsWith(suffix));
1137
+ }
1138
+ var init_entry = __esm({
1139
+ "src/shared/entry.ts"() {
1140
+ "use strict";
1141
+ }
1142
+ });
1143
+
1033
1144
  // src/hub/auth.ts
1034
- import crypto from "crypto";
1145
+ import crypto2 from "crypto";
1035
1146
  function safeEqual(a, b) {
1036
1147
  if (typeof a !== "string") return false;
1037
1148
  const x = Buffer.from(a);
1038
1149
  const y = Buffer.from(b);
1039
- return x.length === y.length && crypto.timingSafeEqual(x, y);
1150
+ return x.length === y.length && crypto2.timingSafeEqual(x, y);
1040
1151
  }
1041
1152
  function dashboardCookieValue(token) {
1042
- return crypto.createHmac("sha256", token).update("claude-alarm-dashboard").digest("hex");
1153
+ return crypto2.createHmac("sha256", token).update("claude-alarm-dashboard").digest("hex");
1043
1154
  }
1044
1155
  function parseCookies(header) {
1045
1156
  const out = {};
@@ -1099,8 +1210,8 @@ __export(server_exports, {
1099
1210
  HubServer: () => HubServer
1100
1211
  });
1101
1212
  import http from "http";
1102
- import fs6 from "fs";
1103
- import path6 from "path";
1213
+ import fs7 from "fs";
1214
+ import path8 from "path";
1104
1215
  import { fileURLToPath } from "url";
1105
1216
  import { WebSocketServer, WebSocket as WebSocket2 } from "ws";
1106
1217
  import { randomUUID as randomUUID3 } from "crypto";
@@ -1109,7 +1220,7 @@ function validChoices(choices) {
1109
1220
  const valid = choices.filter((c) => typeof c?.id === "string" && typeof c?.label === "string");
1110
1221
  return valid.length ? valid : void 0;
1111
1222
  }
1112
- var __dirname, MAX_WS_PAYLOAD, HubServer;
1223
+ var __dirname, MAX_WS_PAYLOAD, nonEmpty, HubServer;
1113
1224
  var init_server = __esm({
1114
1225
  "src/hub/server.ts"() {
1115
1226
  "use strict";
@@ -1123,9 +1234,12 @@ var init_server = __esm({
1123
1234
  init_session_label();
1124
1235
  init_crash_guard();
1125
1236
  init_permission_key();
1237
+ init_entry();
1238
+ init_hub_url();
1126
1239
  init_auth();
1127
- __dirname = path6.dirname(fileURLToPath(import.meta.url));
1240
+ __dirname = path8.dirname(fileURLToPath(import.meta.url));
1128
1241
  MAX_WS_PAYLOAD = 16 * 1024 * 1024;
1242
+ nonEmpty = (v) => typeof v === "string" && v.trim() !== "";
1129
1243
  HubServer = class {
1130
1244
  httpServer;
1131
1245
  wssChannel;
@@ -1151,7 +1265,13 @@ var init_server = __esm({
1151
1265
  token;
1152
1266
  codexEnabled;
1153
1267
  codexSupervisor;
1154
- constructor(config2) {
1268
+ wssCodex;
1269
+ codexAdapters = /* @__PURE__ */ new Map();
1270
+ codexAlive = /* @__PURE__ */ new WeakMap();
1271
+ codexCallTimeoutMs;
1272
+ codexCalls = /* @__PURE__ */ new Map();
1273
+ constructor(config2, options = {}) {
1274
+ this.codexCallTimeoutMs = options.codexCallTimeoutMs ?? 6e4;
1155
1275
  this.host = config2?.hub?.host ?? DEFAULT_HUB_HOST;
1156
1276
  this.port = config2?.hub?.port ?? DEFAULT_HUB_PORT;
1157
1277
  this.token = config2?.hub?.token;
@@ -1164,7 +1284,7 @@ var init_server = __esm({
1164
1284
  if (config2?.webhooks) {
1165
1285
  this.notifier.configure({ webhooks: config2.webhooks });
1166
1286
  }
1167
- const displayHost = this.host === "0.0.0.0" ? "127.0.0.1" : this.host;
1287
+ const displayHost = hubUrlHost(this.host);
1168
1288
  this.notifier.configure({ dashboardUrl: `http://${displayHost}:${this.port}` });
1169
1289
  const fullConfig = loadConfig();
1170
1290
  if (fullConfig.telegram?.enabled && fullConfig.telegram.botToken && fullConfig.telegram.chatId) {
@@ -1177,6 +1297,9 @@ var init_server = __esm({
1177
1297
  this.wssDashboard = new WebSocketServer({ noServer: true, maxPayload: MAX_WS_PAYLOAD });
1178
1298
  this.wssDashboard.on("connection", (ws) => this.handleDashboardConnection(ws));
1179
1299
  this.wssDashboard.on("error", (err) => logger.warn(`Dashboard WebSocket server error: ${err.message}`));
1300
+ this.wssCodex = new WebSocketServer({ noServer: true, maxPayload: MAX_WS_PAYLOAD });
1301
+ this.wssCodex.on("connection", (ws, req) => this.handleCodexConnection(ws, req));
1302
+ this.wssCodex.on("error", (err) => logger.warn(`Codex WebSocket server error: ${err.message}`));
1180
1303
  this.httpServer.on("upgrade", (req, socket, head) => {
1181
1304
  socket.on("error", (err) => logger.debug(`Upgrade socket error: ${err.message}`));
1182
1305
  const url = new URL(req.url ?? "/", "http://hub");
@@ -1197,6 +1320,10 @@ var init_server = __esm({
1197
1320
  this.wssDashboard.handleUpgrade(req, socket, head, (ws) => {
1198
1321
  this.wssDashboard.emit("connection", ws, req);
1199
1322
  });
1323
+ } else if (pathname === WS_PATH_CODEX) {
1324
+ this.wssCodex.handleUpgrade(req, socket, head, (ws) => {
1325
+ this.wssCodex.emit("connection", ws, req);
1326
+ });
1200
1327
  } else {
1201
1328
  socket.destroy();
1202
1329
  }
@@ -1211,7 +1338,7 @@ var init_server = __esm({
1211
1338
  this.httpServer.listen(this.port, this.host, () => {
1212
1339
  this.httpServer.removeListener("error", onStartupError);
1213
1340
  this.httpServer.on("error", (err) => logger.error(`HTTP server error: ${err.message}`));
1214
- const displayHost = this.host === "0.0.0.0" ? "127.0.0.1" : this.host;
1341
+ const displayHost = hubUrlHost(this.host);
1215
1342
  logger.info(`Hub server listening on http://${displayHost}:${this.port}`);
1216
1343
  this.startCodexAdapter();
1217
1344
  resolve();
@@ -1226,10 +1353,12 @@ var init_server = __esm({
1226
1353
  if (this.telegramBot) this.telegramBot.stopPolling();
1227
1354
  for (const ws of this.channelSockets.values()) ws.terminate();
1228
1355
  for (const ws of this.dashboardSockets) ws.terminate();
1356
+ for (const { ws } of this.codexAdapters.values()) ws.terminate();
1229
1357
  this.channelSockets.clear();
1230
1358
  this.dashboardSockets.clear();
1231
1359
  this.wssChannel.close();
1232
1360
  this.wssDashboard.close();
1361
+ this.wssCodex.close();
1233
1362
  this.httpServer.close(() => {
1234
1363
  logger.info("Hub server stopped");
1235
1364
  resolve();
@@ -1294,6 +1423,12 @@ var init_server = __esm({
1294
1423
  this.handleApiSend(req, res);
1295
1424
  } else if (url.pathname === "/api/notify" && req.method === "POST") {
1296
1425
  this.handleApiNotify(req, res);
1426
+ } else if (url.pathname === "/api/codex/folders" && req.method === "GET") {
1427
+ this.handleCodexFolders(url, res);
1428
+ } else if (url.pathname === "/api/codex/threads" && req.method === "POST") {
1429
+ this.handleCodexCreate(req, res);
1430
+ } else if (url.pathname === "/api/codex/threads/close" && req.method === "POST") {
1431
+ this.handleCodexClose(req, res);
1297
1432
  } else if (url.pathname === "/api/webhooks" && req.method === "GET") {
1298
1433
  const config2 = loadConfig();
1299
1434
  this.jsonResponse(res, 200, { webhooks: config2.webhooks || [] });
@@ -1323,23 +1458,23 @@ var init_server = __esm({
1323
1458
  return;
1324
1459
  }
1325
1460
  const candidates = [
1326
- path6.join(__dirname, "..", "dashboard", "index.html"),
1461
+ path8.join(__dirname, "..", "dashboard", "index.html"),
1327
1462
  // from dist/hub/
1328
- path6.join(__dirname, "dashboard", "index.html"),
1463
+ path8.join(__dirname, "dashboard", "index.html"),
1329
1464
  // from dist/ (bundled index.js)
1330
- path6.join(__dirname, "..", "..", "src", "dashboard", "index.html"),
1465
+ path8.join(__dirname, "..", "..", "src", "dashboard", "index.html"),
1331
1466
  // from dist/hub/ -> src/
1332
- path6.join(__dirname, "..", "src", "dashboard", "index.html"),
1467
+ path8.join(__dirname, "..", "src", "dashboard", "index.html"),
1333
1468
  // from dist/ -> src/
1334
- path6.join(process.cwd(), "dist", "dashboard", "index.html"),
1469
+ path8.join(process.cwd(), "dist", "dashboard", "index.html"),
1335
1470
  // from cwd
1336
- path6.join(process.cwd(), "src", "dashboard", "index.html")
1471
+ path8.join(process.cwd(), "src", "dashboard", "index.html")
1337
1472
  // from cwd/src
1338
1473
  ];
1339
1474
  logger.debug(`Dashboard candidates: ${JSON.stringify(candidates)}`);
1340
1475
  for (const candidate of candidates) {
1341
- if (fs6.existsSync(candidate)) {
1342
- const html = fs6.readFileSync(candidate, "utf-8");
1476
+ if (fs7.existsSync(candidate)) {
1477
+ const html = fs7.readFileSync(candidate, "utf-8");
1343
1478
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
1344
1479
  res.end(html);
1345
1480
  return;
@@ -1557,6 +1692,114 @@ var init_server = __esm({
1557
1692
  }
1558
1693
  }
1559
1694
  }
1695
+ handleCodexConnection(ws, req) {
1696
+ const isLocal = this.isLocalRequest(req);
1697
+ let adapterId;
1698
+ ws.on("error", (err) => {
1699
+ logger.warn(`Codex adapter WebSocket error: ${err.message}`);
1700
+ ws.terminate();
1701
+ });
1702
+ ws.on("pong", () => this.codexAlive.set(ws, true));
1703
+ ws.on("message", (data) => {
1704
+ let msg;
1705
+ try {
1706
+ msg = JSON.parse(data.toString());
1707
+ if (msg === null || typeof msg !== "object") {
1708
+ logger.warn("Invalid message from Codex adapter");
1709
+ return;
1710
+ }
1711
+ } catch {
1712
+ logger.warn("Invalid message from Codex adapter");
1713
+ return;
1714
+ }
1715
+ if (msg.type === "adapter_hello") {
1716
+ const a = msg.adapter;
1717
+ if (typeof a?.id !== "string" || !a.id || typeof a.host !== "string") return;
1718
+ if (adapterId && adapterId !== a.id) return;
1719
+ const holder = this.codexAdapters.get(a.id);
1720
+ if (holder && holder.ws !== ws) holder.ws.terminate();
1721
+ if (!adapterId) logger.info(`Codex adapter connected: ${a.host} (${a.id}, local: ${isLocal})`);
1722
+ adapterId = a.id;
1723
+ this.codexAdapters.set(a.id, { ws, info: { id: a.id, host: a.host, ready: a.ready === true, isLocal } });
1724
+ this.broadcastCodexAdapters();
1725
+ } else if (msg.type === "adapter_result" && adapterId) {
1726
+ const call = this.codexCalls.get(msg.requestId);
1727
+ if (!call || call.adapterId !== adapterId) return;
1728
+ call.settle(msg.ok ? { status: 200, body: msg.data } : { status: 422, body: { error: String(msg.error) } });
1729
+ }
1730
+ });
1731
+ ws.on("close", () => {
1732
+ if (!adapterId || this.codexAdapters.get(adapterId)?.ws !== ws) return;
1733
+ this.codexAdapters.delete(adapterId);
1734
+ for (const call of [...this.codexCalls.values()]) {
1735
+ if (call.adapterId === adapterId) call.settle({ status: 502, body: { error: "Codex adapter disconnected" } });
1736
+ }
1737
+ logger.info(`Codex adapter disconnected: ${adapterId}`);
1738
+ this.broadcastCodexAdapters();
1739
+ });
1740
+ }
1741
+ callCodexAdapter(adapterId, call) {
1742
+ const adapter = this.codexAdapters.get(adapterId);
1743
+ if (!adapter || adapter.ws.readyState !== WebSocket2.OPEN) {
1744
+ return Promise.resolve({ status: 404, body: { error: "Codex adapter is not connected" } });
1745
+ }
1746
+ const requestId = randomUUID3();
1747
+ return new Promise((resolve) => {
1748
+ const timer = setTimeout(
1749
+ () => settle({ status: 504, body: { error: "Codex did not respond in time. The conversation may still appear." } }),
1750
+ this.codexCallTimeoutMs
1751
+ );
1752
+ const settle = (outcome) => {
1753
+ clearTimeout(timer);
1754
+ this.codexCalls.delete(requestId);
1755
+ resolve(outcome);
1756
+ };
1757
+ this.codexCalls.set(requestId, { adapterId, settle });
1758
+ adapter.ws.send(JSON.stringify({ type: "adapter_call", requestId, call }));
1759
+ });
1760
+ }
1761
+ async handleCodexFolders(url, res) {
1762
+ const adapterId = url.searchParams.get("adapterId");
1763
+ if (!nonEmpty(adapterId)) {
1764
+ this.jsonResponse(res, 400, { error: "adapterId is required" });
1765
+ return;
1766
+ }
1767
+ const out = await this.callCodexAdapter(adapterId, { kind: "folders" });
1768
+ this.jsonResponse(res, out.status, out.body);
1769
+ }
1770
+ async handleCodexCreate(req, res) {
1771
+ const body = await this.readBody(req);
1772
+ const adapterId = body?.adapterId;
1773
+ const cwd = body?.cwd;
1774
+ if (!nonEmpty(adapterId) || !nonEmpty(cwd)) {
1775
+ this.jsonResponse(res, 400, { error: "adapterId and cwd are required" });
1776
+ return;
1777
+ }
1778
+ const out = await this.callCodexAdapter(adapterId, { kind: "create", cwd });
1779
+ this.jsonResponse(res, out.status, out.body);
1780
+ }
1781
+ async handleCodexClose(req, res) {
1782
+ const body = await this.readBody(req);
1783
+ const sessionId2 = body?.sessionId;
1784
+ if (!nonEmpty(sessionId2)) {
1785
+ this.jsonResponse(res, 400, { error: "sessionId is required" });
1786
+ return;
1787
+ }
1788
+ const session = this.sessions.get(sessionId2);
1789
+ const ws = this.channelSockets.get(sessionId2);
1790
+ if (session?.agentKind !== "codex" || !session.closable || ws?.readyState !== WebSocket2.OPEN) {
1791
+ this.jsonResponse(res, 404, { error: "No closable Codex conversation" });
1792
+ return;
1793
+ }
1794
+ ws.send(JSON.stringify({ type: "codex_close", sessionId: sessionId2 }));
1795
+ this.jsonResponse(res, 200, { ok: true });
1796
+ }
1797
+ codexAdapterList() {
1798
+ return [...this.codexAdapters.values()].map((a) => a.info);
1799
+ }
1800
+ broadcastCodexAdapters() {
1801
+ this.broadcastToDashboards({ type: "codex_adapters", adapters: this.codexAdapterList() });
1802
+ }
1560
1803
  // --- Dashboard WebSocket ---
1561
1804
  handleDashboardConnection(ws) {
1562
1805
  this.dashboardSockets.add(ws);
@@ -1575,6 +1818,7 @@ var init_server = __esm({
1575
1818
  requests: [...this.choiceRequests.values()].map((c) => c.request)
1576
1819
  };
1577
1820
  ws.send(JSON.stringify(pendingMsg));
1821
+ ws.send(JSON.stringify({ type: "codex_adapters", adapters: this.codexAdapterList() }));
1578
1822
  ws.on("message", (data) => {
1579
1823
  try {
1580
1824
  const msg = JSON.parse(data.toString());
@@ -1584,7 +1828,7 @@ var init_server = __esm({
1584
1828
  channelWs.send(JSON.stringify({ ...msg, source: "dashboard" }));
1585
1829
  }
1586
1830
  } else if (msg.type === "image_upload") {
1587
- this.handleImageUpload(msg);
1831
+ this.handleImageUpload(ws, msg);
1588
1832
  } else if (msg.type === "permission_response") {
1589
1833
  if (this.forwardPermissionResponse(msg)) {
1590
1834
  const verdict = msg.choiceId !== void 0 ? `choice ${msg.choiceId}` : msg.behavior;
@@ -1635,43 +1879,55 @@ var init_server = __esm({
1635
1879
  }
1636
1880
  }
1637
1881
  }
1638
- handleImageUpload(msg) {
1882
+ handleImageUpload(ws, msg) {
1639
1883
  const { sessionId: sessionId2, imageData, mimeType, originalName, content } = msg;
1884
+ const reject = (reason) => {
1885
+ logger.warn(`Image upload rejected for ${sessionId2}: ${reason}`);
1886
+ const rejected = { type: "upload_rejected", sessionId: sessionId2, reason, withText: typeof content === "string" && content.trim().length > 0 };
1887
+ if (ws.readyState === WebSocket2.OPEN) ws.send(JSON.stringify(rejected));
1888
+ };
1889
+ const channelWs = this.channelSockets.get(sessionId2);
1890
+ if (!channelWs || channelWs.readyState !== WebSocket2.OPEN) {
1891
+ reject("the session is not connected");
1892
+ return;
1893
+ }
1640
1894
  if (!this.localChannels.has(sessionId2)) {
1641
- logger.warn(`Image upload rejected: session ${sessionId2} is not local`);
1895
+ reject("this session is on another PC; images can only go to sessions on the hub's PC");
1642
1896
  return;
1643
1897
  }
1644
- const channelWs = this.channelSockets.get(sessionId2);
1645
- if (!channelWs || channelWs.readyState !== WebSocket2.OPEN) return;
1646
1898
  const allowedTypes = ["image/png", "image/jpeg", "image/gif", "image/webp"];
1647
- if (!allowedTypes.includes(mimeType)) return;
1899
+ if (!allowedTypes.includes(mimeType)) {
1900
+ reject("only PNG, JPEG, GIF and WebP images are supported");
1901
+ return;
1902
+ }
1648
1903
  const base64Data = imageData.replace(/^data:image\/\w+;base64,/, "");
1649
1904
  const buffer = Buffer.from(base64Data, "base64");
1650
1905
  if (buffer.length > 10 * 1024 * 1024) {
1651
- logger.warn("Image upload rejected: exceeds 10MB");
1906
+ reject("the image is larger than 10 MB");
1652
1907
  return;
1653
1908
  }
1654
- fs6.mkdirSync(UPLOADS_DIR, { recursive: true });
1909
+ fs7.mkdirSync(UPLOADS_DIR, { recursive: true });
1655
1910
  const ext = mimeType.split("/")[1] === "jpeg" ? "jpg" : mimeType.split("/")[1];
1656
1911
  const filename = `${randomUUID3()}.${ext}`;
1657
- const filePath = path6.join(UPLOADS_DIR, filename);
1658
- fs6.writeFileSync(filePath, buffer);
1912
+ const filePath = path8.join(UPLOADS_DIR, filename);
1913
+ fs7.writeFileSync(filePath, buffer);
1659
1914
  const forwardMsg = {
1660
1915
  type: "image_to_session",
1661
1916
  sessionId: sessionId2,
1662
1917
  imagePath: filePath,
1663
1918
  mimeType,
1664
1919
  originalName,
1665
- content
1920
+ content,
1921
+ source: "dashboard"
1666
1922
  };
1667
1923
  channelWs.send(JSON.stringify(forwardMsg));
1668
1924
  logger.info(`Image saved and forwarded: ${filename} (${buffer.length} bytes)`);
1669
1925
  setTimeout(() => {
1670
1926
  try {
1671
- fs6.unlinkSync(filePath);
1927
+ fs7.unlinkSync(filePath);
1672
1928
  } catch {
1673
1929
  }
1674
- }, 5 * 60 * 1e3);
1930
+ }, 5 * 60 * 1e3).unref();
1675
1931
  }
1676
1932
  async handleWebhookSave(req, res) {
1677
1933
  const body = await this.readBody(req);
@@ -1704,7 +1960,7 @@ var init_server = __esm({
1704
1960
  this.telegramBot.onImageToSession = (sessionId2, imagePath, mimeType, caption) => {
1705
1961
  const channelWs = this.channelSockets.get(sessionId2);
1706
1962
  if (channelWs?.readyState === WebSocket2.OPEN) {
1707
- const msg = { type: "image_to_session", sessionId: sessionId2, imagePath, mimeType, content: caption };
1963
+ const msg = { type: "image_to_session", sessionId: sessionId2, imagePath, mimeType, content: caption, source: "telegram" };
1708
1964
  channelWs.send(JSON.stringify(msg));
1709
1965
  logger.info(`Telegram photo forwarded to session: ${sessionId2}`);
1710
1966
  }
@@ -1832,7 +2088,7 @@ var init_server = __esm({
1832
2088
  logger.warn("Codex adapter is enabled but codex/main.js was not found next to the hub");
1833
2089
  return;
1834
2090
  }
1835
- this.codexSupervisor = new CodexSupervisor(script);
2091
+ this.codexSupervisor = new CodexSupervisor(script, { env: adapterEnv(process.env, { host: this.host, port: this.port }) });
1836
2092
  this.codexSupervisor.start();
1837
2093
  logger.info("Codex adapter started");
1838
2094
  }
@@ -1847,15 +2103,23 @@ var init_server = __esm({
1847
2103
  this.channelAlive.set(sessionId2, false);
1848
2104
  ws.ping();
1849
2105
  }
2106
+ for (const { ws } of this.codexAdapters.values()) {
2107
+ if (this.codexAlive.get(ws) === false) {
2108
+ ws.terminate();
2109
+ continue;
2110
+ }
2111
+ this.codexAlive.set(ws, false);
2112
+ ws.ping();
2113
+ }
1850
2114
  }, 3e4);
1851
2115
  }
1852
2116
  cleanupUploads() {
1853
2117
  try {
1854
- if (!fs6.existsSync(UPLOADS_DIR)) return;
1855
- const files = fs6.readdirSync(UPLOADS_DIR);
2118
+ if (!fs7.existsSync(UPLOADS_DIR)) return;
2119
+ const files = fs7.readdirSync(UPLOADS_DIR);
1856
2120
  for (const file of files) {
1857
2121
  try {
1858
- fs6.unlinkSync(path6.join(UPLOADS_DIR, file));
2122
+ fs7.unlinkSync(path8.join(UPLOADS_DIR, file));
1859
2123
  } catch {
1860
2124
  }
1861
2125
  }
@@ -1909,7 +2173,7 @@ Content-Length: 0\r
1909
2173
  });
1910
2174
  }
1911
2175
  };
1912
- if (process.argv[1] && (process.argv[1].endsWith("hub/server.js") || process.argv[1].endsWith("hub/server.ts"))) {
2176
+ if (isEntryScript(process.argv[1], ["/hub/server.js", "/hub/server.ts"])) {
1913
2177
  installCrashGuard("hub daemon");
1914
2178
  logStartup("Hub daemon");
1915
2179
  const config2 = loadConfig();
@@ -2054,9 +2318,9 @@ var init_hub_client = __esm({
2054
2318
  });
2055
2319
 
2056
2320
  // src/channel/peer-name.ts
2057
- import fs7 from "fs";
2321
+ import fs8 from "fs";
2058
2322
  import os2 from "os";
2059
- import path7 from "path";
2323
+ import path9 from "path";
2060
2324
  function findPeerName(records, env) {
2061
2325
  const valid = records.filter((r) => typeof r === "object" && r !== null);
2062
2326
  const nameOf = (match) => {
@@ -2076,18 +2340,18 @@ function readPeerName(env = process.env) {
2076
2340
  sessionId: env.CLAUDE_CODE_SESSION_ID
2077
2341
  };
2078
2342
  if (!lookup.messagingSocket && !lookup.sessionId) return void 0;
2079
- const configDir = env.CLAUDE_CONFIG_DIR || path7.join(os2.homedir(), ".claude");
2080
- const dir = path7.join(configDir, "sessions");
2343
+ const configDir = env.CLAUDE_CONFIG_DIR || path9.join(os2.homedir(), ".claude");
2344
+ const dir = path9.join(configDir, "sessions");
2081
2345
  let files;
2082
2346
  try {
2083
- files = fs7.readdirSync(dir).filter((f) => f.endsWith(".json"));
2347
+ files = fs8.readdirSync(dir).filter((f) => f.endsWith(".json"));
2084
2348
  } catch {
2085
2349
  return void 0;
2086
2350
  }
2087
2351
  const records = [];
2088
2352
  for (const f of files) {
2089
2353
  try {
2090
- records.push(JSON.parse(fs7.readFileSync(path7.join(dir, f), "utf8")));
2354
+ records.push(JSON.parse(fs8.readFileSync(path9.join(dir, f), "utf8")));
2091
2355
  } catch {
2092
2356
  }
2093
2357
  }
@@ -2110,7 +2374,7 @@ import {
2110
2374
  } from "@modelcontextprotocol/sdk/types.js";
2111
2375
  import { z } from "zod";
2112
2376
  import { randomUUID as randomUUID4 } from "crypto";
2113
- import path8 from "path";
2377
+ import path10 from "path";
2114
2378
  async function main() {
2115
2379
  logger.info(`Starting MCP channel server (session: ${sessionId})`);
2116
2380
  hubClient.connect();
@@ -2179,7 +2443,7 @@ var init_server2 = __esm({
2179
2443
  init_hub_client();
2180
2444
  init_peer_name2();
2181
2445
  sessionId = randomUUID4();
2182
- sessionName = process.env.CLAUDE_ALARM_SESSION_NAME ?? path8.basename(process.cwd());
2446
+ sessionName = process.env.CLAUDE_ALARM_SESSION_NAME ?? path10.basename(process.cwd());
2183
2447
  peerName = readPeerName();
2184
2448
  server = new Server(
2185
2449
  {
@@ -2356,8 +2620,8 @@ NOTIFICATIONS:
2356
2620
  // src/cli.ts
2357
2621
  init_config();
2358
2622
  import { spawn as spawn3 } from "child_process";
2359
- import fs8 from "fs";
2360
- import path9 from "path";
2623
+ import fs9 from "fs";
2624
+ import path11 from "path";
2361
2625
  import readline from "readline";
2362
2626
  import { fileURLToPath as fileURLToPath2 } from "url";
2363
2627
 
@@ -2368,6 +2632,12 @@ import fs2 from "fs";
2368
2632
  import path3 from "path";
2369
2633
  import { Duplex } from "stream";
2370
2634
  import WebSocket from "ws";
2635
+ function resolveCommand(command, platform = process.platform, env = process.env) {
2636
+ if (platform !== "win32") return { file: command, shell: false };
2637
+ if (/[\\/]/.test(command) || path3.extname(command)) return { file: command, shell: /\.(cmd|bat)$/i.test(command) };
2638
+ const file = findCodex(command, platform, env);
2639
+ return file ? { file, shell: file.endsWith(".cmd") } : { file: command, shell: false };
2640
+ }
2371
2641
  function findOnPath(command, platform = process.platform, env = process.env) {
2372
2642
  const dirs = (env.PATH ?? env.Path ?? "").split(path3.delimiter).filter(Boolean);
2373
2643
  for (const ext of platform === "win32" ? [".exe", ".cmd"] : [""]) {
@@ -2378,12 +2648,772 @@ function findOnPath(command, platform = process.platform, env = process.env) {
2378
2648
  }
2379
2649
  return void 0;
2380
2650
  }
2651
+ function findCodex(command, platform = process.platform, env = process.env) {
2652
+ const onPath = findOnPath(command, platform, env);
2653
+ if (onPath || platform !== "win32") return onPath;
2654
+ const candidates = [
2655
+ env.LOCALAPPDATA && path3.join(env.LOCALAPPDATA, "Programs", "OpenAI", "Codex", "bin", `${command}.exe`),
2656
+ env.APPDATA && path3.join(env.APPDATA, "npm", `${command}.cmd`)
2657
+ ];
2658
+ return candidates.find((file) => !!file && fs2.existsSync(file));
2659
+ }
2660
+ var defaultSpawn = (command, args) => {
2661
+ const { file, shell } = resolveCommand(command);
2662
+ if (shell) return spawn(`"${file}" ${args.join(" ")}`, { stdio: "pipe", windowsHide: true, shell: true });
2663
+ return spawn(file, args, { stdio: "pipe", windowsHide: true });
2664
+ };
2665
+ var HANDSHAKE_TIMEOUT_MS = 1e4;
2666
+ function connectProxy(command, spawnFn = defaultSpawn, timeoutMs = HANDSHAKE_TIMEOUT_MS) {
2667
+ const child = spawnFn(command, ["app-server", "proxy"]);
2668
+ child.stdin?.on("error", () => {
2669
+ });
2670
+ child.stderr?.on("data", (d) => logger.debug(`codex proxy: ${String(d).trim()}`));
2671
+ const stream = new Duplex({
2672
+ read() {
2673
+ },
2674
+ write(chunk, _encoding, callback) {
2675
+ if (child.stdin?.writable) child.stdin.write(chunk, callback);
2676
+ else callback();
2677
+ },
2678
+ final(callback) {
2679
+ child.stdin?.end();
2680
+ callback();
2681
+ }
2682
+ });
2683
+ child.stdout?.on("data", (d) => stream.push(d));
2684
+ child.stdout?.on("end", () => stream.push(null));
2685
+ Object.assign(stream, { setNoDelay() {
2686
+ }, setTimeout() {
2687
+ }, setKeepAlive() {
2688
+ }, ref() {
2689
+ }, unref() {
2690
+ } });
2691
+ const ws = new WebSocket("ws://localhost/", {
2692
+ createConnection: (() => stream)
2693
+ });
2694
+ const close = () => {
2695
+ ws.terminate();
2696
+ child.kill();
2697
+ };
2698
+ return new Promise((resolve, reject) => {
2699
+ let settled = false;
2700
+ const timer = setTimeout(() => fail(new Error(`codex daemon did not answer within ${timeoutMs / 1e3}s`)), timeoutMs);
2701
+ const fail = (err) => {
2702
+ if (settled) return;
2703
+ settled = true;
2704
+ clearTimeout(timer);
2705
+ close();
2706
+ reject(err);
2707
+ };
2708
+ child.on("error", fail);
2709
+ child.once("exit", (code) => fail(new Error(`codex proxy exited (code ${code})`)));
2710
+ ws.on("error", fail);
2711
+ ws.once("open", () => {
2712
+ if (settled) return;
2713
+ settled = true;
2714
+ clearTimeout(timer);
2715
+ child.on("error", (err) => logger.warn(`codex proxy error: ${err.message}`));
2716
+ ws.on("error", (err) => logger.warn(`codex daemon socket error: ${err.message}`));
2717
+ resolve({ ws, close });
2718
+ });
2719
+ });
2720
+ }
2721
+
2722
+ // src/codex/run.ts
2723
+ import path4 from "path";
2724
+
2725
+ // src/codex/rpc.ts
2726
+ init_logger();
2727
+ import { EventEmitter } from "events";
2728
+ var RpcError = class extends Error {
2729
+ constructor(code, message) {
2730
+ super(message);
2731
+ this.code = code;
2732
+ }
2733
+ };
2734
+ var RpcClient = class extends EventEmitter {
2735
+ constructor(ws, timeoutMs = 3e4) {
2736
+ super();
2737
+ this.ws = ws;
2738
+ this.timeoutMs = timeoutMs;
2739
+ ws.on("message", (data) => this.onMessage(String(data)));
2740
+ ws.on("close", () => {
2741
+ for (const p of this.pending.values()) {
2742
+ clearTimeout(p.timer);
2743
+ p.reject(new Error("connection closed"));
2744
+ }
2745
+ this.pending.clear();
2746
+ this.emit("close");
2747
+ });
2748
+ }
2749
+ nextId = 1;
2750
+ pending = /* @__PURE__ */ new Map();
2751
+ request(method, params, timeoutMs = this.timeoutMs) {
2752
+ if (this.ws.readyState !== this.ws.OPEN) return Promise.reject(new Error("connection closed"));
2753
+ const id = this.nextId++;
2754
+ return new Promise((resolve, reject) => {
2755
+ const timer = timeoutMs === null ? void 0 : setTimeout(() => {
2756
+ this.pending.delete(id);
2757
+ reject(new Error(`${method} timed out`));
2758
+ }, timeoutMs);
2759
+ this.pending.set(id, { resolve, reject, timer });
2760
+ this.ws.send(JSON.stringify(params === void 0 ? { id, method } : { id, method, params }));
2761
+ });
2762
+ }
2763
+ notify(method, params) {
2764
+ if (this.ws.readyState !== this.ws.OPEN) return;
2765
+ this.ws.send(JSON.stringify(params === void 0 ? { method } : { method, params }));
2766
+ }
2767
+ respond(id, result) {
2768
+ if (this.ws.readyState !== this.ws.OPEN) return;
2769
+ this.ws.send(JSON.stringify({ id, result }));
2770
+ }
2771
+ // ws stops reading the socket for good if a 'message' listener throws.
2772
+ guarded(method, fn) {
2773
+ try {
2774
+ fn();
2775
+ } catch (err) {
2776
+ logger.warn(`Codex ${method} handling failed: ${err.message}`);
2777
+ }
2778
+ }
2779
+ onMessage(text) {
2780
+ let msg;
2781
+ try {
2782
+ msg = JSON.parse(text);
2783
+ } catch {
2784
+ return;
2785
+ }
2786
+ if (!msg || typeof msg !== "object") return;
2787
+ if (msg.method === void 0 && msg.id !== void 0) {
2788
+ const p = this.pending.get(msg.id);
2789
+ if (!p) return;
2790
+ this.pending.delete(msg.id);
2791
+ clearTimeout(p.timer);
2792
+ if (msg.error) p.reject(new RpcError(msg.error.code, msg.error.message));
2793
+ else p.resolve(msg.result);
2794
+ } else if (msg.method !== void 0 && msg.id !== void 0) {
2795
+ this.guarded(msg.method, () => this.emit("request", msg.id, msg.method, msg.params));
2796
+ } else if (msg.method !== void 0) {
2797
+ this.guarded(msg.method, () => this.emit("notification", msg.method, msg.params));
2798
+ }
2799
+ }
2800
+ };
2801
+
2802
+ // src/codex/mapping.ts
2803
+ var SOURCE_LABEL = { dashboard: "Dashboard", telegram: "Telegram", api: "API" };
2804
+ var SOURCE_PREFIX = new RegExp(`^\\[claude-alarm(?: \xB7 (?:${Object.values(SOURCE_LABEL).join("|")}))?\\] ?`);
2805
+ function finalAnswer(messages) {
2806
+ const finals = messages.filter((m) => m.phase === "final_answer").map((m) => m.text);
2807
+ if (finals.length) return finals.join("\n\n");
2808
+ return messages.at(-1)?.text ?? null;
2809
+ }
2810
+
2811
+ // src/codex/run.ts
2812
+ init_constants();
2813
+ var MINUTE = 6e4;
2814
+ var SUMMARY_MAX = 200;
2815
+ var clip = (text) => text.length > SUMMARY_MAX ? `${text.slice(0, SUMMARY_MAX)}\u2026` : text;
2816
+ function relayed(method, params) {
2817
+ switch (method) {
2818
+ case "item/commandExecution/requestApproval": {
2819
+ const command = params.command ?? (params.commandActions ?? []).map((a) => a.command).join("\n");
2820
+ return { kind: "command", summary: clip(String(command || params.reason || "")), decline: { decision: "decline" } };
2821
+ }
2822
+ case "item/fileChange/requestApproval":
2823
+ return { kind: "file change", summary: clip(String(params.reason ?? "")), decline: { decision: "decline" } };
2824
+ case "mcpServer/elicitation/request":
2825
+ if (params._meta?.codex_approval_kind !== "mcp_tool_call") return null;
2826
+ return { kind: "MCP tool", summary: clip(`${params.serverName ?? ""}: ${params.message ?? ""}`), decline: { action: "decline" } };
2827
+ default:
2828
+ return null;
2829
+ }
2830
+ }
2831
+ function unrelayed(method, params) {
2832
+ switch (method) {
2833
+ case "item/permissions/requestApproval":
2834
+ return { kind: "permissions", summary: clip(String(params.reason ?? "")), answer: { permissions: {} } };
2835
+ case "item/tool/requestUserInput":
2836
+ return {
2837
+ kind: "question",
2838
+ summary: clip((params.questions ?? []).map((q) => q.question ?? "").join(" / ")),
2839
+ answer: { answers: {} }
2840
+ };
2841
+ case "mcpServer/elicitation/request":
2842
+ return { kind: "MCP form", summary: clip(String(params.message ?? "")), answer: { action: "decline" } };
2843
+ default:
2844
+ return null;
2845
+ }
2846
+ }
2847
+ async function runCodexTask(opts) {
2848
+ const progress = opts.progress ?? (() => {
2849
+ });
2850
+ const approvalTimeoutMs = opts.approvalTimeoutMs ?? 10 * MINUTE;
2851
+ const started = Date.now();
2852
+ const conn = await connectProxy(opts.command ?? "codex", opts.spawnFn ?? defaultSpawn);
2853
+ const rpc = new RpcClient(conn.ws);
2854
+ const timers = /* @__PURE__ */ new Set();
2855
+ try {
2856
+ const init2 = await rpc.request("initialize", {
2857
+ clientInfo: { name: "claude-alarm-run", version: CHANNEL_SERVER_VERSION }
2858
+ });
2859
+ rpc.notify("initialized");
2860
+ const yolo = { sandbox: "danger-full-access", approvalPolicy: "never" };
2861
+ let threadId;
2862
+ if (opts.threadId) {
2863
+ await rpc.request("thread/resume", opts.yolo ? { threadId: opts.threadId, ...yolo } : { threadId: opts.threadId });
2864
+ threadId = opts.threadId;
2865
+ } else {
2866
+ const res = await rpc.request("thread/start", {
2867
+ cwd: opts.cwd,
2868
+ ...opts.yolo ? yolo : { sandbox: "workspace-write", approvalPolicy: "on-request" }
2869
+ });
2870
+ threadId = res.thread.id;
2871
+ if (opts.name) {
2872
+ await rpc.request("thread/name/set", { threadId, name: opts.name }).catch(() => {
2873
+ });
2874
+ }
2875
+ }
2876
+ progress(`thread ${threadId}`);
2877
+ const messages = /* @__PURE__ */ new Map();
2878
+ const approvals = [];
2879
+ const waiting = /* @__PURE__ */ new Map();
2880
+ let finish;
2881
+ let lose;
2882
+ const completed = new Promise((resolve, reject) => {
2883
+ finish = resolve;
2884
+ lose = reject;
2885
+ });
2886
+ completed.catch(() => {
2887
+ });
2888
+ rpc.on("close", () => lose(new Error("connection to the Codex daemon closed")));
2889
+ rpc.on("notification", (method, params) => {
2890
+ if (params?.threadId !== threadId) return;
2891
+ if (method === "item/completed") {
2892
+ const item = params.item ?? {};
2893
+ if (item.type === "agentMessage") {
2894
+ const list = messages.get(params.turnId) ?? [];
2895
+ list.push({ text: item.text, phase: item.phase });
2896
+ messages.set(params.turnId, list);
2897
+ } else if (item.type === "commandExecution") {
2898
+ progress(`ran ${clip(String(item.command ?? ""))} (exit ${item.exitCode ?? "?"})`);
2899
+ }
2900
+ } else if (method === "serverRequest/resolved") {
2901
+ const record = waiting.get(params.requestId);
2902
+ if (record) {
2903
+ waiting.delete(params.requestId);
2904
+ record.outcome = "answered-elsewhere";
2905
+ progress(`approval answered elsewhere: ${record.kind}`);
2906
+ }
2907
+ } else if (method === "turn/completed") {
2908
+ finish(params.turn);
2909
+ }
2910
+ });
2911
+ rpc.on("request", (id, method, params) => {
2912
+ if (params?.threadId !== threadId) return;
2913
+ const wait = relayed(method, params);
2914
+ if (wait) {
2915
+ const record = { kind: wait.kind, summary: wait.summary, outcome: "unanswered" };
2916
+ approvals.push(record);
2917
+ waiting.set(id, record);
2918
+ progress(`approval pending (${Math.round(approvalTimeoutMs / 1e3)}s): ${wait.kind} ${wait.summary}`);
2919
+ const timer = setTimeout(() => {
2920
+ timers.delete(timer);
2921
+ if (!waiting.delete(id)) return;
2922
+ record.outcome = "timeout-declined";
2923
+ rpc.respond(id, wait.decline);
2924
+ progress(`approval declined after timeout: ${wait.kind}`);
2925
+ }, approvalTimeoutMs);
2926
+ timers.add(timer);
2927
+ return;
2928
+ }
2929
+ const answer = unrelayed(method, params);
2930
+ if (answer) {
2931
+ approvals.push({ kind: answer.kind, summary: answer.summary, outcome: "auto-declined" });
2932
+ rpc.respond(id, answer.answer);
2933
+ progress(`declined at once: ${answer.kind}`);
2934
+ }
2935
+ });
2936
+ const turnStart = { threadId, input: [{ type: "text", text: opts.brief }] };
2937
+ if (opts.outputSchema !== void 0) turnStart.outputSchema = opts.outputSchema;
2938
+ const { turn } = await rpc.request("turn/start", turnStart);
2939
+ const turnId = turn.id;
2940
+ progress(`turn ${turnId} started`);
2941
+ let stopReason = null;
2942
+ let interruptRequested;
2943
+ const stopped = new Promise((resolve) => interruptRequested = resolve);
2944
+ const stop = (reason) => {
2945
+ if (stopReason) return;
2946
+ stopReason = reason;
2947
+ progress(reason === "timedOut" ? "timeout reached, interrupting the turn" : "interrupting the turn");
2948
+ rpc.request("turn/interrupt", { threadId, turnId }).catch(() => {
2949
+ });
2950
+ interruptRequested();
2951
+ };
2952
+ const deadline = setTimeout(() => stop("timedOut"), opts.timeoutMs ?? 60 * MINUTE);
2953
+ timers.add(deadline);
2954
+ const onAbort = () => stop("interrupted");
2955
+ if (opts.signal?.aborted) onAbort();
2956
+ opts.signal?.addEventListener("abort", onAbort, { once: true });
2957
+ const grace = stopped.then(
2958
+ () => new Promise((resolve) => {
2959
+ const t = setTimeout(() => resolve(null), opts.interruptGraceMs ?? 15e3);
2960
+ timers.add(t);
2961
+ })
2962
+ );
2963
+ let ended;
2964
+ let lost = null;
2965
+ try {
2966
+ ended = await Promise.race([completed, grace]);
2967
+ } catch (err) {
2968
+ ended = null;
2969
+ lost = err.message;
2970
+ } finally {
2971
+ opts.signal?.removeEventListener("abort", onAbort);
2972
+ }
2973
+ let status;
2974
+ let error = null;
2975
+ if (stopReason) status = stopReason;
2976
+ else if (lost) {
2977
+ status = "failed";
2978
+ error = lost;
2979
+ } else if (ended?.status === "failed" || ended?.error) {
2980
+ status = "failed";
2981
+ error = ended?.error?.message ?? "the turn failed";
2982
+ } else if (ended?.status === "interrupted") status = "interrupted";
2983
+ else status = "completed";
2984
+ const collected = messages.get(turnId) ?? [];
2985
+ const fromTurn = (ended?.items ?? []).filter((i) => i.type === "agentMessage");
2986
+ const finalText = finalAnswer(collected.length ? collected : fromTurn);
2987
+ let result = null;
2988
+ if (opts.outputSchema !== void 0 && finalText) {
2989
+ try {
2990
+ result = JSON.parse(finalText);
2991
+ } catch {
2992
+ result = null;
2993
+ }
2994
+ }
2995
+ return {
2996
+ threadId,
2997
+ turnId,
2998
+ status,
2999
+ error,
3000
+ result,
3001
+ finalText,
3002
+ approvals,
3003
+ durationMs: Date.now() - started,
3004
+ codex: { userAgent: init2.userAgent ?? null }
3005
+ };
3006
+ } finally {
3007
+ for (const t of timers) clearTimeout(t);
3008
+ conn.close();
3009
+ }
3010
+ }
3011
+ var VALUE_FLAGS = {
3012
+ "--brief": "brief",
3013
+ "--cwd": "cwd",
3014
+ "--thread": "threadId",
3015
+ "--name": "name",
3016
+ "--output-schema": "outputSchema",
3017
+ "--approval-timeout": "approvalTimeoutMs",
3018
+ "--timeout": "timeoutMs"
3019
+ };
3020
+ function parseRunArgs(argv) {
3021
+ const out = {};
3022
+ for (let i = 0; i < argv.length; i++) {
3023
+ const flag = argv[i];
3024
+ if (flag === "--yolo") {
3025
+ out.yolo = true;
3026
+ continue;
3027
+ }
3028
+ const key = VALUE_FLAGS[flag];
3029
+ if (!key) throw new Error(`Unknown option: ${flag}`);
3030
+ const value = argv[++i];
3031
+ if (value === void 0) throw new Error(`${flag} needs a value`);
3032
+ if (key === "approvalTimeoutMs" || key === "timeoutMs") {
3033
+ const minutes = Number(value);
3034
+ if (!Number.isFinite(minutes) || minutes <= 0) throw new Error(`${flag} must be a number of minutes`);
3035
+ out[key] = Math.round(minutes * MINUTE);
3036
+ } else {
3037
+ out[key] = value;
3038
+ }
3039
+ }
3040
+ if (out.brief === void 0) throw new Error("--brief <file|-> is required");
3041
+ return out;
3042
+ }
3043
+ async function runFromCli(argv, io) {
3044
+ let task;
3045
+ try {
3046
+ const args = parseRunArgs(argv);
3047
+ const brief = args.brief === "-" ? await io.readStdin() : await io.readFile(args.brief);
3048
+ if (!brief.trim()) throw new Error("the brief is empty");
3049
+ task = {
3050
+ brief,
3051
+ cwd: path4.resolve(io.cwd, args.cwd ?? "."),
3052
+ threadId: args.threadId,
3053
+ name: args.name,
3054
+ yolo: args.yolo,
3055
+ outputSchema: args.outputSchema === void 0 ? void 0 : JSON.parse(await io.readFile(args.outputSchema)),
3056
+ approvalTimeoutMs: args.approvalTimeoutMs,
3057
+ timeoutMs: args.timeoutMs,
3058
+ signal: io.signal,
3059
+ spawnFn: io.spawnFn,
3060
+ progress: (line) => io.err(`[codex run] ${line}
3061
+ `)
3062
+ };
3063
+ } catch (err) {
3064
+ io.err(`codex run: ${err.message}
3065
+ `);
3066
+ return 2;
3067
+ }
3068
+ try {
3069
+ const result = await runCodexTask(task);
3070
+ io.out(`${JSON.stringify(result, null, 2)}
3071
+ `);
3072
+ return result.status === "completed" ? 0 : 1;
3073
+ } catch (err) {
3074
+ io.err(`codex run: could not start the task: ${err.message}
3075
+ `);
3076
+ return 2;
3077
+ }
3078
+ }
2381
3079
 
2382
3080
  // src/cli.ts
2383
3081
  init_constants();
2384
3082
  init_logger();
2385
3083
  init_crash_guard();
2386
- var __dirname2 = path9.dirname(fileURLToPath2(import.meta.url));
3084
+
3085
+ // src/hub/readiness.ts
3086
+ var HUB_START_TIMEOUT_MS = 5e3;
3087
+ async function waitForHub(opts) {
3088
+ const timeoutMs = opts.timeoutMs ?? HUB_START_TIMEOUT_MS;
3089
+ const intervalMs = opts.intervalMs ?? 200;
3090
+ const deadline = Date.now() + timeoutMs;
3091
+ const headers = opts.token ? { Authorization: `Bearer ${opts.token}` } : {};
3092
+ for (; ; ) {
3093
+ if (!opts.isAlive()) return "exited";
3094
+ const remaining = deadline - Date.now();
3095
+ if (remaining <= 0) return "timeout";
3096
+ try {
3097
+ const res = await fetch(opts.url, { headers, signal: AbortSignal.timeout(Math.min(remaining, 1e3)) });
3098
+ if (res.ok) {
3099
+ const body = await res.json();
3100
+ if (body.pid === opts.pid) return "ready";
3101
+ }
3102
+ } catch {
3103
+ }
3104
+ await new Promise((resolve) => setTimeout(resolve, Math.min(intervalMs, Math.max(0, deadline - Date.now()))));
3105
+ }
3106
+ }
3107
+
3108
+ // src/cli.ts
3109
+ init_hub_url();
3110
+
3111
+ // src/codex/hub-target.ts
3112
+ init_hub_url();
3113
+ function resolveAdapterHub(config2, env = process.env) {
3114
+ const envHost = env.CLAUDE_ALARM_HUB_HOST;
3115
+ const envPort = env.CLAUDE_ALARM_HUB_PORT;
3116
+ const envToken = env.CLAUDE_ALARM_HUB_TOKEN;
3117
+ return {
3118
+ host: hubUrlHost(envHost ?? config2.hub.host),
3119
+ port: envPort ? parseInt(envPort, 10) : config2.hub.port,
3120
+ token: envToken ?? config2.hub.token,
3121
+ fromEnv: { host: envHost !== void 0, port: !!envPort, token: envToken !== void 0 }
3122
+ };
3123
+ }
3124
+
3125
+ // src/codex/start-check.ts
3126
+ var ADAPTER_REPORT_TIMEOUT_MS = 1e4;
3127
+ var HUB_CHECK_TIMEOUT_MS = 3e3;
3128
+ var RESTART_HINT = "After fixing this, restart the adapter: claude-alarm codex stop, then claude-alarm codex start (if the hub started the adapter, restart the hub instead)";
3129
+ var CONFIG_PATH = "~/.claude-alarm/config.json";
3130
+ var NOT_CHECKED = { text: "Codex daemon: not checked (the adapter was already running)", warning: false };
3131
+ function toOutcome(msg) {
3132
+ if (msg.connected === true) return typeof msg.userAgent === "string" ? { connected: true, userAgent: msg.userAgent } : { connected: true };
3133
+ return { connected: false, error: String(msg.error ?? ""), notFound: msg.notFound === true };
3134
+ }
3135
+ function waitForAdapterReport(child, timeoutMs = ADAPTER_REPORT_TIMEOUT_MS) {
3136
+ return new Promise((resolve) => {
3137
+ const finish = (result) => {
3138
+ clearTimeout(timer);
3139
+ child.off("message", onMessage);
3140
+ child.off("exit", onExit);
3141
+ child.off("error", onError);
3142
+ resolve(result);
3143
+ };
3144
+ const onMessage = (msg) => {
3145
+ if (!msg || typeof msg !== "object") return;
3146
+ const m = msg;
3147
+ if (m.type === "codex-first-connect") finish({ kind: "report", outcome: toOutcome(m) });
3148
+ else if (m.type === "codex-already-running" && Number.isInteger(m.pid)) finish({ kind: "already", pid: m.pid });
3149
+ else if (m.type === "codex-lock-failed") finish({ kind: "lockFailed", error: String(m.error ?? "") });
3150
+ };
3151
+ const onExit = (code, signal) => finish({ kind: "exited", code, signal });
3152
+ const onError = (error) => finish({ kind: "exited", code: null, signal: null, error });
3153
+ const timer = setTimeout(() => finish({ kind: "timeout" }), timeoutMs);
3154
+ child.on("message", onMessage);
3155
+ child.on("exit", onExit);
3156
+ child.on("error", onError);
3157
+ });
3158
+ }
3159
+ function hubUrl(hub) {
3160
+ return `http://${hub.host}:${hub.port}`;
3161
+ }
3162
+ async function checkHub(hub, timeoutMs = HUB_CHECK_TIMEOUT_MS) {
3163
+ let res;
3164
+ try {
3165
+ res = await fetch(`${hubUrl(hub)}/api/status`, {
3166
+ headers: hub.token ? { Authorization: `Bearer ${hub.token}` } : {},
3167
+ redirect: "manual",
3168
+ signal: AbortSignal.timeout(timeoutMs)
3169
+ });
3170
+ } catch (err) {
3171
+ const e = err;
3172
+ return { kind: "unreachable", reason: typeof e.cause?.code === "string" ? e.cause.code : e.name };
3173
+ }
3174
+ if (res.status === 200) {
3175
+ try {
3176
+ const body = await res.json();
3177
+ if (body?.running === true) return { kind: "ok" };
3178
+ } catch {
3179
+ }
3180
+ return { kind: "not-hub", status: 200 };
3181
+ }
3182
+ await res.body?.cancel().catch(() => {
3183
+ });
3184
+ return res.status === 401 ? { kind: "unauthorized" } : { kind: "not-hub", status: res.status };
3185
+ }
3186
+ function daemonLine(report, command, logFile) {
3187
+ if (report.kind === "timeout") {
3188
+ return { text: `Codex daemon: no answer within ${ADAPTER_REPORT_TIMEOUT_MS / 1e3}s. The adapter keeps trying. See ${logFile}`, warning: true };
3189
+ }
3190
+ const o = report.outcome;
3191
+ if (o.connected) return { text: `Codex daemon: connected (${o.userAgent ?? "unknown version"})`, warning: false };
3192
+ if (o.notFound) {
3193
+ return { text: `Codex daemon: "${command}" not found. Install Codex or set "codex.command" in ${CONFIG_PATH}, and run the restart from a new terminal.`, warning: true };
3194
+ }
3195
+ return { text: `Codex daemon: not connected (${o.error}). Is Codex running? The adapter keeps retrying.`, warning: true };
3196
+ }
3197
+ function addressSource(hub) {
3198
+ const host = hub.fromEnv.host ? "CLAUDE_ALARM_HUB_HOST" : "hub.host";
3199
+ const port = hub.fromEnv.port ? "CLAUDE_ALARM_HUB_PORT" : "hub.port";
3200
+ const fromConfig = !hub.fromEnv.host || !hub.fromEnv.port;
3201
+ return `${host} and ${port}${fromConfig ? ` in ${CONFIG_PATH}` : ""}`;
3202
+ }
3203
+ function tokenSource(hub) {
3204
+ return hub.fromEnv.token ? "CLAUDE_ALARM_HUB_TOKEN" : `hub.token in ${CONFIG_PATH}`;
3205
+ }
3206
+ function hubLine(check, hub) {
3207
+ const url = hubUrl(hub);
3208
+ switch (check.kind) {
3209
+ case "ok":
3210
+ return { text: `Hub: reachable at ${url}`, warning: false };
3211
+ case "unauthorized":
3212
+ return { text: `Hub: ${url} rejected the token (401). Check ${tokenSource(hub)}.`, warning: true };
3213
+ case "not-hub":
3214
+ return { text: `Hub: ${url} answered ${check.status} but is not a claude-alarm hub. Check ${addressSource(hub)}.`, warning: true };
3215
+ case "unreachable":
3216
+ return { text: `Hub: not reachable at ${url} (${check.reason}). Check ${addressSource(hub)} and that the hub is running.`, warning: true };
3217
+ }
3218
+ }
3219
+ function printChecks(d, daemon, hub) {
3220
+ d.out(` ${daemon.text}`);
3221
+ d.out(` ${hub.text}`);
3222
+ if (daemon.warning || hub.warning) d.out(` ${RESTART_HINT}`);
3223
+ }
3224
+ function printAlreadyRunning(d, pid, hub) {
3225
+ d.out(`Codex adapter is already running (PID: ${pid})`);
3226
+ printChecks(d, NOT_CHECKED, hubLine(hub, d.hub));
3227
+ }
3228
+ async function startAdapter(d) {
3229
+ const existing = await d.queryOwner();
3230
+ if (existing.state === "running") {
3231
+ printAlreadyRunning(d, existing.pid, await checkHub(d.hub, d.hubTimeoutMs));
3232
+ return 0;
3233
+ }
3234
+ const child = d.spawnAdapter();
3235
+ const seen = {};
3236
+ child.on("exit", (code, signal) => {
3237
+ seen.exit ??= { code, signal };
3238
+ });
3239
+ child.on("error", (error) => {
3240
+ seen.exit ??= { code: null, signal: null, error };
3241
+ });
3242
+ const [report, hub] = await Promise.all([
3243
+ waitForAdapterReport(child, d.reportTimeoutMs),
3244
+ checkHub(d.hub, d.hubTimeoutMs)
3245
+ ]);
3246
+ if (child.connected) child.disconnect();
3247
+ child.unref();
3248
+ if (report.kind === "already") {
3249
+ printAlreadyRunning(d, report.pid, hub);
3250
+ return 0;
3251
+ }
3252
+ if (report.kind === "lockFailed") {
3253
+ d.err(`Codex adapter cannot start: ${report.error}`);
3254
+ return 1;
3255
+ }
3256
+ if (report.kind !== "exited" && !seen.exit && child.pid !== void 0) {
3257
+ d.out(`Codex adapter started (PID: ${child.pid}). Logs: ${d.logFile}`);
3258
+ printChecks(d, daemonLine(report, d.command, d.logFile), hubLine(hub, d.hub));
3259
+ return 0;
3260
+ }
3261
+ const exit = seen.exit ?? (report.kind === "exited" ? report : void 0);
3262
+ if (!exit || exit.error) {
3263
+ d.err(`Codex adapter failed to start: ${exit?.error?.message ?? "no process id"}`);
3264
+ return 1;
3265
+ }
3266
+ const owner = await d.queryOwner();
3267
+ if (owner.state === "running" && owner.pid !== child.pid) {
3268
+ printAlreadyRunning(d, owner.pid, hub);
3269
+ return 0;
3270
+ }
3271
+ const how = exit.code !== null ? `code ${exit.code}` : `signal ${exit.signal}`;
3272
+ d.err(`Codex adapter exited during startup (${how}). See ${d.logFile}`);
3273
+ return 1;
3274
+ }
3275
+
3276
+ // src/codex/instance-lock.ts
3277
+ import crypto from "crypto";
3278
+ import fs4 from "fs";
3279
+ import net2 from "net";
3280
+ import path5 from "path";
3281
+ var CONTROL_PROTOCOL = 1;
3282
+ var STOP_PROOF_LABEL = "claude-alarm codex stop";
3283
+ var MAX_LINE_BYTES = 4096;
3284
+ function stopProof(token) {
3285
+ return crypto.createHmac("sha256", token).update(STOP_PROOF_LABEL).digest("hex");
3286
+ }
3287
+ function controlEndpoint(configDir, platform = process.platform) {
3288
+ if (platform !== "win32") return path5.posix.join(configDir, "codex.sock");
3289
+ let dir;
3290
+ try {
3291
+ dir = fs4.realpathSync.native(configDir);
3292
+ } catch {
3293
+ dir = path5.win32.resolve(configDir);
3294
+ }
3295
+ const hash = crypto.createHash("sha256").update(dir.toLowerCase()).digest("hex").slice(0, 16);
3296
+ return `\\\\.\\pipe\\claude-alarm-codex-${hash}`;
3297
+ }
3298
+ function ask(endpoint, request, timeoutMs) {
3299
+ return new Promise((resolve) => {
3300
+ let buffer = "";
3301
+ let settled = false;
3302
+ const socket = net2.connect(endpoint);
3303
+ const finish = (answer) => {
3304
+ if (settled) return;
3305
+ settled = true;
3306
+ clearTimeout(timer);
3307
+ socket.destroy();
3308
+ resolve(answer);
3309
+ };
3310
+ const timer = setTimeout(() => finish({ state: "unknown", error: `no answer within ${timeoutMs}ms` }), timeoutMs);
3311
+ socket.setEncoding("utf8");
3312
+ socket.on("connect", () => socket.write(`${JSON.stringify(request)}
3313
+ `));
3314
+ socket.on("data", (chunk) => {
3315
+ buffer += chunk;
3316
+ const end = buffer.indexOf("\n");
3317
+ if (end < 0) {
3318
+ if (Buffer.byteLength(buffer) > MAX_LINE_BYTES) finish({ state: "unknown", error: "reply too long" });
3319
+ return;
3320
+ }
3321
+ try {
3322
+ finish({ reply: JSON.parse(buffer.slice(0, end)) });
3323
+ } catch {
3324
+ finish({ state: "unknown", error: "unreadable reply" });
3325
+ }
3326
+ });
3327
+ socket.on("end", () => finish({ state: "unknown", error: "closed without a reply" }));
3328
+ socket.on("error", (err) => {
3329
+ if (err.code === "ENOENT" || err.code === "ECONNREFUSED") finish({ state: "absent" });
3330
+ else finish({ state: "unknown", error: err.code ?? err.message });
3331
+ });
3332
+ });
3333
+ }
3334
+ async function queryOwner(endpoint, timeoutMs = 1e3) {
3335
+ const answer = await ask(endpoint, { type: "status" }, timeoutMs);
3336
+ if (!("reply" in answer)) return answer;
3337
+ const reply = answer.reply;
3338
+ if (reply?.type === "status" && reply.protocol === CONTROL_PROTOCOL && Number.isInteger(reply.pid)) {
3339
+ return { state: "running", pid: reply.pid };
3340
+ }
3341
+ return { state: "unknown", error: "unexpected reply" };
3342
+ }
3343
+ async function requestStop(endpoint, token, timeoutMs = 1e3) {
3344
+ const answer = await ask(endpoint, { type: "stop", proof: stopProof(token) }, timeoutMs);
3345
+ if (!("reply" in answer)) return answer;
3346
+ const reply = answer.reply;
3347
+ if (reply?.type === "stopping" && Number.isInteger(reply.pid)) return { state: "stopping", pid: reply.pid };
3348
+ if (reply?.type === "error" && reply.error === "unauthorized") return { state: "unauthorized" };
3349
+ return { state: "unknown", error: "unexpected reply" };
3350
+ }
3351
+
3352
+ // src/codex/control-cli.ts
3353
+ var STOP_WAIT_MS = 5e3;
3354
+ var STOP_POLL_MS = 100;
3355
+ function legacyNote(d, ownerPid) {
3356
+ const pid = d.legacyPid();
3357
+ if (pid === void 0 || pid === ownerPid) return;
3358
+ if (!d.isRunning(pid)) {
3359
+ d.removeLegacyPidFile();
3360
+ return;
3361
+ }
3362
+ d.out(`Note: ${d.legacyPidFile} names a running process (PID: ${pid}). claude-alarm 1.2.0 and earlier wrote this file; if that process is an old Codex adapter, restart the hub or end it yourself.`);
3363
+ }
3364
+ function unavailable(d, error) {
3365
+ d.err(`Codex adapter control endpoint is unavailable: ${error}`);
3366
+ return 1;
3367
+ }
3368
+ async function stopAdapter(d, startWithHub) {
3369
+ const owner = await d.queryOwner();
3370
+ if (owner.state === "unknown") return unavailable(d, owner.error);
3371
+ if (owner.state === "absent") {
3372
+ d.out("Codex adapter is not running");
3373
+ legacyNote(d);
3374
+ return 0;
3375
+ }
3376
+ const reply = await d.requestStop();
3377
+ if (reply.state === "unknown") return unavailable(d, reply.error);
3378
+ if (reply.state === "unauthorized") {
3379
+ d.err(`Codex adapter refused to stop: the token in ${d.configFile} does not match the one it started with`);
3380
+ return 1;
3381
+ }
3382
+ if (reply.state === "absent") {
3383
+ d.out("Codex adapter is not running");
3384
+ return 0;
3385
+ }
3386
+ const sleep = d.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
3387
+ for (let waited = 0; waited < (d.stopWaitMs ?? STOP_WAIT_MS); waited += STOP_POLL_MS) {
3388
+ await sleep(STOP_POLL_MS);
3389
+ const now = await d.queryOwner();
3390
+ if (now.state === "absent" || now.state === "running" && now.pid !== reply.pid) {
3391
+ d.out(`Codex adapter stopped (PID: ${reply.pid})`);
3392
+ if (now.state === "running") d.out(`Another Codex adapter took over (PID: ${now.pid})`);
3393
+ else if (startWithHub) d.out("If the hub started it, the hub will not start it again until you restart the hub.");
3394
+ return 0;
3395
+ }
3396
+ }
3397
+ d.err(`Stop requested, but the Codex adapter (PID: ${reply.pid}) is still running. Check with: claude-alarm codex status`);
3398
+ return 1;
3399
+ }
3400
+ async function adapterStatus(d, startWithHub) {
3401
+ const owner = await d.queryOwner();
3402
+ if (owner.state === "running") {
3403
+ d.out(`Codex adapter: running (PID: ${owner.pid})`);
3404
+ legacyNote(d, owner.pid);
3405
+ } else if (owner.state === "unknown") {
3406
+ d.out(`Codex adapter: unknown (${owner.error})`);
3407
+ } else {
3408
+ d.out("Codex adapter: not running");
3409
+ legacyNote(d);
3410
+ }
3411
+ d.out(`Start with hub: ${startWithHub ? "enabled" : "disabled"}`);
3412
+ return 0;
3413
+ }
3414
+
3415
+ // src/cli.ts
3416
+ var __dirname2 = path11.dirname(fileURLToPath2(import.meta.url));
2387
3417
  function authHeaders(token) {
2388
3418
  return token ? { Authorization: `Bearer ${token}` } : {};
2389
3419
  }
@@ -2407,6 +3437,10 @@ Usage:
2407
3437
  claude-alarm codex start Run the Codex adapter on its own (e.g. Codex on another PC)
2408
3438
  claude-alarm codex stop Stop a running Codex adapter
2409
3439
  claude-alarm codex status Show Codex adapter status
3440
+ claude-alarm codex run --brief <file|-> [--cwd <dir>] [--thread <id>] [--name <title>]
3441
+ [--output-schema <file>] [--approval-timeout <min>] [--timeout <min>] [--yolo]
3442
+ Hand a task to Codex through the shared daemon and print the result as JSON
3443
+ (--yolo: no sandbox and no approvals, like codex --yolo)
2410
3444
  claude-alarm help Show this help
2411
3445
 
2412
3446
  Quick start:
@@ -2415,8 +3449,8 @@ Quick start:
2415
3449
  }
2416
3450
  async function checkForUpdates() {
2417
3451
  try {
2418
- const pkgPath = path9.join(__dirname2, "..", "package.json");
2419
- const pkg = JSON.parse(fs8.readFileSync(pkgPath, "utf-8"));
3452
+ const pkgPath = path11.join(__dirname2, "..", "package.json");
3453
+ const pkg = JSON.parse(fs9.readFileSync(pkgPath, "utf-8"));
2420
3454
  const currentVersion = pkg.version;
2421
3455
  const res = await fetch("https://registry.npmjs.org/@delt/claude-alarm/latest", {
2422
3456
  signal: AbortSignal.timeout(3e3)
@@ -2437,36 +3471,52 @@ async function hubStart(daemon) {
2437
3471
  const config2 = loadConfig();
2438
3472
  const host = config2.hub.host ?? DEFAULT_HUB_HOST;
2439
3473
  const port = config2.hub.port ?? DEFAULT_HUB_PORT;
2440
- const displayHost = host === "0.0.0.0" ? "127.0.0.1" : host;
3474
+ const displayHost = hubUrlHost(host);
2441
3475
  checkForUpdates();
2442
- if (fs8.existsSync(PID_FILE)) {
2443
- const pid = parseInt(fs8.readFileSync(PID_FILE, "utf-8").trim(), 10);
3476
+ if (fs9.existsSync(PID_FILE)) {
3477
+ const pid = parseInt(fs9.readFileSync(PID_FILE, "utf-8").trim(), 10);
2444
3478
  if (isProcessRunning(pid)) {
2445
3479
  console.log(`Hub is already running (PID: ${pid}). Dashboard: ${loginLink(displayHost, port, config2.hub.token)}`);
2446
3480
  return;
2447
3481
  }
2448
- fs8.unlinkSync(PID_FILE);
3482
+ fs9.unlinkSync(PID_FILE);
2449
3483
  }
2450
3484
  if (daemon) {
2451
3485
  ensureConfigDir();
2452
- const logFd = fs8.openSync(LOG_FILE, "a");
2453
- const hubScript = path9.join(__dirname2, "hub", "server.js");
3486
+ const logFd = fs9.openSync(LOG_FILE, "a");
3487
+ const hubScript = path11.join(__dirname2, "hub", "server.js");
2454
3488
  const child = spawn3(process.execPath, [hubScript], {
2455
3489
  detached: true,
2456
3490
  stdio: ["ignore", logFd, logFd],
2457
3491
  env: { ...process.env }
2458
3492
  });
2459
- if (child.pid) {
2460
- fs8.writeFileSync(PID_FILE, String(child.pid), "utf-8");
2461
- child.unref();
2462
- console.log(`Hub started as daemon (PID: ${child.pid})`);
2463
- console.log(`Dashboard: ${loginLink(displayHost, port, config2.hub.token)}`);
2464
- console.log(`Token: ${config2.hub.token}`);
2465
- console.log(`Logs: ${LOG_FILE}`);
2466
- } else {
3493
+ const pid = child.pid;
3494
+ if (!pid) {
2467
3495
  console.error("Failed to start hub daemon");
2468
3496
  process.exit(1);
2469
3497
  }
3498
+ fs9.writeFileSync(PID_FILE, String(pid), "utf-8");
3499
+ let exited = false;
3500
+ child.once("exit", () => {
3501
+ exited = true;
3502
+ });
3503
+ child.unref();
3504
+ const startup = await waitForHub({
3505
+ url: `http://${displayHost}:${port}/api/status`,
3506
+ token: config2.hub.token,
3507
+ pid,
3508
+ isAlive: () => !exited
3509
+ });
3510
+ if (startup !== "ready") {
3511
+ if (!exited) child.kill();
3512
+ if (fs9.existsSync(PID_FILE)) fs9.unlinkSync(PID_FILE);
3513
+ console.error(startup === "exited" ? `Hub exited during startup. See ${LOG_FILE}` : `Hub did not answer at http://${displayHost}:${port} within ${HUB_START_TIMEOUT_MS / 1e3}s and was stopped. See ${LOG_FILE}`);
3514
+ process.exit(1);
3515
+ }
3516
+ console.log(`Hub started as daemon (PID: ${pid})`);
3517
+ console.log(`Dashboard: ${loginLink(displayHost, port, config2.hub.token)}`);
3518
+ console.log(`Token: ${config2.hub.token}`);
3519
+ console.log(`Logs: ${LOG_FILE}`);
2470
3520
  } else {
2471
3521
  console.log(`Starting hub on http://${displayHost}:${port} (press Ctrl+C to stop)`);
2472
3522
  console.log(`Dashboard: ${loginLink(displayHost, port, config2.hub.token)}`);
@@ -2478,11 +3528,11 @@ async function hubStart(daemon) {
2478
3528
  const hub = new HubServer2(config2);
2479
3529
  await hub.start();
2480
3530
  ensureConfigDir();
2481
- fs8.writeFileSync(PID_FILE, String(process.pid), "utf-8");
3531
+ fs9.writeFileSync(PID_FILE, String(process.pid), "utf-8");
2482
3532
  const shutdown = async () => {
2483
3533
  console.log("\nShutting down...");
2484
3534
  await hub.stop();
2485
- if (fs8.existsSync(PID_FILE)) fs8.unlinkSync(PID_FILE);
3535
+ if (fs9.existsSync(PID_FILE)) fs9.unlinkSync(PID_FILE);
2486
3536
  process.exit(0);
2487
3537
  };
2488
3538
  process.on("SIGINT", shutdown);
@@ -2490,27 +3540,27 @@ async function hubStart(daemon) {
2490
3540
  }
2491
3541
  }
2492
3542
  function hubStop() {
2493
- if (!fs8.existsSync(PID_FILE)) {
3543
+ if (!fs9.existsSync(PID_FILE)) {
2494
3544
  console.log("Hub is not running (no PID file found)");
2495
3545
  return;
2496
3546
  }
2497
- const pid = parseInt(fs8.readFileSync(PID_FILE, "utf-8").trim(), 10);
3547
+ const pid = parseInt(fs9.readFileSync(PID_FILE, "utf-8").trim(), 10);
2498
3548
  try {
2499
3549
  process.kill(pid, "SIGTERM");
2500
3550
  console.log(`Hub stopped (PID: ${pid})`);
2501
3551
  } catch {
2502
3552
  console.log("Hub process not found (may have already stopped)");
2503
3553
  }
2504
- fs8.unlinkSync(PID_FILE);
3554
+ fs9.unlinkSync(PID_FILE);
2505
3555
  }
2506
3556
  async function hubStatus() {
2507
3557
  const config2 = loadConfig();
2508
3558
  const host = config2.hub.host ?? DEFAULT_HUB_HOST;
2509
3559
  const port = config2.hub.port ?? DEFAULT_HUB_PORT;
2510
- const displayHost = host === "0.0.0.0" ? "127.0.0.1" : host;
3560
+ const displayHost = hubUrlHost(host);
2511
3561
  let pidInfo = "not running";
2512
- if (fs8.existsSync(PID_FILE)) {
2513
- const pid = parseInt(fs8.readFileSync(PID_FILE, "utf-8").trim(), 10);
3562
+ if (fs9.existsSync(PID_FILE)) {
3563
+ const pid = parseInt(fs9.readFileSync(PID_FILE, "utf-8").trim(), 10);
2514
3564
  if (isProcessRunning(pid)) {
2515
3565
  pidInfo = `running (PID: ${pid})`;
2516
3566
  } else {
@@ -2518,7 +3568,7 @@ async function hubStatus() {
2518
3568
  }
2519
3569
  }
2520
3570
  try {
2521
- const res = await fetch(`http://${host}:${port}/api/status`, { headers: authHeaders(config2.hub.token) });
3571
+ const res = await fetch(`http://${displayHost}:${port}/api/status`, { headers: authHeaders(config2.hub.token) });
2522
3572
  if (res.ok) {
2523
3573
  const data = await res.json();
2524
3574
  console.log(`Hub: running (PID: ${data.pid})`);
@@ -2553,7 +3603,7 @@ async function test() {
2553
3603
  if (config2.hub.token) {
2554
3604
  headers["Authorization"] = `Bearer ${config2.hub.token}`;
2555
3605
  }
2556
- const res = await fetch(`http://${host}:${port}/api/notify`, {
3606
+ const res = await fetch(`http://${hubUrlHost(host)}:${port}/api/notify`, {
2557
3607
  method: "POST",
2558
3608
  headers,
2559
3609
  body: JSON.stringify({
@@ -2571,7 +3621,7 @@ async function test() {
2571
3621
  console.error("Could not reach hub. Start it first: claude-alarm hub start");
2572
3622
  }
2573
3623
  }
2574
- function ask(question) {
3624
+ function ask2(question) {
2575
3625
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
2576
3626
  return new Promise((resolve) => {
2577
3627
  rl.question(question, (answer) => {
@@ -2582,18 +3632,18 @@ function ask(question) {
2582
3632
  }
2583
3633
  async function init() {
2584
3634
  const dir = process.cwd();
2585
- const projectName = path9.basename(dir);
3635
+ const projectName = path11.basename(dir);
2586
3636
  console.log(`
2587
3637
  claude-alarm init for "${projectName}"
2588
3638
  `);
2589
- const remote = await ask("Connect to a remote hub? (y/N): ");
3639
+ const remote = await ask2("Connect to a remote hub? (y/N): ");
2590
3640
  let env = {
2591
3641
  CLAUDE_ALARM_SESSION_NAME: projectName
2592
3642
  };
2593
3643
  if (remote.toLowerCase() === "y") {
2594
- const host = await ask("Hub host (e.g. 192.168.1.100): ");
2595
- const port = await ask("Hub port (default: 7900): ");
2596
- const token = await ask("Hub token: ");
3644
+ const host = await ask2("Hub host (e.g. 192.168.1.100): ");
3645
+ const port = await ask2("Hub port (default: 7900): ");
3646
+ const token = await ask2("Hub token: ");
2597
3647
  if (!host) {
2598
3648
  console.error("Host is required.");
2599
3649
  process.exit(1);
@@ -2602,11 +3652,11 @@ claude-alarm init for "${projectName}"
2602
3652
  if (port) env.CLAUDE_ALARM_HUB_PORT = port;
2603
3653
  if (token) env.CLAUDE_ALARM_HUB_TOKEN = token;
2604
3654
  }
2605
- const mcpPath = path9.join(dir, ".mcp.json");
3655
+ const mcpPath = path11.join(dir, ".mcp.json");
2606
3656
  let mcpConfig = {};
2607
- if (fs8.existsSync(mcpPath)) {
3657
+ if (fs9.existsSync(mcpPath)) {
2608
3658
  try {
2609
- mcpConfig = JSON.parse(fs8.readFileSync(mcpPath, "utf-8"));
3659
+ mcpConfig = JSON.parse(fs9.readFileSync(mcpPath, "utf-8"));
2610
3660
  } catch {
2611
3661
  mcpConfig = {};
2612
3662
  }
@@ -2617,17 +3667,17 @@ claude-alarm init for "${projectName}"
2617
3667
  args: ["-y", "@delt/claude-alarm", "serve"],
2618
3668
  env
2619
3669
  };
2620
- fs8.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2), "utf-8");
3670
+ fs9.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2), "utf-8");
2621
3671
  console.log(`
2622
3672
  \u2713 Created ${mcpPath}`);
2623
3673
  if (remote.toLowerCase() !== "y") {
2624
3674
  const config2 = loadConfig();
2625
3675
  const host = config2.hub.host ?? DEFAULT_HUB_HOST;
2626
3676
  const port = config2.hub.port ?? DEFAULT_HUB_PORT;
2627
- const displayHost = host === "0.0.0.0" ? "127.0.0.1" : host;
3677
+ const displayHost = hubUrlHost(host);
2628
3678
  let hubRunning = false;
2629
3679
  try {
2630
- const res = await fetch(`http://${host}:${port}/api/status`, { headers: authHeaders(config2.hub.token) });
3680
+ const res = await fetch(`http://${displayHost}:${port}/api/status`, { headers: authHeaders(config2.hub.token) });
2631
3681
  hubRunning = res.ok;
2632
3682
  } catch {
2633
3683
  }
@@ -2638,8 +3688,8 @@ claude-alarm init for "${projectName}"
2638
3688
  console.log(` claude-alarm hub start`);
2639
3689
  }
2640
3690
  console.log(` Dashboard: http://${displayHost}:${port}`);
2641
- if (shouldOfferCodex(config2, findOnPath("codex") !== void 0)) {
2642
- const answer = await ask("\nCodex is installed. Show Codex conversations on the dashboard too? (y/N): ");
3691
+ if (shouldOfferCodex(config2, findCodex("codex") !== void 0)) {
3692
+ const answer = await ask2("\nCodex is installed. Show Codex conversations on the dashboard too? (y/N): ");
2643
3693
  if (answer.toLowerCase() === "y") codexEnable(true);
2644
3694
  else setCodexEnabled(false);
2645
3695
  }
@@ -2668,53 +3718,80 @@ function isProcessRunning(pid) {
2668
3718
  }
2669
3719
  }
2670
3720
  function readCodexPid() {
2671
- if (!fs8.existsSync(CODEX_PID_FILE)) return void 0;
2672
- const pid = parseInt(fs8.readFileSync(CODEX_PID_FILE, "utf-8").trim(), 10);
3721
+ if (!fs9.existsSync(CODEX_PID_FILE)) return void 0;
3722
+ const pid = parseInt(fs9.readFileSync(CODEX_PID_FILE, "utf-8").trim(), 10);
2673
3723
  return Number.isNaN(pid) ? void 0 : pid;
2674
3724
  }
2675
3725
  function codexEnable(enabled) {
2676
3726
  setCodexEnabled(enabled);
2677
3727
  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.");
2678
3728
  }
2679
- function codexStart() {
2680
- const pid = readCodexPid();
2681
- if (pid !== void 0 && isProcessRunning(pid)) {
2682
- console.log(`Codex adapter is already running (PID: ${pid})`);
2683
- return;
2684
- }
3729
+ async function codexStart() {
3730
+ const config2 = loadConfig();
2685
3731
  ensureConfigDir();
2686
- const logFd = fs8.openSync(CODEX_LOG_FILE, "a");
2687
- const child = spawn3(process.execPath, [path9.join(__dirname2, "codex", "main.js")], {
2688
- detached: true,
2689
- stdio: ["ignore", logFd, logFd],
2690
- env: { ...process.env }
3732
+ legacyNote(controlDeps());
3733
+ const code = await startAdapter({
3734
+ hub: resolveAdapterHub(config2),
3735
+ command: config2.codex?.command ?? "codex",
3736
+ logFile: CODEX_LOG_FILE,
3737
+ queryOwner: () => queryOwner(controlEndpoint(CONFIG_DIR)),
3738
+ spawnAdapter: () => {
3739
+ const logFd = fs9.openSync(CODEX_LOG_FILE, "a");
3740
+ return spawn3(process.execPath, [path11.join(__dirname2, "codex", "main.js")], {
3741
+ detached: true,
3742
+ stdio: ["ignore", logFd, logFd, "ipc"],
3743
+ windowsHide: true,
3744
+ env: { ...process.env }
3745
+ });
3746
+ },
3747
+ out: (line) => console.log(line),
3748
+ err: (line) => console.error(line)
2691
3749
  });
2692
- child.unref();
2693
- console.log(`Codex adapter started (PID: ${child.pid}). Logs: ${CODEX_LOG_FILE}`);
3750
+ if (code !== 0) process.exit(code);
2694
3751
  }
2695
- function codexStop() {
2696
- const pid = readCodexPid();
2697
- if (pid === void 0) {
2698
- console.log("Codex adapter is not running");
2699
- return;
2700
- }
2701
- try {
2702
- process.kill(pid, "SIGTERM");
2703
- console.log(`Codex adapter stopped (PID: ${pid})`);
2704
- } catch {
2705
- console.log("Codex adapter process not found (may have already stopped)");
2706
- }
3752
+ function controlDeps() {
3753
+ const endpoint = controlEndpoint(CONFIG_DIR);
3754
+ const token = loadConfig().hub.token ?? "";
3755
+ return {
3756
+ queryOwner: () => queryOwner(endpoint),
3757
+ requestStop: () => requestStop(endpoint, token),
3758
+ legacyPid: readCodexPid,
3759
+ isRunning: isProcessRunning,
3760
+ removeLegacyPidFile: () => {
3761
+ try {
3762
+ fs9.unlinkSync(CODEX_PID_FILE);
3763
+ } catch {
3764
+ }
3765
+ },
3766
+ legacyPidFile: CODEX_PID_FILE,
3767
+ configFile: CONFIG_FILE,
3768
+ out: (line) => console.log(line),
3769
+ err: (line) => console.error(line)
3770
+ };
3771
+ }
3772
+ async function codexRun(argv) {
3773
+ const abort = new AbortController();
3774
+ const stop = () => abort.abort();
3775
+ process.on("SIGINT", stop);
3776
+ process.on("SIGTERM", stop);
2707
3777
  try {
2708
- fs8.unlinkSync(CODEX_PID_FILE);
2709
- } catch {
3778
+ return await runFromCli(argv, {
3779
+ readFile: (file) => fs9.promises.readFile(file, "utf-8"),
3780
+ readStdin: async () => {
3781
+ let text = "";
3782
+ for await (const chunk of process.stdin) text += chunk;
3783
+ return text;
3784
+ },
3785
+ out: (text) => process.stdout.write(text),
3786
+ err: (text) => process.stderr.write(text),
3787
+ cwd: process.cwd(),
3788
+ signal: abort.signal
3789
+ });
3790
+ } finally {
3791
+ process.off("SIGINT", stop);
3792
+ process.off("SIGTERM", stop);
2710
3793
  }
2711
3794
  }
2712
- function codexStatus() {
2713
- const pid = readCodexPid();
2714
- const state = pid === void 0 ? "not running" : isProcessRunning(pid) ? `running (PID: ${pid})` : "not running (stale PID file)";
2715
- console.log(`Codex adapter: ${state}`);
2716
- console.log(`Start with hub: ${loadConfig().codex?.enabled ? "enabled" : "disabled"}`);
2717
- }
2718
3795
  async function main2() {
2719
3796
  const args = process.argv.slice(2);
2720
3797
  const cmd = args[0];
@@ -2749,9 +3826,10 @@ async function main2() {
2749
3826
  if (cmd === "codex") {
2750
3827
  if (sub === "enable") codexEnable(true);
2751
3828
  else if (sub === "disable") codexEnable(false);
2752
- else if (sub === "start") codexStart();
2753
- else if (sub === "stop") codexStop();
2754
- else if (sub === "status") codexStatus();
3829
+ else if (sub === "start") await codexStart();
3830
+ else if (sub === "stop") process.exitCode = await stopAdapter(controlDeps(), loadConfig().codex?.enabled === true);
3831
+ else if (sub === "status") process.exitCode = await adapterStatus(controlDeps(), loadConfig().codex?.enabled === true);
3832
+ else if (sub === "run") process.exitCode = await codexRun(args.slice(2));
2755
3833
  else {
2756
3834
  console.error(`Unknown codex command: ${sub}`);
2757
3835
  printUsage();