@delt/claude-alarm 1.1.1 → 1.2.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;
@@ -425,7 +440,7 @@ var init_permission_key = __esm({
425
440
 
426
441
  // src/hub/telegram.ts
427
442
  import fs4 from "fs";
428
- import path4 from "path";
443
+ import path5 from "path";
429
444
  import { randomUUID as randomUUID2 } from "crypto";
430
445
  var TELEGRAM_API, MAX_CHOICE_MESSAGES, TelegramBot;
431
446
  var init_telegram = __esm({
@@ -653,7 +668,7 @@ ${this.mdToHtml(message)}`;
653
668
  const mimeType = ext === "png" ? "image/png" : ext === "gif" ? "image/gif" : ext === "webp" ? "image/webp" : "image/jpeg";
654
669
  fs4.mkdirSync(UPLOADS_DIR, { recursive: true });
655
670
  const filename = `${randomUUID2()}.${ext}`;
656
- const filePath = path4.join(UPLOADS_DIR, filename);
671
+ const filePath = path5.join(UPLOADS_DIR, filename);
657
672
  fs4.writeFileSync(filePath, buffer);
658
673
  logger.info(`Telegram photo saved: ${filename} (${buffer.length} bytes)`);
659
674
  if (this.onImageToSession) {
@@ -913,8 +928,8 @@ ${label}`);
913
928
  css: "css",
914
929
  scss: "scss"
915
930
  };
916
- langOf(path10) {
917
- const m = /\.([a-z0-9]+)$/i.exec(path10 || "");
931
+ langOf(path11) {
932
+ const m = /\.([a-z0-9]+)$/i.exec(path11 || "");
918
933
  return m ? _TelegramBot.LANG_BY_EXT[m[1].toLowerCase()] ?? "" : "";
919
934
  }
920
935
  /** Convert markdown to Telegram HTML (escape first, then apply formatting) */
@@ -962,9 +977,9 @@ ${bodyLines.join("\n")}`;
962
977
  // src/hub/codex-supervisor.ts
963
978
  import { spawn as spawn2 } from "child_process";
964
979
  import fs5 from "fs";
965
- import path5 from "path";
980
+ import path6 from "path";
966
981
  function resolveAdapterScript(baseDir) {
967
- return [path5.join(baseDir, "..", "codex", "main.js"), path5.join(baseDir, "codex", "main.js")].find((p) => fs5.existsSync(p));
982
+ return [path6.join(baseDir, "..", "codex", "main.js"), path6.join(baseDir, "codex", "main.js")].find((p) => fs5.existsSync(p));
968
983
  }
969
984
  var HEALTHY_RUN_MS, CodexSupervisor;
970
985
  var init_codex_supervisor = __esm({
@@ -1030,6 +1045,18 @@ var init_codex_supervisor = __esm({
1030
1045
  }
1031
1046
  });
1032
1047
 
1048
+ // src/shared/entry.ts
1049
+ function isEntryScript(argv1, suffixes) {
1050
+ if (!argv1) return false;
1051
+ const normalized = argv1.replace(/\\/g, "/");
1052
+ return suffixes.some((suffix) => normalized.endsWith(suffix));
1053
+ }
1054
+ var init_entry = __esm({
1055
+ "src/shared/entry.ts"() {
1056
+ "use strict";
1057
+ }
1058
+ });
1059
+
1033
1060
  // src/hub/auth.ts
1034
1061
  import crypto from "crypto";
1035
1062
  function safeEqual(a, b) {
@@ -1100,7 +1127,7 @@ __export(server_exports, {
1100
1127
  });
1101
1128
  import http from "http";
1102
1129
  import fs6 from "fs";
1103
- import path6 from "path";
1130
+ import path7 from "path";
1104
1131
  import { fileURLToPath } from "url";
1105
1132
  import { WebSocketServer, WebSocket as WebSocket2 } from "ws";
1106
1133
  import { randomUUID as randomUUID3 } from "crypto";
@@ -1109,7 +1136,7 @@ function validChoices(choices) {
1109
1136
  const valid = choices.filter((c) => typeof c?.id === "string" && typeof c?.label === "string");
1110
1137
  return valid.length ? valid : void 0;
1111
1138
  }
1112
- var __dirname, MAX_WS_PAYLOAD, HubServer;
1139
+ var __dirname, MAX_WS_PAYLOAD, nonEmpty, HubServer;
1113
1140
  var init_server = __esm({
1114
1141
  "src/hub/server.ts"() {
1115
1142
  "use strict";
@@ -1123,9 +1150,12 @@ var init_server = __esm({
1123
1150
  init_session_label();
1124
1151
  init_crash_guard();
1125
1152
  init_permission_key();
1153
+ init_entry();
1154
+ init_hub_url();
1126
1155
  init_auth();
1127
- __dirname = path6.dirname(fileURLToPath(import.meta.url));
1156
+ __dirname = path7.dirname(fileURLToPath(import.meta.url));
1128
1157
  MAX_WS_PAYLOAD = 16 * 1024 * 1024;
1158
+ nonEmpty = (v) => typeof v === "string" && v.trim() !== "";
1129
1159
  HubServer = class {
1130
1160
  httpServer;
1131
1161
  wssChannel;
@@ -1151,7 +1181,13 @@ var init_server = __esm({
1151
1181
  token;
1152
1182
  codexEnabled;
1153
1183
  codexSupervisor;
1154
- constructor(config2) {
1184
+ wssCodex;
1185
+ codexAdapters = /* @__PURE__ */ new Map();
1186
+ codexAlive = /* @__PURE__ */ new WeakMap();
1187
+ codexCallTimeoutMs;
1188
+ codexCalls = /* @__PURE__ */ new Map();
1189
+ constructor(config2, options = {}) {
1190
+ this.codexCallTimeoutMs = options.codexCallTimeoutMs ?? 6e4;
1155
1191
  this.host = config2?.hub?.host ?? DEFAULT_HUB_HOST;
1156
1192
  this.port = config2?.hub?.port ?? DEFAULT_HUB_PORT;
1157
1193
  this.token = config2?.hub?.token;
@@ -1164,7 +1200,7 @@ var init_server = __esm({
1164
1200
  if (config2?.webhooks) {
1165
1201
  this.notifier.configure({ webhooks: config2.webhooks });
1166
1202
  }
1167
- const displayHost = this.host === "0.0.0.0" ? "127.0.0.1" : this.host;
1203
+ const displayHost = hubUrlHost(this.host);
1168
1204
  this.notifier.configure({ dashboardUrl: `http://${displayHost}:${this.port}` });
1169
1205
  const fullConfig = loadConfig();
1170
1206
  if (fullConfig.telegram?.enabled && fullConfig.telegram.botToken && fullConfig.telegram.chatId) {
@@ -1177,6 +1213,9 @@ var init_server = __esm({
1177
1213
  this.wssDashboard = new WebSocketServer({ noServer: true, maxPayload: MAX_WS_PAYLOAD });
1178
1214
  this.wssDashboard.on("connection", (ws) => this.handleDashboardConnection(ws));
1179
1215
  this.wssDashboard.on("error", (err) => logger.warn(`Dashboard WebSocket server error: ${err.message}`));
1216
+ this.wssCodex = new WebSocketServer({ noServer: true, maxPayload: MAX_WS_PAYLOAD });
1217
+ this.wssCodex.on("connection", (ws, req) => this.handleCodexConnection(ws, req));
1218
+ this.wssCodex.on("error", (err) => logger.warn(`Codex WebSocket server error: ${err.message}`));
1180
1219
  this.httpServer.on("upgrade", (req, socket, head) => {
1181
1220
  socket.on("error", (err) => logger.debug(`Upgrade socket error: ${err.message}`));
1182
1221
  const url = new URL(req.url ?? "/", "http://hub");
@@ -1197,6 +1236,10 @@ var init_server = __esm({
1197
1236
  this.wssDashboard.handleUpgrade(req, socket, head, (ws) => {
1198
1237
  this.wssDashboard.emit("connection", ws, req);
1199
1238
  });
1239
+ } else if (pathname === WS_PATH_CODEX) {
1240
+ this.wssCodex.handleUpgrade(req, socket, head, (ws) => {
1241
+ this.wssCodex.emit("connection", ws, req);
1242
+ });
1200
1243
  } else {
1201
1244
  socket.destroy();
1202
1245
  }
@@ -1211,7 +1254,7 @@ var init_server = __esm({
1211
1254
  this.httpServer.listen(this.port, this.host, () => {
1212
1255
  this.httpServer.removeListener("error", onStartupError);
1213
1256
  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;
1257
+ const displayHost = hubUrlHost(this.host);
1215
1258
  logger.info(`Hub server listening on http://${displayHost}:${this.port}`);
1216
1259
  this.startCodexAdapter();
1217
1260
  resolve();
@@ -1226,10 +1269,12 @@ var init_server = __esm({
1226
1269
  if (this.telegramBot) this.telegramBot.stopPolling();
1227
1270
  for (const ws of this.channelSockets.values()) ws.terminate();
1228
1271
  for (const ws of this.dashboardSockets) ws.terminate();
1272
+ for (const { ws } of this.codexAdapters.values()) ws.terminate();
1229
1273
  this.channelSockets.clear();
1230
1274
  this.dashboardSockets.clear();
1231
1275
  this.wssChannel.close();
1232
1276
  this.wssDashboard.close();
1277
+ this.wssCodex.close();
1233
1278
  this.httpServer.close(() => {
1234
1279
  logger.info("Hub server stopped");
1235
1280
  resolve();
@@ -1294,6 +1339,12 @@ var init_server = __esm({
1294
1339
  this.handleApiSend(req, res);
1295
1340
  } else if (url.pathname === "/api/notify" && req.method === "POST") {
1296
1341
  this.handleApiNotify(req, res);
1342
+ } else if (url.pathname === "/api/codex/folders" && req.method === "GET") {
1343
+ this.handleCodexFolders(url, res);
1344
+ } else if (url.pathname === "/api/codex/threads" && req.method === "POST") {
1345
+ this.handleCodexCreate(req, res);
1346
+ } else if (url.pathname === "/api/codex/threads/close" && req.method === "POST") {
1347
+ this.handleCodexClose(req, res);
1297
1348
  } else if (url.pathname === "/api/webhooks" && req.method === "GET") {
1298
1349
  const config2 = loadConfig();
1299
1350
  this.jsonResponse(res, 200, { webhooks: config2.webhooks || [] });
@@ -1323,17 +1374,17 @@ var init_server = __esm({
1323
1374
  return;
1324
1375
  }
1325
1376
  const candidates = [
1326
- path6.join(__dirname, "..", "dashboard", "index.html"),
1377
+ path7.join(__dirname, "..", "dashboard", "index.html"),
1327
1378
  // from dist/hub/
1328
- path6.join(__dirname, "dashboard", "index.html"),
1379
+ path7.join(__dirname, "dashboard", "index.html"),
1329
1380
  // from dist/ (bundled index.js)
1330
- path6.join(__dirname, "..", "..", "src", "dashboard", "index.html"),
1381
+ path7.join(__dirname, "..", "..", "src", "dashboard", "index.html"),
1331
1382
  // from dist/hub/ -> src/
1332
- path6.join(__dirname, "..", "src", "dashboard", "index.html"),
1383
+ path7.join(__dirname, "..", "src", "dashboard", "index.html"),
1333
1384
  // from dist/ -> src/
1334
- path6.join(process.cwd(), "dist", "dashboard", "index.html"),
1385
+ path7.join(process.cwd(), "dist", "dashboard", "index.html"),
1335
1386
  // from cwd
1336
- path6.join(process.cwd(), "src", "dashboard", "index.html")
1387
+ path7.join(process.cwd(), "src", "dashboard", "index.html")
1337
1388
  // from cwd/src
1338
1389
  ];
1339
1390
  logger.debug(`Dashboard candidates: ${JSON.stringify(candidates)}`);
@@ -1557,6 +1608,114 @@ var init_server = __esm({
1557
1608
  }
1558
1609
  }
1559
1610
  }
1611
+ handleCodexConnection(ws, req) {
1612
+ const isLocal = this.isLocalRequest(req);
1613
+ let adapterId;
1614
+ ws.on("error", (err) => {
1615
+ logger.warn(`Codex adapter WebSocket error: ${err.message}`);
1616
+ ws.terminate();
1617
+ });
1618
+ ws.on("pong", () => this.codexAlive.set(ws, true));
1619
+ ws.on("message", (data) => {
1620
+ let msg;
1621
+ try {
1622
+ msg = JSON.parse(data.toString());
1623
+ if (msg === null || typeof msg !== "object") {
1624
+ logger.warn("Invalid message from Codex adapter");
1625
+ return;
1626
+ }
1627
+ } catch {
1628
+ logger.warn("Invalid message from Codex adapter");
1629
+ return;
1630
+ }
1631
+ if (msg.type === "adapter_hello") {
1632
+ const a = msg.adapter;
1633
+ if (typeof a?.id !== "string" || !a.id || typeof a.host !== "string") return;
1634
+ if (adapterId && adapterId !== a.id) return;
1635
+ const holder = this.codexAdapters.get(a.id);
1636
+ if (holder && holder.ws !== ws) holder.ws.terminate();
1637
+ if (!adapterId) logger.info(`Codex adapter connected: ${a.host} (${a.id}, local: ${isLocal})`);
1638
+ adapterId = a.id;
1639
+ this.codexAdapters.set(a.id, { ws, info: { id: a.id, host: a.host, ready: a.ready === true, isLocal } });
1640
+ this.broadcastCodexAdapters();
1641
+ } else if (msg.type === "adapter_result" && adapterId) {
1642
+ const call = this.codexCalls.get(msg.requestId);
1643
+ if (!call || call.adapterId !== adapterId) return;
1644
+ call.settle(msg.ok ? { status: 200, body: msg.data } : { status: 422, body: { error: String(msg.error) } });
1645
+ }
1646
+ });
1647
+ ws.on("close", () => {
1648
+ if (!adapterId || this.codexAdapters.get(adapterId)?.ws !== ws) return;
1649
+ this.codexAdapters.delete(adapterId);
1650
+ for (const call of [...this.codexCalls.values()]) {
1651
+ if (call.adapterId === adapterId) call.settle({ status: 502, body: { error: "Codex adapter disconnected" } });
1652
+ }
1653
+ logger.info(`Codex adapter disconnected: ${adapterId}`);
1654
+ this.broadcastCodexAdapters();
1655
+ });
1656
+ }
1657
+ callCodexAdapter(adapterId, call) {
1658
+ const adapter = this.codexAdapters.get(adapterId);
1659
+ if (!adapter || adapter.ws.readyState !== WebSocket2.OPEN) {
1660
+ return Promise.resolve({ status: 404, body: { error: "Codex adapter is not connected" } });
1661
+ }
1662
+ const requestId = randomUUID3();
1663
+ return new Promise((resolve) => {
1664
+ const timer = setTimeout(
1665
+ () => settle({ status: 504, body: { error: "Codex did not respond in time. The conversation may still appear." } }),
1666
+ this.codexCallTimeoutMs
1667
+ );
1668
+ const settle = (outcome) => {
1669
+ clearTimeout(timer);
1670
+ this.codexCalls.delete(requestId);
1671
+ resolve(outcome);
1672
+ };
1673
+ this.codexCalls.set(requestId, { adapterId, settle });
1674
+ adapter.ws.send(JSON.stringify({ type: "adapter_call", requestId, call }));
1675
+ });
1676
+ }
1677
+ async handleCodexFolders(url, res) {
1678
+ const adapterId = url.searchParams.get("adapterId");
1679
+ if (!nonEmpty(adapterId)) {
1680
+ this.jsonResponse(res, 400, { error: "adapterId is required" });
1681
+ return;
1682
+ }
1683
+ const out = await this.callCodexAdapter(adapterId, { kind: "folders" });
1684
+ this.jsonResponse(res, out.status, out.body);
1685
+ }
1686
+ async handleCodexCreate(req, res) {
1687
+ const body = await this.readBody(req);
1688
+ const adapterId = body?.adapterId;
1689
+ const cwd = body?.cwd;
1690
+ if (!nonEmpty(adapterId) || !nonEmpty(cwd)) {
1691
+ this.jsonResponse(res, 400, { error: "adapterId and cwd are required" });
1692
+ return;
1693
+ }
1694
+ const out = await this.callCodexAdapter(adapterId, { kind: "create", cwd });
1695
+ this.jsonResponse(res, out.status, out.body);
1696
+ }
1697
+ async handleCodexClose(req, res) {
1698
+ const body = await this.readBody(req);
1699
+ const sessionId2 = body?.sessionId;
1700
+ if (!nonEmpty(sessionId2)) {
1701
+ this.jsonResponse(res, 400, { error: "sessionId is required" });
1702
+ return;
1703
+ }
1704
+ const session = this.sessions.get(sessionId2);
1705
+ const ws = this.channelSockets.get(sessionId2);
1706
+ if (session?.agentKind !== "codex" || !session.closable || ws?.readyState !== WebSocket2.OPEN) {
1707
+ this.jsonResponse(res, 404, { error: "No closable Codex conversation" });
1708
+ return;
1709
+ }
1710
+ ws.send(JSON.stringify({ type: "codex_close", sessionId: sessionId2 }));
1711
+ this.jsonResponse(res, 200, { ok: true });
1712
+ }
1713
+ codexAdapterList() {
1714
+ return [...this.codexAdapters.values()].map((a) => a.info);
1715
+ }
1716
+ broadcastCodexAdapters() {
1717
+ this.broadcastToDashboards({ type: "codex_adapters", adapters: this.codexAdapterList() });
1718
+ }
1560
1719
  // --- Dashboard WebSocket ---
1561
1720
  handleDashboardConnection(ws) {
1562
1721
  this.dashboardSockets.add(ws);
@@ -1575,6 +1734,7 @@ var init_server = __esm({
1575
1734
  requests: [...this.choiceRequests.values()].map((c) => c.request)
1576
1735
  };
1577
1736
  ws.send(JSON.stringify(pendingMsg));
1737
+ ws.send(JSON.stringify({ type: "codex_adapters", adapters: this.codexAdapterList() }));
1578
1738
  ws.on("message", (data) => {
1579
1739
  try {
1580
1740
  const msg = JSON.parse(data.toString());
@@ -1654,7 +1814,7 @@ var init_server = __esm({
1654
1814
  fs6.mkdirSync(UPLOADS_DIR, { recursive: true });
1655
1815
  const ext = mimeType.split("/")[1] === "jpeg" ? "jpg" : mimeType.split("/")[1];
1656
1816
  const filename = `${randomUUID3()}.${ext}`;
1657
- const filePath = path6.join(UPLOADS_DIR, filename);
1817
+ const filePath = path7.join(UPLOADS_DIR, filename);
1658
1818
  fs6.writeFileSync(filePath, buffer);
1659
1819
  const forwardMsg = {
1660
1820
  type: "image_to_session",
@@ -1662,7 +1822,8 @@ var init_server = __esm({
1662
1822
  imagePath: filePath,
1663
1823
  mimeType,
1664
1824
  originalName,
1665
- content
1825
+ content,
1826
+ source: "dashboard"
1666
1827
  };
1667
1828
  channelWs.send(JSON.stringify(forwardMsg));
1668
1829
  logger.info(`Image saved and forwarded: ${filename} (${buffer.length} bytes)`);
@@ -1671,7 +1832,7 @@ var init_server = __esm({
1671
1832
  fs6.unlinkSync(filePath);
1672
1833
  } catch {
1673
1834
  }
1674
- }, 5 * 60 * 1e3);
1835
+ }, 5 * 60 * 1e3).unref();
1675
1836
  }
1676
1837
  async handleWebhookSave(req, res) {
1677
1838
  const body = await this.readBody(req);
@@ -1704,7 +1865,7 @@ var init_server = __esm({
1704
1865
  this.telegramBot.onImageToSession = (sessionId2, imagePath, mimeType, caption) => {
1705
1866
  const channelWs = this.channelSockets.get(sessionId2);
1706
1867
  if (channelWs?.readyState === WebSocket2.OPEN) {
1707
- const msg = { type: "image_to_session", sessionId: sessionId2, imagePath, mimeType, content: caption };
1868
+ const msg = { type: "image_to_session", sessionId: sessionId2, imagePath, mimeType, content: caption, source: "telegram" };
1708
1869
  channelWs.send(JSON.stringify(msg));
1709
1870
  logger.info(`Telegram photo forwarded to session: ${sessionId2}`);
1710
1871
  }
@@ -1847,6 +2008,14 @@ var init_server = __esm({
1847
2008
  this.channelAlive.set(sessionId2, false);
1848
2009
  ws.ping();
1849
2010
  }
2011
+ for (const { ws } of this.codexAdapters.values()) {
2012
+ if (this.codexAlive.get(ws) === false) {
2013
+ ws.terminate();
2014
+ continue;
2015
+ }
2016
+ this.codexAlive.set(ws, false);
2017
+ ws.ping();
2018
+ }
1850
2019
  }, 3e4);
1851
2020
  }
1852
2021
  cleanupUploads() {
@@ -1855,7 +2024,7 @@ var init_server = __esm({
1855
2024
  const files = fs6.readdirSync(UPLOADS_DIR);
1856
2025
  for (const file of files) {
1857
2026
  try {
1858
- fs6.unlinkSync(path6.join(UPLOADS_DIR, file));
2027
+ fs6.unlinkSync(path7.join(UPLOADS_DIR, file));
1859
2028
  } catch {
1860
2029
  }
1861
2030
  }
@@ -1909,7 +2078,7 @@ Content-Length: 0\r
1909
2078
  });
1910
2079
  }
1911
2080
  };
1912
- if (process.argv[1] && (process.argv[1].endsWith("hub/server.js") || process.argv[1].endsWith("hub/server.ts"))) {
2081
+ if (isEntryScript(process.argv[1], ["/hub/server.js", "/hub/server.ts"])) {
1913
2082
  installCrashGuard("hub daemon");
1914
2083
  logStartup("Hub daemon");
1915
2084
  const config2 = loadConfig();
@@ -2056,7 +2225,7 @@ var init_hub_client = __esm({
2056
2225
  // src/channel/peer-name.ts
2057
2226
  import fs7 from "fs";
2058
2227
  import os2 from "os";
2059
- import path7 from "path";
2228
+ import path8 from "path";
2060
2229
  function findPeerName(records, env) {
2061
2230
  const valid = records.filter((r) => typeof r === "object" && r !== null);
2062
2231
  const nameOf = (match) => {
@@ -2076,8 +2245,8 @@ function readPeerName(env = process.env) {
2076
2245
  sessionId: env.CLAUDE_CODE_SESSION_ID
2077
2246
  };
2078
2247
  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");
2248
+ const configDir = env.CLAUDE_CONFIG_DIR || path8.join(os2.homedir(), ".claude");
2249
+ const dir = path8.join(configDir, "sessions");
2081
2250
  let files;
2082
2251
  try {
2083
2252
  files = fs7.readdirSync(dir).filter((f) => f.endsWith(".json"));
@@ -2087,7 +2256,7 @@ function readPeerName(env = process.env) {
2087
2256
  const records = [];
2088
2257
  for (const f of files) {
2089
2258
  try {
2090
- records.push(JSON.parse(fs7.readFileSync(path7.join(dir, f), "utf8")));
2259
+ records.push(JSON.parse(fs7.readFileSync(path8.join(dir, f), "utf8")));
2091
2260
  } catch {
2092
2261
  }
2093
2262
  }
@@ -2110,7 +2279,7 @@ import {
2110
2279
  } from "@modelcontextprotocol/sdk/types.js";
2111
2280
  import { z } from "zod";
2112
2281
  import { randomUUID as randomUUID4 } from "crypto";
2113
- import path8 from "path";
2282
+ import path9 from "path";
2114
2283
  async function main() {
2115
2284
  logger.info(`Starting MCP channel server (session: ${sessionId})`);
2116
2285
  hubClient.connect();
@@ -2179,7 +2348,7 @@ var init_server2 = __esm({
2179
2348
  init_hub_client();
2180
2349
  init_peer_name2();
2181
2350
  sessionId = randomUUID4();
2182
- sessionName = process.env.CLAUDE_ALARM_SESSION_NAME ?? path8.basename(process.cwd());
2351
+ sessionName = process.env.CLAUDE_ALARM_SESSION_NAME ?? path9.basename(process.cwd());
2183
2352
  peerName = readPeerName();
2184
2353
  server = new Server(
2185
2354
  {
@@ -2357,7 +2526,7 @@ NOTIFICATIONS:
2357
2526
  init_config();
2358
2527
  import { spawn as spawn3 } from "child_process";
2359
2528
  import fs8 from "fs";
2360
- import path9 from "path";
2529
+ import path10 from "path";
2361
2530
  import readline from "readline";
2362
2531
  import { fileURLToPath as fileURLToPath2 } from "url";
2363
2532
 
@@ -2368,6 +2537,11 @@ import fs2 from "fs";
2368
2537
  import path3 from "path";
2369
2538
  import { Duplex } from "stream";
2370
2539
  import WebSocket from "ws";
2540
+ function resolveCommand(command, platform = process.platform, env = process.env) {
2541
+ if (platform !== "win32" || /[\\/]/.test(command) || path3.extname(command)) return { file: command, shell: false };
2542
+ const file = findCodex(command, platform, env);
2543
+ return file ? { file, shell: file.endsWith(".cmd") } : { file: command, shell: false };
2544
+ }
2371
2545
  function findOnPath(command, platform = process.platform, env = process.env) {
2372
2546
  const dirs = (env.PATH ?? env.Path ?? "").split(path3.delimiter).filter(Boolean);
2373
2547
  for (const ext of platform === "win32" ? [".exe", ".cmd"] : [""]) {
@@ -2378,12 +2552,620 @@ function findOnPath(command, platform = process.platform, env = process.env) {
2378
2552
  }
2379
2553
  return void 0;
2380
2554
  }
2555
+ function findCodex(command, platform = process.platform, env = process.env) {
2556
+ const onPath = findOnPath(command, platform, env);
2557
+ if (onPath || platform !== "win32") return onPath;
2558
+ const candidates = [
2559
+ env.LOCALAPPDATA && path3.join(env.LOCALAPPDATA, "Programs", "OpenAI", "Codex", "bin", `${command}.exe`),
2560
+ env.APPDATA && path3.join(env.APPDATA, "npm", `${command}.cmd`)
2561
+ ];
2562
+ return candidates.find((file) => !!file && fs2.existsSync(file));
2563
+ }
2564
+ var defaultSpawn = (command, args) => {
2565
+ const { file, shell } = resolveCommand(command);
2566
+ if (shell) return spawn(`"${file}" ${args.join(" ")}`, { stdio: "pipe", windowsHide: true, shell: true });
2567
+ return spawn(file, args, { stdio: "pipe", windowsHide: true });
2568
+ };
2569
+ function connectProxy(command, spawnFn = defaultSpawn) {
2570
+ const child = spawnFn(command, ["app-server", "proxy"]);
2571
+ child.stdin?.on("error", () => {
2572
+ });
2573
+ child.stderr?.on("data", (d) => logger.debug(`codex proxy: ${String(d).trim()}`));
2574
+ const stream = new Duplex({
2575
+ read() {
2576
+ },
2577
+ write(chunk, _encoding, callback) {
2578
+ if (child.stdin?.writable) child.stdin.write(chunk, callback);
2579
+ else callback();
2580
+ },
2581
+ final(callback) {
2582
+ child.stdin?.end();
2583
+ callback();
2584
+ }
2585
+ });
2586
+ child.stdout?.on("data", (d) => stream.push(d));
2587
+ child.stdout?.on("end", () => stream.push(null));
2588
+ Object.assign(stream, { setNoDelay() {
2589
+ }, setTimeout() {
2590
+ }, setKeepAlive() {
2591
+ }, ref() {
2592
+ }, unref() {
2593
+ } });
2594
+ const ws = new WebSocket("ws://localhost/", {
2595
+ createConnection: (() => stream)
2596
+ });
2597
+ const close = () => {
2598
+ ws.terminate();
2599
+ child.kill();
2600
+ };
2601
+ return new Promise((resolve, reject) => {
2602
+ let settled = false;
2603
+ const fail = (err) => {
2604
+ if (settled) return;
2605
+ settled = true;
2606
+ close();
2607
+ reject(err);
2608
+ };
2609
+ child.on("error", fail);
2610
+ child.once("exit", (code) => fail(new Error(`codex proxy exited (code ${code})`)));
2611
+ ws.on("error", fail);
2612
+ ws.once("open", () => {
2613
+ if (settled) return;
2614
+ settled = true;
2615
+ child.on("error", (err) => logger.warn(`codex proxy error: ${err.message}`));
2616
+ ws.on("error", (err) => logger.warn(`codex daemon socket error: ${err.message}`));
2617
+ resolve({ ws, close });
2618
+ });
2619
+ });
2620
+ }
2621
+
2622
+ // src/codex/run.ts
2623
+ import path4 from "path";
2624
+
2625
+ // src/codex/rpc.ts
2626
+ init_logger();
2627
+ import { EventEmitter } from "events";
2628
+ var RpcError = class extends Error {
2629
+ constructor(code, message) {
2630
+ super(message);
2631
+ this.code = code;
2632
+ }
2633
+ };
2634
+ var RpcClient = class extends EventEmitter {
2635
+ constructor(ws, timeoutMs = 3e4) {
2636
+ super();
2637
+ this.ws = ws;
2638
+ this.timeoutMs = timeoutMs;
2639
+ ws.on("message", (data) => this.onMessage(String(data)));
2640
+ ws.on("close", () => {
2641
+ for (const p of this.pending.values()) {
2642
+ clearTimeout(p.timer);
2643
+ p.reject(new Error("connection closed"));
2644
+ }
2645
+ this.pending.clear();
2646
+ this.emit("close");
2647
+ });
2648
+ }
2649
+ nextId = 1;
2650
+ pending = /* @__PURE__ */ new Map();
2651
+ request(method, params, timeoutMs = this.timeoutMs) {
2652
+ if (this.ws.readyState !== this.ws.OPEN) return Promise.reject(new Error("connection closed"));
2653
+ const id = this.nextId++;
2654
+ return new Promise((resolve, reject) => {
2655
+ const timer = timeoutMs === null ? void 0 : setTimeout(() => {
2656
+ this.pending.delete(id);
2657
+ reject(new Error(`${method} timed out`));
2658
+ }, timeoutMs);
2659
+ this.pending.set(id, { resolve, reject, timer });
2660
+ this.ws.send(JSON.stringify(params === void 0 ? { id, method } : { id, method, params }));
2661
+ });
2662
+ }
2663
+ notify(method, params) {
2664
+ if (this.ws.readyState !== this.ws.OPEN) return;
2665
+ this.ws.send(JSON.stringify(params === void 0 ? { method } : { method, params }));
2666
+ }
2667
+ respond(id, result) {
2668
+ if (this.ws.readyState !== this.ws.OPEN) return;
2669
+ this.ws.send(JSON.stringify({ id, result }));
2670
+ }
2671
+ // ws stops reading the socket for good if a 'message' listener throws.
2672
+ guarded(method, fn) {
2673
+ try {
2674
+ fn();
2675
+ } catch (err) {
2676
+ logger.warn(`Codex ${method} handling failed: ${err.message}`);
2677
+ }
2678
+ }
2679
+ onMessage(text) {
2680
+ let msg;
2681
+ try {
2682
+ msg = JSON.parse(text);
2683
+ } catch {
2684
+ return;
2685
+ }
2686
+ if (!msg || typeof msg !== "object") return;
2687
+ if (msg.method === void 0 && msg.id !== void 0) {
2688
+ const p = this.pending.get(msg.id);
2689
+ if (!p) return;
2690
+ this.pending.delete(msg.id);
2691
+ clearTimeout(p.timer);
2692
+ if (msg.error) p.reject(new RpcError(msg.error.code, msg.error.message));
2693
+ else p.resolve(msg.result);
2694
+ } else if (msg.method !== void 0 && msg.id !== void 0) {
2695
+ this.guarded(msg.method, () => this.emit("request", msg.id, msg.method, msg.params));
2696
+ } else if (msg.method !== void 0) {
2697
+ this.guarded(msg.method, () => this.emit("notification", msg.method, msg.params));
2698
+ }
2699
+ }
2700
+ };
2701
+
2702
+ // src/codex/mapping.ts
2703
+ var SOURCE_LABEL = { dashboard: "Dashboard", telegram: "Telegram", api: "API" };
2704
+ var SOURCE_PREFIX = new RegExp(`^\\[claude-alarm(?: \xB7 (?:${Object.values(SOURCE_LABEL).join("|")}))?\\] ?`);
2705
+ function finalAnswer(messages) {
2706
+ const finals = messages.filter((m) => m.phase === "final_answer").map((m) => m.text);
2707
+ if (finals.length) return finals.join("\n\n");
2708
+ return messages.at(-1)?.text ?? null;
2709
+ }
2710
+
2711
+ // src/codex/run.ts
2712
+ init_constants();
2713
+ var MINUTE = 6e4;
2714
+ var SUMMARY_MAX = 200;
2715
+ var clip = (text) => text.length > SUMMARY_MAX ? `${text.slice(0, SUMMARY_MAX)}\u2026` : text;
2716
+ function relayed(method, params) {
2717
+ switch (method) {
2718
+ case "item/commandExecution/requestApproval": {
2719
+ const command = params.command ?? (params.commandActions ?? []).map((a) => a.command).join("\n");
2720
+ return { kind: "command", summary: clip(String(command || params.reason || "")), decline: { decision: "decline" } };
2721
+ }
2722
+ case "item/fileChange/requestApproval":
2723
+ return { kind: "file change", summary: clip(String(params.reason ?? "")), decline: { decision: "decline" } };
2724
+ case "mcpServer/elicitation/request":
2725
+ if (params._meta?.codex_approval_kind !== "mcp_tool_call") return null;
2726
+ return { kind: "MCP tool", summary: clip(`${params.serverName ?? ""}: ${params.message ?? ""}`), decline: { action: "decline" } };
2727
+ default:
2728
+ return null;
2729
+ }
2730
+ }
2731
+ function unrelayed(method, params) {
2732
+ switch (method) {
2733
+ case "item/permissions/requestApproval":
2734
+ return { kind: "permissions", summary: clip(String(params.reason ?? "")), answer: { permissions: {} } };
2735
+ case "item/tool/requestUserInput":
2736
+ return {
2737
+ kind: "question",
2738
+ summary: clip((params.questions ?? []).map((q) => q.question ?? "").join(" / ")),
2739
+ answer: { answers: {} }
2740
+ };
2741
+ case "mcpServer/elicitation/request":
2742
+ return { kind: "MCP form", summary: clip(String(params.message ?? "")), answer: { action: "decline" } };
2743
+ default:
2744
+ return null;
2745
+ }
2746
+ }
2747
+ async function runCodexTask(opts) {
2748
+ const progress = opts.progress ?? (() => {
2749
+ });
2750
+ const approvalTimeoutMs = opts.approvalTimeoutMs ?? 10 * MINUTE;
2751
+ const started = Date.now();
2752
+ const conn = await connectProxy(opts.command ?? "codex", opts.spawnFn ?? defaultSpawn);
2753
+ const rpc = new RpcClient(conn.ws);
2754
+ const timers = /* @__PURE__ */ new Set();
2755
+ try {
2756
+ const init2 = await rpc.request("initialize", {
2757
+ clientInfo: { name: "claude-alarm-run", version: CHANNEL_SERVER_VERSION }
2758
+ });
2759
+ rpc.notify("initialized");
2760
+ const yolo = { sandbox: "danger-full-access", approvalPolicy: "never" };
2761
+ let threadId;
2762
+ if (opts.threadId) {
2763
+ await rpc.request("thread/resume", opts.yolo ? { threadId: opts.threadId, ...yolo } : { threadId: opts.threadId });
2764
+ threadId = opts.threadId;
2765
+ } else {
2766
+ const res = await rpc.request("thread/start", {
2767
+ cwd: opts.cwd,
2768
+ ...opts.yolo ? yolo : { sandbox: "workspace-write", approvalPolicy: "on-request" }
2769
+ });
2770
+ threadId = res.thread.id;
2771
+ if (opts.name) {
2772
+ await rpc.request("thread/name/set", { threadId, name: opts.name }).catch(() => {
2773
+ });
2774
+ }
2775
+ }
2776
+ progress(`thread ${threadId}`);
2777
+ const messages = /* @__PURE__ */ new Map();
2778
+ const approvals = [];
2779
+ const waiting = /* @__PURE__ */ new Map();
2780
+ let finish;
2781
+ let lose;
2782
+ const completed = new Promise((resolve, reject) => {
2783
+ finish = resolve;
2784
+ lose = reject;
2785
+ });
2786
+ completed.catch(() => {
2787
+ });
2788
+ rpc.on("close", () => lose(new Error("connection to the Codex daemon closed")));
2789
+ rpc.on("notification", (method, params) => {
2790
+ if (params?.threadId !== threadId) return;
2791
+ if (method === "item/completed") {
2792
+ const item = params.item ?? {};
2793
+ if (item.type === "agentMessage") {
2794
+ const list = messages.get(params.turnId) ?? [];
2795
+ list.push({ text: item.text, phase: item.phase });
2796
+ messages.set(params.turnId, list);
2797
+ } else if (item.type === "commandExecution") {
2798
+ progress(`ran ${clip(String(item.command ?? ""))} (exit ${item.exitCode ?? "?"})`);
2799
+ }
2800
+ } else if (method === "serverRequest/resolved") {
2801
+ const record = waiting.get(params.requestId);
2802
+ if (record) {
2803
+ waiting.delete(params.requestId);
2804
+ record.outcome = "answered-elsewhere";
2805
+ progress(`approval answered elsewhere: ${record.kind}`);
2806
+ }
2807
+ } else if (method === "turn/completed") {
2808
+ finish(params.turn);
2809
+ }
2810
+ });
2811
+ rpc.on("request", (id, method, params) => {
2812
+ if (params?.threadId !== threadId) return;
2813
+ const wait = relayed(method, params);
2814
+ if (wait) {
2815
+ const record = { kind: wait.kind, summary: wait.summary, outcome: "unanswered" };
2816
+ approvals.push(record);
2817
+ waiting.set(id, record);
2818
+ progress(`approval pending (${Math.round(approvalTimeoutMs / 1e3)}s): ${wait.kind} ${wait.summary}`);
2819
+ const timer = setTimeout(() => {
2820
+ timers.delete(timer);
2821
+ if (!waiting.delete(id)) return;
2822
+ record.outcome = "timeout-declined";
2823
+ rpc.respond(id, wait.decline);
2824
+ progress(`approval declined after timeout: ${wait.kind}`);
2825
+ }, approvalTimeoutMs);
2826
+ timers.add(timer);
2827
+ return;
2828
+ }
2829
+ const answer = unrelayed(method, params);
2830
+ if (answer) {
2831
+ approvals.push({ kind: answer.kind, summary: answer.summary, outcome: "auto-declined" });
2832
+ rpc.respond(id, answer.answer);
2833
+ progress(`declined at once: ${answer.kind}`);
2834
+ }
2835
+ });
2836
+ const turnStart = { threadId, input: [{ type: "text", text: opts.brief }] };
2837
+ if (opts.outputSchema !== void 0) turnStart.outputSchema = opts.outputSchema;
2838
+ const { turn } = await rpc.request("turn/start", turnStart);
2839
+ const turnId = turn.id;
2840
+ progress(`turn ${turnId} started`);
2841
+ let stopReason = null;
2842
+ let interruptRequested;
2843
+ const stopped = new Promise((resolve) => interruptRequested = resolve);
2844
+ const stop = (reason) => {
2845
+ if (stopReason) return;
2846
+ stopReason = reason;
2847
+ progress(reason === "timedOut" ? "timeout reached, interrupting the turn" : "interrupting the turn");
2848
+ rpc.request("turn/interrupt", { threadId, turnId }).catch(() => {
2849
+ });
2850
+ interruptRequested();
2851
+ };
2852
+ const deadline = setTimeout(() => stop("timedOut"), opts.timeoutMs ?? 60 * MINUTE);
2853
+ timers.add(deadline);
2854
+ const onAbort = () => stop("interrupted");
2855
+ if (opts.signal?.aborted) onAbort();
2856
+ opts.signal?.addEventListener("abort", onAbort, { once: true });
2857
+ const grace = stopped.then(
2858
+ () => new Promise((resolve) => {
2859
+ const t = setTimeout(() => resolve(null), opts.interruptGraceMs ?? 15e3);
2860
+ timers.add(t);
2861
+ })
2862
+ );
2863
+ let ended;
2864
+ let lost = null;
2865
+ try {
2866
+ ended = await Promise.race([completed, grace]);
2867
+ } catch (err) {
2868
+ ended = null;
2869
+ lost = err.message;
2870
+ } finally {
2871
+ opts.signal?.removeEventListener("abort", onAbort);
2872
+ }
2873
+ let status;
2874
+ let error = null;
2875
+ if (stopReason) status = stopReason;
2876
+ else if (lost) {
2877
+ status = "failed";
2878
+ error = lost;
2879
+ } else if (ended?.status === "failed" || ended?.error) {
2880
+ status = "failed";
2881
+ error = ended?.error?.message ?? "the turn failed";
2882
+ } else if (ended?.status === "interrupted") status = "interrupted";
2883
+ else status = "completed";
2884
+ const collected = messages.get(turnId) ?? [];
2885
+ const fromTurn = (ended?.items ?? []).filter((i) => i.type === "agentMessage");
2886
+ const finalText = finalAnswer(collected.length ? collected : fromTurn);
2887
+ let result = null;
2888
+ if (opts.outputSchema !== void 0 && finalText) {
2889
+ try {
2890
+ result = JSON.parse(finalText);
2891
+ } catch {
2892
+ result = null;
2893
+ }
2894
+ }
2895
+ return {
2896
+ threadId,
2897
+ turnId,
2898
+ status,
2899
+ error,
2900
+ result,
2901
+ finalText,
2902
+ approvals,
2903
+ durationMs: Date.now() - started,
2904
+ codex: { userAgent: init2.userAgent ?? null }
2905
+ };
2906
+ } finally {
2907
+ for (const t of timers) clearTimeout(t);
2908
+ conn.close();
2909
+ }
2910
+ }
2911
+ var VALUE_FLAGS = {
2912
+ "--brief": "brief",
2913
+ "--cwd": "cwd",
2914
+ "--thread": "threadId",
2915
+ "--name": "name",
2916
+ "--output-schema": "outputSchema",
2917
+ "--approval-timeout": "approvalTimeoutMs",
2918
+ "--timeout": "timeoutMs"
2919
+ };
2920
+ function parseRunArgs(argv) {
2921
+ const out = {};
2922
+ for (let i = 0; i < argv.length; i++) {
2923
+ const flag = argv[i];
2924
+ if (flag === "--yolo") {
2925
+ out.yolo = true;
2926
+ continue;
2927
+ }
2928
+ const key = VALUE_FLAGS[flag];
2929
+ if (!key) throw new Error(`Unknown option: ${flag}`);
2930
+ const value = argv[++i];
2931
+ if (value === void 0) throw new Error(`${flag} needs a value`);
2932
+ if (key === "approvalTimeoutMs" || key === "timeoutMs") {
2933
+ const minutes = Number(value);
2934
+ if (!Number.isFinite(minutes) || minutes <= 0) throw new Error(`${flag} must be a number of minutes`);
2935
+ out[key] = Math.round(minutes * MINUTE);
2936
+ } else {
2937
+ out[key] = value;
2938
+ }
2939
+ }
2940
+ if (out.brief === void 0) throw new Error("--brief <file|-> is required");
2941
+ return out;
2942
+ }
2943
+ async function runFromCli(argv, io) {
2944
+ let task;
2945
+ try {
2946
+ const args = parseRunArgs(argv);
2947
+ const brief = args.brief === "-" ? await io.readStdin() : await io.readFile(args.brief);
2948
+ if (!brief.trim()) throw new Error("the brief is empty");
2949
+ task = {
2950
+ brief,
2951
+ cwd: path4.resolve(io.cwd, args.cwd ?? "."),
2952
+ threadId: args.threadId,
2953
+ name: args.name,
2954
+ yolo: args.yolo,
2955
+ outputSchema: args.outputSchema === void 0 ? void 0 : JSON.parse(await io.readFile(args.outputSchema)),
2956
+ approvalTimeoutMs: args.approvalTimeoutMs,
2957
+ timeoutMs: args.timeoutMs,
2958
+ signal: io.signal,
2959
+ spawnFn: io.spawnFn,
2960
+ progress: (line) => io.err(`[codex run] ${line}
2961
+ `)
2962
+ };
2963
+ } catch (err) {
2964
+ io.err(`codex run: ${err.message}
2965
+ `);
2966
+ return 2;
2967
+ }
2968
+ try {
2969
+ const result = await runCodexTask(task);
2970
+ io.out(`${JSON.stringify(result, null, 2)}
2971
+ `);
2972
+ return result.status === "completed" ? 0 : 1;
2973
+ } catch (err) {
2974
+ io.err(`codex run: could not start the task: ${err.message}
2975
+ `);
2976
+ return 2;
2977
+ }
2978
+ }
2381
2979
 
2382
2980
  // src/cli.ts
2383
2981
  init_constants();
2384
2982
  init_logger();
2385
2983
  init_crash_guard();
2386
- var __dirname2 = path9.dirname(fileURLToPath2(import.meta.url));
2984
+
2985
+ // src/hub/readiness.ts
2986
+ var HUB_START_TIMEOUT_MS = 5e3;
2987
+ async function waitForHub(opts) {
2988
+ const timeoutMs = opts.timeoutMs ?? HUB_START_TIMEOUT_MS;
2989
+ const intervalMs = opts.intervalMs ?? 200;
2990
+ const deadline = Date.now() + timeoutMs;
2991
+ const headers = opts.token ? { Authorization: `Bearer ${opts.token}` } : {};
2992
+ for (; ; ) {
2993
+ if (!opts.isAlive()) return "exited";
2994
+ const remaining = deadline - Date.now();
2995
+ if (remaining <= 0) return "timeout";
2996
+ try {
2997
+ const res = await fetch(opts.url, { headers, signal: AbortSignal.timeout(Math.min(remaining, 1e3)) });
2998
+ if (res.ok) {
2999
+ const body = await res.json();
3000
+ if (body.pid === opts.pid) return "ready";
3001
+ }
3002
+ } catch {
3003
+ }
3004
+ await new Promise((resolve) => setTimeout(resolve, Math.min(intervalMs, Math.max(0, deadline - Date.now()))));
3005
+ }
3006
+ }
3007
+
3008
+ // src/cli.ts
3009
+ init_hub_url();
3010
+
3011
+ // src/codex/hub-target.ts
3012
+ init_hub_url();
3013
+ function resolveAdapterHub(config2, env = process.env) {
3014
+ const envHost = env.CLAUDE_ALARM_HUB_HOST;
3015
+ const envPort = env.CLAUDE_ALARM_HUB_PORT;
3016
+ const envToken = env.CLAUDE_ALARM_HUB_TOKEN;
3017
+ return {
3018
+ host: hubUrlHost(envHost ?? config2.hub.host),
3019
+ port: envPort ? parseInt(envPort, 10) : config2.hub.port,
3020
+ token: envToken ?? config2.hub.token,
3021
+ fromEnv: { host: envHost !== void 0, port: !!envPort, token: envToken !== void 0 }
3022
+ };
3023
+ }
3024
+
3025
+ // src/codex/start-check.ts
3026
+ var ADAPTER_REPORT_TIMEOUT_MS = 1e4;
3027
+ var HUB_CHECK_TIMEOUT_MS = 3e3;
3028
+ var RESTART_HINT = "After fixing this, restart the adapter: claude-alarm codex stop, then claude-alarm codex start";
3029
+ var CONFIG_PATH = "~/.claude-alarm/config.json";
3030
+ var NOT_CHECKED = { text: "Codex daemon: not checked (the adapter was already running)", warning: false };
3031
+ function toOutcome(msg) {
3032
+ if (msg.connected === true) return typeof msg.userAgent === "string" ? { connected: true, userAgent: msg.userAgent } : { connected: true };
3033
+ return { connected: false, error: String(msg.error ?? ""), notFound: msg.notFound === true };
3034
+ }
3035
+ function waitForAdapterReport(child, timeoutMs = ADAPTER_REPORT_TIMEOUT_MS) {
3036
+ return new Promise((resolve) => {
3037
+ const finish = (result) => {
3038
+ clearTimeout(timer);
3039
+ child.off("message", onMessage);
3040
+ child.off("exit", onExit);
3041
+ child.off("error", onError);
3042
+ resolve(result);
3043
+ };
3044
+ const onMessage = (msg) => {
3045
+ if (msg && typeof msg === "object" && msg.type === "codex-first-connect") {
3046
+ finish({ kind: "report", outcome: toOutcome(msg) });
3047
+ }
3048
+ };
3049
+ const onExit = (code, signal) => finish({ kind: "exited", code, signal });
3050
+ const onError = (error) => finish({ kind: "exited", code: null, signal: null, error });
3051
+ const timer = setTimeout(() => finish({ kind: "timeout" }), timeoutMs);
3052
+ child.on("message", onMessage);
3053
+ child.on("exit", onExit);
3054
+ child.on("error", onError);
3055
+ });
3056
+ }
3057
+ function hubUrl(hub) {
3058
+ return `http://${hub.host}:${hub.port}`;
3059
+ }
3060
+ async function checkHub(hub, timeoutMs = HUB_CHECK_TIMEOUT_MS) {
3061
+ let res;
3062
+ try {
3063
+ res = await fetch(`${hubUrl(hub)}/api/status`, {
3064
+ headers: hub.token ? { Authorization: `Bearer ${hub.token}` } : {},
3065
+ redirect: "manual",
3066
+ signal: AbortSignal.timeout(timeoutMs)
3067
+ });
3068
+ } catch (err) {
3069
+ const e = err;
3070
+ return { kind: "unreachable", reason: typeof e.cause?.code === "string" ? e.cause.code : e.name };
3071
+ }
3072
+ if (res.status === 200) {
3073
+ try {
3074
+ const body = await res.json();
3075
+ if (body?.running === true) return { kind: "ok" };
3076
+ } catch {
3077
+ }
3078
+ return { kind: "not-hub", status: 200 };
3079
+ }
3080
+ await res.body?.cancel().catch(() => {
3081
+ });
3082
+ return res.status === 401 ? { kind: "unauthorized" } : { kind: "not-hub", status: res.status };
3083
+ }
3084
+ function daemonLine(report, command, logFile) {
3085
+ if (report.kind === "timeout") {
3086
+ return { text: `Codex daemon: no answer within ${ADAPTER_REPORT_TIMEOUT_MS / 1e3}s. The adapter keeps trying. See ${logFile}`, warning: true };
3087
+ }
3088
+ const o = report.outcome;
3089
+ if (o.connected) return { text: `Codex daemon: connected (${o.userAgent ?? "unknown version"})`, warning: false };
3090
+ if (o.notFound) {
3091
+ 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 };
3092
+ }
3093
+ return { text: `Codex daemon: not connected (${o.error}). Is Codex running? The adapter keeps retrying.`, warning: true };
3094
+ }
3095
+ function addressSource(hub) {
3096
+ const host = hub.fromEnv.host ? "CLAUDE_ALARM_HUB_HOST" : "hub.host";
3097
+ const port = hub.fromEnv.port ? "CLAUDE_ALARM_HUB_PORT" : "hub.port";
3098
+ const fromConfig = !hub.fromEnv.host || !hub.fromEnv.port;
3099
+ return `${host} and ${port}${fromConfig ? ` in ${CONFIG_PATH}` : ""}`;
3100
+ }
3101
+ function tokenSource(hub) {
3102
+ return hub.fromEnv.token ? "CLAUDE_ALARM_HUB_TOKEN" : `hub.token in ${CONFIG_PATH}`;
3103
+ }
3104
+ function hubLine(check, hub) {
3105
+ const url = hubUrl(hub);
3106
+ switch (check.kind) {
3107
+ case "ok":
3108
+ return { text: `Hub: reachable at ${url}`, warning: false };
3109
+ case "unauthorized":
3110
+ return { text: `Hub: ${url} rejected the token (401). Check ${tokenSource(hub)}.`, warning: true };
3111
+ case "not-hub":
3112
+ return { text: `Hub: ${url} answered ${check.status} but is not a claude-alarm hub. Check ${addressSource(hub)}.`, warning: true };
3113
+ case "unreachable":
3114
+ return { text: `Hub: not reachable at ${url} (${check.reason}). Check ${addressSource(hub)} and that the hub is running.`, warning: true };
3115
+ }
3116
+ }
3117
+ function printChecks(d, daemon, hub) {
3118
+ d.out(` ${daemon.text}`);
3119
+ d.out(` ${hub.text}`);
3120
+ if (daemon.warning || hub.warning) d.out(` ${RESTART_HINT}`);
3121
+ }
3122
+ function printAlreadyRunning(d, pid, hub) {
3123
+ d.out(`Codex adapter is already running (PID: ${pid})`);
3124
+ printChecks(d, NOT_CHECKED, hubLine(hub, d.hub));
3125
+ }
3126
+ async function startAdapter(d) {
3127
+ const existing = d.readPid();
3128
+ if (existing !== void 0 && d.isRunning(existing)) {
3129
+ printAlreadyRunning(d, existing, await checkHub(d.hub, d.hubTimeoutMs));
3130
+ return 0;
3131
+ }
3132
+ const child = d.spawnAdapter();
3133
+ const seen = {};
3134
+ child.on("exit", (code, signal) => {
3135
+ seen.exit ??= { code, signal };
3136
+ });
3137
+ child.on("error", (error) => {
3138
+ seen.exit ??= { code: null, signal: null, error };
3139
+ });
3140
+ const [report, hub] = await Promise.all([
3141
+ waitForAdapterReport(child, d.reportTimeoutMs),
3142
+ checkHub(d.hub, d.hubTimeoutMs)
3143
+ ]);
3144
+ if (child.connected) child.disconnect();
3145
+ child.unref();
3146
+ if (report.kind !== "exited" && !seen.exit && child.pid !== void 0) {
3147
+ d.out(`Codex adapter started (PID: ${child.pid}). Logs: ${d.logFile}`);
3148
+ printChecks(d, daemonLine(report, d.command, d.logFile), hubLine(hub, d.hub));
3149
+ return 0;
3150
+ }
3151
+ const exit = seen.exit ?? (report.kind === "exited" ? report : void 0);
3152
+ if (!exit || exit.error) {
3153
+ d.err(`Codex adapter failed to start: ${exit?.error?.message ?? "no process id"}`);
3154
+ return 1;
3155
+ }
3156
+ const owner = d.readPid();
3157
+ if (owner !== void 0 && owner === child.pid) d.removePidFile();
3158
+ else if (owner !== void 0 && d.isRunning(owner)) {
3159
+ printAlreadyRunning(d, owner, hub);
3160
+ return 0;
3161
+ }
3162
+ const how = exit.code !== null ? `code ${exit.code}` : `signal ${exit.signal}`;
3163
+ d.err(`Codex adapter exited during startup (${how}). See ${d.logFile}`);
3164
+ return 1;
3165
+ }
3166
+
3167
+ // src/cli.ts
3168
+ var __dirname2 = path10.dirname(fileURLToPath2(import.meta.url));
2387
3169
  function authHeaders(token) {
2388
3170
  return token ? { Authorization: `Bearer ${token}` } : {};
2389
3171
  }
@@ -2407,6 +3189,10 @@ Usage:
2407
3189
  claude-alarm codex start Run the Codex adapter on its own (e.g. Codex on another PC)
2408
3190
  claude-alarm codex stop Stop a running Codex adapter
2409
3191
  claude-alarm codex status Show Codex adapter status
3192
+ claude-alarm codex run --brief <file|-> [--cwd <dir>] [--thread <id>] [--name <title>]
3193
+ [--output-schema <file>] [--approval-timeout <min>] [--timeout <min>] [--yolo]
3194
+ Hand a task to Codex through the shared daemon and print the result as JSON
3195
+ (--yolo: no sandbox and no approvals, like codex --yolo)
2410
3196
  claude-alarm help Show this help
2411
3197
 
2412
3198
  Quick start:
@@ -2415,7 +3201,7 @@ Quick start:
2415
3201
  }
2416
3202
  async function checkForUpdates() {
2417
3203
  try {
2418
- const pkgPath = path9.join(__dirname2, "..", "package.json");
3204
+ const pkgPath = path10.join(__dirname2, "..", "package.json");
2419
3205
  const pkg = JSON.parse(fs8.readFileSync(pkgPath, "utf-8"));
2420
3206
  const currentVersion = pkg.version;
2421
3207
  const res = await fetch("https://registry.npmjs.org/@delt/claude-alarm/latest", {
@@ -2437,7 +3223,7 @@ async function hubStart(daemon) {
2437
3223
  const config2 = loadConfig();
2438
3224
  const host = config2.hub.host ?? DEFAULT_HUB_HOST;
2439
3225
  const port = config2.hub.port ?? DEFAULT_HUB_PORT;
2440
- const displayHost = host === "0.0.0.0" ? "127.0.0.1" : host;
3226
+ const displayHost = hubUrlHost(host);
2441
3227
  checkForUpdates();
2442
3228
  if (fs8.existsSync(PID_FILE)) {
2443
3229
  const pid = parseInt(fs8.readFileSync(PID_FILE, "utf-8").trim(), 10);
@@ -2450,23 +3236,39 @@ async function hubStart(daemon) {
2450
3236
  if (daemon) {
2451
3237
  ensureConfigDir();
2452
3238
  const logFd = fs8.openSync(LOG_FILE, "a");
2453
- const hubScript = path9.join(__dirname2, "hub", "server.js");
3239
+ const hubScript = path10.join(__dirname2, "hub", "server.js");
2454
3240
  const child = spawn3(process.execPath, [hubScript], {
2455
3241
  detached: true,
2456
3242
  stdio: ["ignore", logFd, logFd],
2457
3243
  env: { ...process.env }
2458
3244
  });
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 {
3245
+ const pid = child.pid;
3246
+ if (!pid) {
2467
3247
  console.error("Failed to start hub daemon");
2468
3248
  process.exit(1);
2469
3249
  }
3250
+ fs8.writeFileSync(PID_FILE, String(pid), "utf-8");
3251
+ let exited = false;
3252
+ child.once("exit", () => {
3253
+ exited = true;
3254
+ });
3255
+ child.unref();
3256
+ const startup = await waitForHub({
3257
+ url: `http://${displayHost}:${port}/api/status`,
3258
+ token: config2.hub.token,
3259
+ pid,
3260
+ isAlive: () => !exited
3261
+ });
3262
+ if (startup !== "ready") {
3263
+ if (!exited) child.kill();
3264
+ if (fs8.existsSync(PID_FILE)) fs8.unlinkSync(PID_FILE);
3265
+ 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}`);
3266
+ process.exit(1);
3267
+ }
3268
+ console.log(`Hub started as daemon (PID: ${pid})`);
3269
+ console.log(`Dashboard: ${loginLink(displayHost, port, config2.hub.token)}`);
3270
+ console.log(`Token: ${config2.hub.token}`);
3271
+ console.log(`Logs: ${LOG_FILE}`);
2470
3272
  } else {
2471
3273
  console.log(`Starting hub on http://${displayHost}:${port} (press Ctrl+C to stop)`);
2472
3274
  console.log(`Dashboard: ${loginLink(displayHost, port, config2.hub.token)}`);
@@ -2507,7 +3309,7 @@ async function hubStatus() {
2507
3309
  const config2 = loadConfig();
2508
3310
  const host = config2.hub.host ?? DEFAULT_HUB_HOST;
2509
3311
  const port = config2.hub.port ?? DEFAULT_HUB_PORT;
2510
- const displayHost = host === "0.0.0.0" ? "127.0.0.1" : host;
3312
+ const displayHost = hubUrlHost(host);
2511
3313
  let pidInfo = "not running";
2512
3314
  if (fs8.existsSync(PID_FILE)) {
2513
3315
  const pid = parseInt(fs8.readFileSync(PID_FILE, "utf-8").trim(), 10);
@@ -2518,7 +3320,7 @@ async function hubStatus() {
2518
3320
  }
2519
3321
  }
2520
3322
  try {
2521
- const res = await fetch(`http://${host}:${port}/api/status`, { headers: authHeaders(config2.hub.token) });
3323
+ const res = await fetch(`http://${displayHost}:${port}/api/status`, { headers: authHeaders(config2.hub.token) });
2522
3324
  if (res.ok) {
2523
3325
  const data = await res.json();
2524
3326
  console.log(`Hub: running (PID: ${data.pid})`);
@@ -2553,7 +3355,7 @@ async function test() {
2553
3355
  if (config2.hub.token) {
2554
3356
  headers["Authorization"] = `Bearer ${config2.hub.token}`;
2555
3357
  }
2556
- const res = await fetch(`http://${host}:${port}/api/notify`, {
3358
+ const res = await fetch(`http://${hubUrlHost(host)}:${port}/api/notify`, {
2557
3359
  method: "POST",
2558
3360
  headers,
2559
3361
  body: JSON.stringify({
@@ -2582,7 +3384,7 @@ function ask(question) {
2582
3384
  }
2583
3385
  async function init() {
2584
3386
  const dir = process.cwd();
2585
- const projectName = path9.basename(dir);
3387
+ const projectName = path10.basename(dir);
2586
3388
  console.log(`
2587
3389
  claude-alarm init for "${projectName}"
2588
3390
  `);
@@ -2602,7 +3404,7 @@ claude-alarm init for "${projectName}"
2602
3404
  if (port) env.CLAUDE_ALARM_HUB_PORT = port;
2603
3405
  if (token) env.CLAUDE_ALARM_HUB_TOKEN = token;
2604
3406
  }
2605
- const mcpPath = path9.join(dir, ".mcp.json");
3407
+ const mcpPath = path10.join(dir, ".mcp.json");
2606
3408
  let mcpConfig = {};
2607
3409
  if (fs8.existsSync(mcpPath)) {
2608
3410
  try {
@@ -2624,10 +3426,10 @@ claude-alarm init for "${projectName}"
2624
3426
  const config2 = loadConfig();
2625
3427
  const host = config2.hub.host ?? DEFAULT_HUB_HOST;
2626
3428
  const port = config2.hub.port ?? DEFAULT_HUB_PORT;
2627
- const displayHost = host === "0.0.0.0" ? "127.0.0.1" : host;
3429
+ const displayHost = hubUrlHost(host);
2628
3430
  let hubRunning = false;
2629
3431
  try {
2630
- const res = await fetch(`http://${host}:${port}/api/status`, { headers: authHeaders(config2.hub.token) });
3432
+ const res = await fetch(`http://${displayHost}:${port}/api/status`, { headers: authHeaders(config2.hub.token) });
2631
3433
  hubRunning = res.ok;
2632
3434
  } catch {
2633
3435
  }
@@ -2638,7 +3440,7 @@ claude-alarm init for "${projectName}"
2638
3440
  console.log(` claude-alarm hub start`);
2639
3441
  }
2640
3442
  console.log(` Dashboard: http://${displayHost}:${port}`);
2641
- if (shouldOfferCodex(config2, findOnPath("codex") !== void 0)) {
3443
+ if (shouldOfferCodex(config2, findCodex("codex") !== void 0)) {
2642
3444
  const answer = await ask("\nCodex is installed. Show Codex conversations on the dashboard too? (y/N): ");
2643
3445
  if (answer.toLowerCase() === "y") codexEnable(true);
2644
3446
  else setCodexEnabled(false);
@@ -2676,21 +3478,34 @@ function codexEnable(enabled) {
2676
3478
  setCodexEnabled(enabled);
2677
3479
  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
3480
  }
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
- }
3481
+ async function codexStart() {
3482
+ const config2 = loadConfig();
2685
3483
  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 }
3484
+ const code = await startAdapter({
3485
+ hub: resolveAdapterHub(config2),
3486
+ command: config2.codex?.command ?? "codex",
3487
+ logFile: CODEX_LOG_FILE,
3488
+ readPid: readCodexPid,
3489
+ isRunning: isProcessRunning,
3490
+ removePidFile: () => {
3491
+ try {
3492
+ fs8.unlinkSync(CODEX_PID_FILE);
3493
+ } catch {
3494
+ }
3495
+ },
3496
+ spawnAdapter: () => {
3497
+ const logFd = fs8.openSync(CODEX_LOG_FILE, "a");
3498
+ return spawn3(process.execPath, [path10.join(__dirname2, "codex", "main.js")], {
3499
+ detached: true,
3500
+ stdio: ["ignore", logFd, logFd, "ipc"],
3501
+ windowsHide: true,
3502
+ env: { ...process.env }
3503
+ });
3504
+ },
3505
+ out: (line) => console.log(line),
3506
+ err: (line) => console.error(line)
2691
3507
  });
2692
- child.unref();
2693
- console.log(`Codex adapter started (PID: ${child.pid}). Logs: ${CODEX_LOG_FILE}`);
3508
+ if (code !== 0) process.exit(code);
2694
3509
  }
2695
3510
  function codexStop() {
2696
3511
  const pid = readCodexPid();
@@ -2715,6 +3530,29 @@ function codexStatus() {
2715
3530
  console.log(`Codex adapter: ${state}`);
2716
3531
  console.log(`Start with hub: ${loadConfig().codex?.enabled ? "enabled" : "disabled"}`);
2717
3532
  }
3533
+ async function codexRun(argv) {
3534
+ const abort = new AbortController();
3535
+ const stop = () => abort.abort();
3536
+ process.on("SIGINT", stop);
3537
+ process.on("SIGTERM", stop);
3538
+ try {
3539
+ return await runFromCli(argv, {
3540
+ readFile: (file) => fs8.promises.readFile(file, "utf-8"),
3541
+ readStdin: async () => {
3542
+ let text = "";
3543
+ for await (const chunk of process.stdin) text += chunk;
3544
+ return text;
3545
+ },
3546
+ out: (text) => process.stdout.write(text),
3547
+ err: (text) => process.stderr.write(text),
3548
+ cwd: process.cwd(),
3549
+ signal: abort.signal
3550
+ });
3551
+ } finally {
3552
+ process.off("SIGINT", stop);
3553
+ process.off("SIGTERM", stop);
3554
+ }
3555
+ }
2718
3556
  async function main2() {
2719
3557
  const args = process.argv.slice(2);
2720
3558
  const cmd = args[0];
@@ -2749,9 +3587,10 @@ async function main2() {
2749
3587
  if (cmd === "codex") {
2750
3588
  if (sub === "enable") codexEnable(true);
2751
3589
  else if (sub === "disable") codexEnable(false);
2752
- else if (sub === "start") codexStart();
3590
+ else if (sub === "start") await codexStart();
2753
3591
  else if (sub === "stop") codexStop();
2754
3592
  else if (sub === "status") codexStatus();
3593
+ else if (sub === "run") process.exitCode = await codexRun(args.slice(2));
2755
3594
  else {
2756
3595
  console.error(`Unknown codex command: ${sub}`);
2757
3596
  printUsage();