@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.
@@ -143,6 +143,19 @@
143
143
  color: var(--text-dim);
144
144
  margin-bottom: 8px;
145
145
  }
146
+ [hidden] { display: none !important; }
147
+ .codex-section { margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--border); display: grid; gap: 8px; }
148
+ .codex-section .cmd-popup-title { margin-bottom: 0; color: var(--text); font-weight: 600; }
149
+ #codexAdapter, #codexFolder { box-sizing: border-box; width: 100%; min-width: 0; min-height: 36px; padding: 8px; border: 1px solid var(--border); border-radius: 6px; background: var(--surface); color: var(--text); font: inherit; font-size: 12px; }
150
+ #codexFolder::placeholder { color: var(--text-dim); }
151
+ #codexAdapter:focus, #codexFolder:focus { outline: 1px solid var(--accent); border-color: var(--accent); }
152
+ .codex-warning { color: var(--text-dim); font-size: 11px; line-height: 1.5; }
153
+ #codexCreate { min-height: 36px; padding: 8px 12px; border: 1px solid var(--accent); border-radius: 6px; background: var(--surface); color: var(--text); font: inherit; font-size: 12px; font-weight: 600; cursor: pointer; }
154
+ #codexCreate:disabled { opacity: 0.6; cursor: wait; }
155
+ .codex-error { color: var(--red); font-size: 12px; line-height: 1.5; overflow-wrap: anywhere; }
156
+ .session-close { flex-shrink: 0; align-self: flex-end; min-width: 32px; min-height: 32px; padding: 4px 6px; border: 1px solid var(--border); border-radius: 6px; background: var(--surface); color: var(--text-dim); font-size: 16px; cursor: pointer; }
157
+ .session-close:hover, .session-close:focus-visible { border-color: var(--red); color: var(--red); }
158
+ .session-close.armed { border-color: var(--red); color: var(--red); font-size: 11px; font-weight: 600; }
146
159
  .session-card {
147
160
  background: var(--surface);
148
161
  border: 1px solid var(--border);
@@ -863,6 +876,9 @@
863
876
 
864
877
  /* Mobile responsive */
865
878
  @media (max-width: 768px) {
879
+ .codex-section { gap: 10px; }
880
+ #codexAdapter, #codexFolder, #codexCreate { min-height: 40px; font-size: 13px; }
881
+ .session-close { min-width: 36px; min-height: 36px; }
866
882
  .container {
867
883
  grid-template-columns: 1fr;
868
884
  height: calc(100vh - 97px);
@@ -1015,6 +1031,15 @@
1015
1031
  <span class="cmd-text">claude --dangerously-load-development-channels server:claude-alarm --dangerously-skip-permissions</span>
1016
1032
  <span class="cmd-icon">&#128203;</span>
1017
1033
  </div>
1034
+ <div class="codex-section" id="codexSection" hidden>
1035
+ <div class="cmd-popup-title">New Codex conversation:</div>
1036
+ <select id="codexAdapter" hidden></select>
1037
+ <input id="codexFolder" list="codexFolders" placeholder="Folder on that PC, e.g. C:\work\project" autocomplete="off" spellcheck="false">
1038
+ <datalist id="codexFolders"></datalist>
1039
+ <div class="codex-warning">Runs with full access: no approval prompts.</div>
1040
+ <button id="codexCreate" type="button">Create</button>
1041
+ <div class="codex-error" id="codexError" hidden></div>
1042
+ </div>
1018
1043
  </div>
1019
1044
  <div id="sessionsList"></div>
1020
1045
  </div>
@@ -1066,6 +1091,9 @@
1066
1091
  waitingReply: {},
1067
1092
  permissionRequests: {}, // sessionId -> [{ requestId, toolName, description, inputPreview, timestamp, resolved, behavior }]
1068
1093
  sessionMeta: { names: {}, order: [] }, // custom names + order, keyed by volatile sessionId
1094
+ codexAdapters: [],
1095
+ pendingSelect: null,
1096
+ closeArmed: null,
1069
1097
  };
1070
1098
 
1071
1099
  const $ = (sel) => document.querySelector(sel);
@@ -1390,11 +1418,13 @@
1390
1418
  msg.sessions.forEach(s => { state.sessions[s.id] = s; });
1391
1419
  pruneSessionData();
1392
1420
  renderSessions();
1421
+ if (state.pendingSelect && state.sessions[state.pendingSelect]) selectSession(state.pendingSelect);
1393
1422
  break;
1394
1423
  case 'session_connected':
1395
1424
  state.sessions[msg.session.id] = msg.session;
1396
1425
  if (!state.messages[msg.session.id]) state.messages[msg.session.id] = [];
1397
1426
  renderSessions();
1427
+ if (state.pendingSelect === msg.session.id) selectSession(msg.session.id);
1398
1428
  break;
1399
1429
  case 'session_disconnected':
1400
1430
  delete state.sessions[msg.sessionId];
@@ -1463,6 +1493,15 @@
1463
1493
  renderPermissionBar();
1464
1494
  renderNotifications();
1465
1495
  break;
1496
+ case 'codex_adapters': {
1497
+ state.codexAdapters = msg.adapters || [];
1498
+ if ($('#cmdPopup').classList.contains('active')) {
1499
+ const before = $('#codexAdapter').value;
1500
+ renderCodexForm();
1501
+ if ($('#codexAdapter').value !== before) loadCodexFolders();
1502
+ }
1503
+ break;
1504
+ }
1466
1505
  case 'permission_resolved': {
1467
1506
  const req = (state.permissionRequests[msg.sessionId] || []).find(r => r.requestId === msg.requestId);
1468
1507
  if (req && !req.resolved) {
@@ -1491,6 +1530,10 @@
1491
1530
  const unread = state.unread[id] || 0;
1492
1531
  const dispName = sessionDisplayName(s);
1493
1532
  const connTime = new Date(s.connectedAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
1533
+ const armed = state.closeArmed && state.closeArmed.id === id && closeStep(state.closeArmed.at, Date.now()) !== 'arm';
1534
+ const closeBtn = s.closable
1535
+ ? `<button class="session-close${armed ? ' armed' : ''}" type="button" title="Close — stays in Codex history">${armed ? 'Close?' : '&times;'}</button>`
1536
+ : '';
1494
1537
  return `<div class="session-card${active}" data-id="${id}">
1495
1538
  <span class="drag-handle" title="Drag to reorder">&#10303;</span>
1496
1539
  <div class="session-body">
@@ -1498,6 +1541,7 @@
1498
1541
  ${cwdDisplay ? `<div class="session-cwd" title="${esc(s.cwd)}">${esc(cwdDisplay)} &middot; ${connTime}</div>` : `<div class="session-cwd">${connTime}</div>`}
1499
1542
  <span class="session-status ${s.status}">${s.status.replace('_', ' ')}</span>
1500
1543
  </div>
1544
+ ${closeBtn}
1501
1545
  </div>`;
1502
1546
  }).join('');
1503
1547
  bindSessionCards(el);
@@ -1508,10 +1552,13 @@
1508
1552
  el.querySelectorAll('.session-card').forEach(card => {
1509
1553
  const id = card.dataset.id;
1510
1554
  card.addEventListener('click', (e) => {
1555
+ if (e.target.closest('.session-close')) return;
1511
1556
  if (e.target.closest('.drag-handle')) return;
1512
1557
  if (e.target.classList.contains('name-edit-input')) return;
1513
1558
  selectSession(id);
1514
1559
  });
1560
+ const closeBtn = card.querySelector('.session-close');
1561
+ if (closeBtn) closeBtn.addEventListener('click', (e) => { e.stopPropagation(); onCloseClick(id); });
1515
1562
  const nameEl = card.querySelector('.session-name');
1516
1563
  nameEl.addEventListener('dblclick', (e) => {
1517
1564
  e.stopPropagation();
@@ -1621,6 +1668,7 @@
1621
1668
  }
1622
1669
 
1623
1670
  function selectSession(id) {
1671
+ state.pendingSelect = null;
1624
1672
  state.selectedSession = id;
1625
1673
  state.unread[id] = 0;
1626
1674
  if (!state.messages[id]) state.messages[id] = [];
@@ -1641,7 +1689,7 @@
1641
1689
 
1642
1690
  function updateImageUI() {
1643
1691
  const s = state.selectedSession ? state.sessions[state.selectedSession] : null;
1644
- const canImage = s && s.isLocal && s.agentKind !== 'codex';
1692
+ const canImage = s && s.isLocal;
1645
1693
  $('#attachBtn').disabled = !canImage;
1646
1694
  }
1647
1695
 
@@ -2185,10 +2233,32 @@
2185
2233
  msgList.scrollTo({ top: msgList.scrollHeight, behavior: 'smooth' });
2186
2234
  });
2187
2235
 
2236
+ // --- Codex helpers ---
2237
+ function usableAdapters(adapters) {
2238
+ return adapters.filter(a => a.ready).sort((a, b) => (b.isLocal ? 1 : 0) - (a.isLocal ? 1 : 0));
2239
+ }
2240
+
2241
+ function adapterLabels(adapters) {
2242
+ const counts = {};
2243
+ adapters.forEach(a => { counts[a.host] = (counts[a.host] || 0) + 1; });
2244
+ return adapters.map(a => counts[a.host] > 1 ? `${a.host} (${a.id.slice(0, 4)})` : a.host);
2245
+ }
2246
+
2247
+ // A second tap within 0.4 s is a phone double-tap, not a confirmation.
2248
+ function closeStep(armedAt, now) {
2249
+ if (armedAt === null || now - armedAt >= 3000) return 'arm';
2250
+ return now - armedAt < 400 ? 'wait' : 'close';
2251
+ }
2252
+
2188
2253
  // --- Add session popup ---
2189
2254
  $('#addSessionBtn').addEventListener('click', (e) => {
2190
2255
  e.stopPropagation();
2256
+ const opening = !$('#cmdPopup').classList.contains('active');
2191
2257
  $('#cmdPopup').classList.toggle('active');
2258
+ if (opening) {
2259
+ renderCodexForm();
2260
+ loadCodexFolders();
2261
+ }
2192
2262
  });
2193
2263
  document.addEventListener('click', (e) => {
2194
2264
  if (!e.target.closest('#cmdPopup') && !e.target.closest('#addSessionBtn')) {
@@ -2209,6 +2279,97 @@
2209
2279
  });
2210
2280
  });
2211
2281
 
2282
+ function renderCodexForm() {
2283
+ const usable = usableAdapters(state.codexAdapters);
2284
+ $('#codexSection').hidden = !usable.length;
2285
+ if (!usable.length) return;
2286
+ const select = $('#codexAdapter');
2287
+ const current = select.value;
2288
+ const labels = adapterLabels(usable);
2289
+ select.replaceChildren(...usable.map((a, i) => new Option(labels[i], a.id)));
2290
+ if (usable.some(a => a.id === current)) select.value = current;
2291
+ select.hidden = usable.length < 2;
2292
+ }
2293
+
2294
+ async function loadCodexFolders() {
2295
+ const list = $('#codexFolders');
2296
+ list.innerHTML = '';
2297
+ const adapterId = $('#codexAdapter').value;
2298
+ if (!adapterId) return;
2299
+ try {
2300
+ const res = await fetch(`/api/codex/folders?adapterId=${encodeURIComponent(adapterId)}`);
2301
+ if (!res.ok || $('#codexAdapter').value !== adapterId) return;
2302
+ const { folders } = await res.json();
2303
+ list.replaceChildren(...folders.map(f => new Option('', f)));
2304
+ } catch {}
2305
+ }
2306
+
2307
+ async function createCodexConversation() {
2308
+ const btn = $('#codexCreate');
2309
+ const folder = $('#codexFolder').value.trim();
2310
+ const adapterId = $('#codexAdapter').value;
2311
+ if (!folder || !adapterId || btn.disabled) return;
2312
+ const error = $('#codexError');
2313
+ error.hidden = true;
2314
+ btn.disabled = true;
2315
+ btn.textContent = 'Creating…';
2316
+ try {
2317
+ const res = await fetch('/api/codex/threads', {
2318
+ method: 'POST',
2319
+ headers: { 'Content-Type': 'application/json' },
2320
+ body: JSON.stringify({ adapterId, cwd: folder }),
2321
+ });
2322
+ const body = await res.json().catch(() => ({}));
2323
+ if (!res.ok) throw new Error(body.error || `Request failed (${res.status})`);
2324
+ $('#cmdPopup').classList.remove('active');
2325
+ $('#codexFolder').value = '';
2326
+ // The session can register before or after this answer arrives.
2327
+ if (state.sessions[body.sessionId]) selectSession(body.sessionId);
2328
+ else state.pendingSelect = body.sessionId;
2329
+ } catch (err) {
2330
+ error.textContent = err.message;
2331
+ error.hidden = false;
2332
+ } finally {
2333
+ btn.disabled = false;
2334
+ btn.textContent = 'Create';
2335
+ }
2336
+ }
2337
+
2338
+ async function onCloseClick(id) {
2339
+ const armedAt = state.closeArmed && state.closeArmed.id === id ? state.closeArmed.at : null;
2340
+ const step = closeStep(armedAt, Date.now());
2341
+ if (step === 'wait') return;
2342
+ if (step === 'arm') {
2343
+ const at = Date.now();
2344
+ state.closeArmed = { id, at };
2345
+ renderSessions();
2346
+ setTimeout(() => {
2347
+ if (state.closeArmed && state.closeArmed.at === at) {
2348
+ state.closeArmed = null;
2349
+ renderSessions();
2350
+ }
2351
+ }, 3000);
2352
+ return;
2353
+ }
2354
+ state.closeArmed = null;
2355
+ renderSessions();
2356
+ try {
2357
+ await fetch('/api/codex/threads/close', {
2358
+ method: 'POST',
2359
+ headers: { 'Content-Type': 'application/json' },
2360
+ body: JSON.stringify({ sessionId: id }),
2361
+ });
2362
+ } catch {}
2363
+ }
2364
+
2365
+ $('#codexAdapter').addEventListener('change', loadCodexFolders);
2366
+ $('#codexCreate').addEventListener('click', createCodexConversation);
2367
+ $('#codexFolder').addEventListener('keydown', (e) => {
2368
+ if (e.key === 'Enter' && !e.isComposing) {
2369
+ e.preventDefault();
2370
+ createCodexConversation();
2371
+ }
2372
+ });
2212
2373
  // --- Settings modal (Webhook + Telegram) ---
2213
2374
  let webhookData = [];
2214
2375
  const closeSettings = () => $('#settingsOverlay').classList.add('hidden');
@@ -42,6 +42,7 @@ var CODEX_PID_FILE = path.join(CONFIG_DIR, "codex.pid");
42
42
  var CODEX_LOG_FILE = path.join(CONFIG_DIR, "codex.log");
43
43
  var WS_PATH_CHANNEL = "/ws/channel";
44
44
  var WS_PATH_DASHBOARD = "/ws/dashboard";
45
+ var WS_PATH_CODEX = "/ws/codex";
45
46
 
46
47
  // src/shared/peer-name.ts
47
48
  var SAFE_PEER_NAME = /^[^\u0000-\u001f\u007f"\\[\]]{1,128}$/;
@@ -175,6 +176,7 @@ var Notifier = class {
175
176
  execFile(
176
177
  "powershell",
177
178
  ["-Command", '(Get-ItemProperty -Path "HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\PushNotifications" -Name ToastEnabled -ErrorAction SilentlyContinue).ToastEnabled'],
179
+ { windowsHide: true },
178
180
  (err, stdout) => {
179
181
  if (err) {
180
182
  resolve(true);
@@ -192,7 +194,7 @@ var Notifier = class {
192
194
  logger.warn("Windows notifications are disabled. Opening notification settings...");
193
195
  logger.warn("Please enable notifications for this app, then try again.");
194
196
  if (process.platform === "win32") {
195
- execFile("powershell", ["-Command", "Start-Process ms-settings:notifications"]);
197
+ execFile("powershell", ["-Command", "Start-Process ms-settings:notifications"], { windowsHide: true });
196
198
  }
197
199
  setTimeout(() => {
198
200
  this.notificationSettingsOpened = false;
@@ -920,6 +922,21 @@ function logStartup(context) {
920
922
  `);
921
923
  }
922
924
 
925
+ // src/shared/entry.ts
926
+ function isEntryScript(argv1, suffixes) {
927
+ if (!argv1) return false;
928
+ const normalized = argv1.replace(/\\/g, "/");
929
+ return suffixes.some((suffix) => normalized.endsWith(suffix));
930
+ }
931
+
932
+ // src/shared/hub-url.ts
933
+ import net from "net";
934
+ function hubUrlHost(host) {
935
+ if (host === "" || host === "0.0.0.0") return "127.0.0.1";
936
+ if (host === "::") return "[::1]";
937
+ return net.isIPv6(host) ? `[${host}]` : host;
938
+ }
939
+
923
940
  // src/hub/auth.ts
924
941
  import crypto from "crypto";
925
942
  var SESSION_COOKIE = "ca_session";
@@ -985,6 +1002,7 @@ function validChoices(choices) {
985
1002
  const valid = choices.filter((c) => typeof c?.id === "string" && typeof c?.label === "string");
986
1003
  return valid.length ? valid : void 0;
987
1004
  }
1005
+ var nonEmpty = (v) => typeof v === "string" && v.trim() !== "";
988
1006
  var HubServer = class {
989
1007
  httpServer;
990
1008
  wssChannel;
@@ -1010,7 +1028,13 @@ var HubServer = class {
1010
1028
  token;
1011
1029
  codexEnabled;
1012
1030
  codexSupervisor;
1013
- constructor(config) {
1031
+ wssCodex;
1032
+ codexAdapters = /* @__PURE__ */ new Map();
1033
+ codexAlive = /* @__PURE__ */ new WeakMap();
1034
+ codexCallTimeoutMs;
1035
+ codexCalls = /* @__PURE__ */ new Map();
1036
+ constructor(config, options = {}) {
1037
+ this.codexCallTimeoutMs = options.codexCallTimeoutMs ?? 6e4;
1014
1038
  this.host = config?.hub?.host ?? DEFAULT_HUB_HOST;
1015
1039
  this.port = config?.hub?.port ?? DEFAULT_HUB_PORT;
1016
1040
  this.token = config?.hub?.token;
@@ -1023,7 +1047,7 @@ var HubServer = class {
1023
1047
  if (config?.webhooks) {
1024
1048
  this.notifier.configure({ webhooks: config.webhooks });
1025
1049
  }
1026
- const displayHost = this.host === "0.0.0.0" ? "127.0.0.1" : this.host;
1050
+ const displayHost = hubUrlHost(this.host);
1027
1051
  this.notifier.configure({ dashboardUrl: `http://${displayHost}:${this.port}` });
1028
1052
  const fullConfig = loadConfig();
1029
1053
  if (fullConfig.telegram?.enabled && fullConfig.telegram.botToken && fullConfig.telegram.chatId) {
@@ -1036,6 +1060,9 @@ var HubServer = class {
1036
1060
  this.wssDashboard = new WebSocketServer({ noServer: true, maxPayload: MAX_WS_PAYLOAD });
1037
1061
  this.wssDashboard.on("connection", (ws) => this.handleDashboardConnection(ws));
1038
1062
  this.wssDashboard.on("error", (err) => logger.warn(`Dashboard WebSocket server error: ${err.message}`));
1063
+ this.wssCodex = new WebSocketServer({ noServer: true, maxPayload: MAX_WS_PAYLOAD });
1064
+ this.wssCodex.on("connection", (ws, req) => this.handleCodexConnection(ws, req));
1065
+ this.wssCodex.on("error", (err) => logger.warn(`Codex WebSocket server error: ${err.message}`));
1039
1066
  this.httpServer.on("upgrade", (req, socket, head) => {
1040
1067
  socket.on("error", (err) => logger.debug(`Upgrade socket error: ${err.message}`));
1041
1068
  const url = new URL(req.url ?? "/", "http://hub");
@@ -1056,6 +1083,10 @@ var HubServer = class {
1056
1083
  this.wssDashboard.handleUpgrade(req, socket, head, (ws) => {
1057
1084
  this.wssDashboard.emit("connection", ws, req);
1058
1085
  });
1086
+ } else if (pathname === WS_PATH_CODEX) {
1087
+ this.wssCodex.handleUpgrade(req, socket, head, (ws) => {
1088
+ this.wssCodex.emit("connection", ws, req);
1089
+ });
1059
1090
  } else {
1060
1091
  socket.destroy();
1061
1092
  }
@@ -1070,7 +1101,7 @@ var HubServer = class {
1070
1101
  this.httpServer.listen(this.port, this.host, () => {
1071
1102
  this.httpServer.removeListener("error", onStartupError);
1072
1103
  this.httpServer.on("error", (err) => logger.error(`HTTP server error: ${err.message}`));
1073
- const displayHost = this.host === "0.0.0.0" ? "127.0.0.1" : this.host;
1104
+ const displayHost = hubUrlHost(this.host);
1074
1105
  logger.info(`Hub server listening on http://${displayHost}:${this.port}`);
1075
1106
  this.startCodexAdapter();
1076
1107
  resolve();
@@ -1085,10 +1116,12 @@ var HubServer = class {
1085
1116
  if (this.telegramBot) this.telegramBot.stopPolling();
1086
1117
  for (const ws of this.channelSockets.values()) ws.terminate();
1087
1118
  for (const ws of this.dashboardSockets) ws.terminate();
1119
+ for (const { ws } of this.codexAdapters.values()) ws.terminate();
1088
1120
  this.channelSockets.clear();
1089
1121
  this.dashboardSockets.clear();
1090
1122
  this.wssChannel.close();
1091
1123
  this.wssDashboard.close();
1124
+ this.wssCodex.close();
1092
1125
  this.httpServer.close(() => {
1093
1126
  logger.info("Hub server stopped");
1094
1127
  resolve();
@@ -1153,6 +1186,12 @@ var HubServer = class {
1153
1186
  this.handleApiSend(req, res);
1154
1187
  } else if (url.pathname === "/api/notify" && req.method === "POST") {
1155
1188
  this.handleApiNotify(req, res);
1189
+ } else if (url.pathname === "/api/codex/folders" && req.method === "GET") {
1190
+ this.handleCodexFolders(url, res);
1191
+ } else if (url.pathname === "/api/codex/threads" && req.method === "POST") {
1192
+ this.handleCodexCreate(req, res);
1193
+ } else if (url.pathname === "/api/codex/threads/close" && req.method === "POST") {
1194
+ this.handleCodexClose(req, res);
1156
1195
  } else if (url.pathname === "/api/webhooks" && req.method === "GET") {
1157
1196
  const config = loadConfig();
1158
1197
  this.jsonResponse(res, 200, { webhooks: config.webhooks || [] });
@@ -1416,6 +1455,114 @@ var HubServer = class {
1416
1455
  }
1417
1456
  }
1418
1457
  }
1458
+ handleCodexConnection(ws, req) {
1459
+ const isLocal = this.isLocalRequest(req);
1460
+ let adapterId;
1461
+ ws.on("error", (err) => {
1462
+ logger.warn(`Codex adapter WebSocket error: ${err.message}`);
1463
+ ws.terminate();
1464
+ });
1465
+ ws.on("pong", () => this.codexAlive.set(ws, true));
1466
+ ws.on("message", (data) => {
1467
+ let msg;
1468
+ try {
1469
+ msg = JSON.parse(data.toString());
1470
+ if (msg === null || typeof msg !== "object") {
1471
+ logger.warn("Invalid message from Codex adapter");
1472
+ return;
1473
+ }
1474
+ } catch {
1475
+ logger.warn("Invalid message from Codex adapter");
1476
+ return;
1477
+ }
1478
+ if (msg.type === "adapter_hello") {
1479
+ const a = msg.adapter;
1480
+ if (typeof a?.id !== "string" || !a.id || typeof a.host !== "string") return;
1481
+ if (adapterId && adapterId !== a.id) return;
1482
+ const holder = this.codexAdapters.get(a.id);
1483
+ if (holder && holder.ws !== ws) holder.ws.terminate();
1484
+ if (!adapterId) logger.info(`Codex adapter connected: ${a.host} (${a.id}, local: ${isLocal})`);
1485
+ adapterId = a.id;
1486
+ this.codexAdapters.set(a.id, { ws, info: { id: a.id, host: a.host, ready: a.ready === true, isLocal } });
1487
+ this.broadcastCodexAdapters();
1488
+ } else if (msg.type === "adapter_result" && adapterId) {
1489
+ const call = this.codexCalls.get(msg.requestId);
1490
+ if (!call || call.adapterId !== adapterId) return;
1491
+ call.settle(msg.ok ? { status: 200, body: msg.data } : { status: 422, body: { error: String(msg.error) } });
1492
+ }
1493
+ });
1494
+ ws.on("close", () => {
1495
+ if (!adapterId || this.codexAdapters.get(adapterId)?.ws !== ws) return;
1496
+ this.codexAdapters.delete(adapterId);
1497
+ for (const call of [...this.codexCalls.values()]) {
1498
+ if (call.adapterId === adapterId) call.settle({ status: 502, body: { error: "Codex adapter disconnected" } });
1499
+ }
1500
+ logger.info(`Codex adapter disconnected: ${adapterId}`);
1501
+ this.broadcastCodexAdapters();
1502
+ });
1503
+ }
1504
+ callCodexAdapter(adapterId, call) {
1505
+ const adapter = this.codexAdapters.get(adapterId);
1506
+ if (!adapter || adapter.ws.readyState !== WebSocket.OPEN) {
1507
+ return Promise.resolve({ status: 404, body: { error: "Codex adapter is not connected" } });
1508
+ }
1509
+ const requestId = randomUUID3();
1510
+ return new Promise((resolve) => {
1511
+ const timer = setTimeout(
1512
+ () => settle({ status: 504, body: { error: "Codex did not respond in time. The conversation may still appear." } }),
1513
+ this.codexCallTimeoutMs
1514
+ );
1515
+ const settle = (outcome) => {
1516
+ clearTimeout(timer);
1517
+ this.codexCalls.delete(requestId);
1518
+ resolve(outcome);
1519
+ };
1520
+ this.codexCalls.set(requestId, { adapterId, settle });
1521
+ adapter.ws.send(JSON.stringify({ type: "adapter_call", requestId, call }));
1522
+ });
1523
+ }
1524
+ async handleCodexFolders(url, res) {
1525
+ const adapterId = url.searchParams.get("adapterId");
1526
+ if (!nonEmpty(adapterId)) {
1527
+ this.jsonResponse(res, 400, { error: "adapterId is required" });
1528
+ return;
1529
+ }
1530
+ const out = await this.callCodexAdapter(adapterId, { kind: "folders" });
1531
+ this.jsonResponse(res, out.status, out.body);
1532
+ }
1533
+ async handleCodexCreate(req, res) {
1534
+ const body = await this.readBody(req);
1535
+ const adapterId = body?.adapterId;
1536
+ const cwd = body?.cwd;
1537
+ if (!nonEmpty(adapterId) || !nonEmpty(cwd)) {
1538
+ this.jsonResponse(res, 400, { error: "adapterId and cwd are required" });
1539
+ return;
1540
+ }
1541
+ const out = await this.callCodexAdapter(adapterId, { kind: "create", cwd });
1542
+ this.jsonResponse(res, out.status, out.body);
1543
+ }
1544
+ async handleCodexClose(req, res) {
1545
+ const body = await this.readBody(req);
1546
+ const sessionId = body?.sessionId;
1547
+ if (!nonEmpty(sessionId)) {
1548
+ this.jsonResponse(res, 400, { error: "sessionId is required" });
1549
+ return;
1550
+ }
1551
+ const session = this.sessions.get(sessionId);
1552
+ const ws = this.channelSockets.get(sessionId);
1553
+ if (session?.agentKind !== "codex" || !session.closable || ws?.readyState !== WebSocket.OPEN) {
1554
+ this.jsonResponse(res, 404, { error: "No closable Codex conversation" });
1555
+ return;
1556
+ }
1557
+ ws.send(JSON.stringify({ type: "codex_close", sessionId }));
1558
+ this.jsonResponse(res, 200, { ok: true });
1559
+ }
1560
+ codexAdapterList() {
1561
+ return [...this.codexAdapters.values()].map((a) => a.info);
1562
+ }
1563
+ broadcastCodexAdapters() {
1564
+ this.broadcastToDashboards({ type: "codex_adapters", adapters: this.codexAdapterList() });
1565
+ }
1419
1566
  // --- Dashboard WebSocket ---
1420
1567
  handleDashboardConnection(ws) {
1421
1568
  this.dashboardSockets.add(ws);
@@ -1434,6 +1581,7 @@ var HubServer = class {
1434
1581
  requests: [...this.choiceRequests.values()].map((c) => c.request)
1435
1582
  };
1436
1583
  ws.send(JSON.stringify(pendingMsg));
1584
+ ws.send(JSON.stringify({ type: "codex_adapters", adapters: this.codexAdapterList() }));
1437
1585
  ws.on("message", (data) => {
1438
1586
  try {
1439
1587
  const msg = JSON.parse(data.toString());
@@ -1521,7 +1669,8 @@ var HubServer = class {
1521
1669
  imagePath: filePath,
1522
1670
  mimeType,
1523
1671
  originalName,
1524
- content
1672
+ content,
1673
+ source: "dashboard"
1525
1674
  };
1526
1675
  channelWs.send(JSON.stringify(forwardMsg));
1527
1676
  logger.info(`Image saved and forwarded: ${filename} (${buffer.length} bytes)`);
@@ -1530,7 +1679,7 @@ var HubServer = class {
1530
1679
  fs5.unlinkSync(filePath);
1531
1680
  } catch {
1532
1681
  }
1533
- }, 5 * 60 * 1e3);
1682
+ }, 5 * 60 * 1e3).unref();
1534
1683
  }
1535
1684
  async handleWebhookSave(req, res) {
1536
1685
  const body = await this.readBody(req);
@@ -1563,7 +1712,7 @@ var HubServer = class {
1563
1712
  this.telegramBot.onImageToSession = (sessionId, imagePath, mimeType, caption) => {
1564
1713
  const channelWs = this.channelSockets.get(sessionId);
1565
1714
  if (channelWs?.readyState === WebSocket.OPEN) {
1566
- const msg = { type: "image_to_session", sessionId, imagePath, mimeType, content: caption };
1715
+ const msg = { type: "image_to_session", sessionId, imagePath, mimeType, content: caption, source: "telegram" };
1567
1716
  channelWs.send(JSON.stringify(msg));
1568
1717
  logger.info(`Telegram photo forwarded to session: ${sessionId}`);
1569
1718
  }
@@ -1706,6 +1855,14 @@ var HubServer = class {
1706
1855
  this.channelAlive.set(sessionId, false);
1707
1856
  ws.ping();
1708
1857
  }
1858
+ for (const { ws } of this.codexAdapters.values()) {
1859
+ if (this.codexAlive.get(ws) === false) {
1860
+ ws.terminate();
1861
+ continue;
1862
+ }
1863
+ this.codexAlive.set(ws, false);
1864
+ ws.ping();
1865
+ }
1709
1866
  }, 3e4);
1710
1867
  }
1711
1868
  cleanupUploads() {
@@ -1768,7 +1925,7 @@ Content-Length: 0\r
1768
1925
  });
1769
1926
  }
1770
1927
  };
1771
- if (process.argv[1] && (process.argv[1].endsWith("hub/server.js") || process.argv[1].endsWith("hub/server.ts"))) {
1928
+ if (isEntryScript(process.argv[1], ["/hub/server.js", "/hub/server.ts"])) {
1772
1929
  installCrashGuard("hub daemon");
1773
1930
  logStartup("Hub daemon");
1774
1931
  const config = loadConfig();