@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/index.d.ts CHANGED
@@ -29,6 +29,7 @@ interface SessionInfo {
29
29
  peerName?: string;
30
30
  agentKind?: AgentKind;
31
31
  title?: string;
32
+ closable?: boolean;
32
33
  }
33
34
  /** Messages sent between channel server and hub */
34
35
  type ChannelMessage = {
@@ -71,6 +72,7 @@ type ChannelMessage = {
71
72
  mimeType: string;
72
73
  originalName?: string;
73
74
  content?: string;
75
+ source?: MessageSource;
74
76
  } | {
75
77
  type: 'sessions_list';
76
78
  sessions: SessionInfo[];
@@ -118,10 +120,51 @@ type ChannelMessage = {
118
120
  } | {
119
121
  type: 'permission_pending';
120
122
  requests: PendingChoiceRequest[];
123
+ } | {
124
+ type: 'codex_adapters';
125
+ adapters: CodexAdapterInfo[];
126
+ } | {
127
+ type: 'codex_close';
128
+ sessionId: string;
121
129
  } | {
122
130
  type: 'error';
123
131
  message: string;
124
132
  };
133
+ interface CodexAdapterInfo {
134
+ id: string;
135
+ host: string;
136
+ ready: boolean;
137
+ isLocal: boolean;
138
+ }
139
+ type CodexCall = {
140
+ kind: 'folders';
141
+ } | {
142
+ kind: 'create';
143
+ cwd: string;
144
+ };
145
+ /** Messages on the per-adapter socket between the Codex adapter and the hub */
146
+ type CodexLinkMessage = {
147
+ type: 'adapter_hello';
148
+ adapter: {
149
+ id: string;
150
+ host: string;
151
+ ready: boolean;
152
+ };
153
+ } | {
154
+ type: 'adapter_call';
155
+ requestId: string;
156
+ call: CodexCall;
157
+ } | {
158
+ type: 'adapter_result';
159
+ requestId: string;
160
+ ok: true;
161
+ data: unknown;
162
+ } | {
163
+ type: 'adapter_result';
164
+ requestId: string;
165
+ ok: false;
166
+ error: string;
167
+ };
125
168
  type NotifyLevel = 'info' | 'warning' | 'error' | 'success';
126
169
  /** Webhook configuration */
127
170
  interface WebhookConfig {
@@ -184,7 +227,14 @@ declare class HubServer {
184
227
  private token?;
185
228
  private codexEnabled;
186
229
  private codexSupervisor?;
187
- constructor(config?: Partial<AppConfig>);
230
+ private wssCodex;
231
+ private codexAdapters;
232
+ private codexAlive;
233
+ private codexCallTimeoutMs;
234
+ private codexCalls;
235
+ constructor(config?: Partial<AppConfig>, options?: {
236
+ codexCallTimeoutMs?: number;
237
+ });
188
238
  start(): Promise<void>;
189
239
  stop(): Promise<void>;
190
240
  private handleHttp;
@@ -194,6 +244,13 @@ declare class HubServer {
194
244
  private handleApiNotify;
195
245
  private handleChannelConnection;
196
246
  private handleChannelMessage;
247
+ private handleCodexConnection;
248
+ private callCodexAdapter;
249
+ private handleCodexFolders;
250
+ private handleCodexCreate;
251
+ private handleCodexClose;
252
+ private codexAdapterList;
253
+ private broadcastCodexAdapters;
197
254
  private handleDashboardConnection;
198
255
  private forwardPermissionResponse;
199
256
  private resolveChoice;
@@ -355,7 +412,8 @@ declare const CODEX_PID_FILE: string;
355
412
  declare const CODEX_LOG_FILE: string;
356
413
  declare const WS_PATH_CHANNEL = "/ws/channel";
357
414
  declare const WS_PATH_DASHBOARD = "/ws/dashboard";
415
+ declare const WS_PATH_CODEX = "/ws/codex";
358
416
  declare const CHANNEL_SERVER_NAME = "claude-alarm";
359
417
  declare const CHANNEL_SERVER_VERSION = "0.1.0";
360
418
 
361
- export { type AgentKind, type AppConfig, CHANNEL_SERVER_NAME, CHANNEL_SERVER_VERSION, CODEX_LOG_FILE, CODEX_PID_FILE, CONFIG_DIR, CONFIG_FILE, type ChannelMessage, type CodexConfig, DEFAULT_HUB_HOST, DEFAULT_HUB_PORT, HubClient, HubServer, type HubStatus, LOG_FILE, type MessageSource, Notifier, type NotifyLevel, PID_FILE, type PendingChoiceRequest, type PermissionChoice, type SessionInfo, SessionManager, type SessionStatus, type TelegramConfig, UPLOADS_DIR, WS_PATH_CHANNEL, WS_PATH_DASHBOARD, type WebhookConfig, loadConfig, logger, saveConfig, setupMcpConfig };
419
+ export { type AgentKind, type AppConfig, CHANNEL_SERVER_NAME, CHANNEL_SERVER_VERSION, CODEX_LOG_FILE, CODEX_PID_FILE, CONFIG_DIR, CONFIG_FILE, type ChannelMessage, type CodexAdapterInfo, type CodexCall, type CodexConfig, type CodexLinkMessage, DEFAULT_HUB_HOST, DEFAULT_HUB_PORT, HubClient, HubServer, type HubStatus, LOG_FILE, type MessageSource, Notifier, type NotifyLevel, PID_FILE, type PendingChoiceRequest, type PermissionChoice, type SessionInfo, SessionManager, type SessionStatus, type TelegramConfig, UPLOADS_DIR, WS_PATH_CHANNEL, WS_PATH_CODEX, WS_PATH_DASHBOARD, type WebhookConfig, loadConfig, logger, saveConfig, setupMcpConfig };
package/dist/index.js CHANGED
@@ -40,6 +40,7 @@ var CODEX_PID_FILE = path.join(CONFIG_DIR, "codex.pid");
40
40
  var CODEX_LOG_FILE = path.join(CONFIG_DIR, "codex.log");
41
41
  var WS_PATH_CHANNEL = "/ws/channel";
42
42
  var WS_PATH_DASHBOARD = "/ws/dashboard";
43
+ var WS_PATH_CODEX = "/ws/codex";
43
44
  var CHANNEL_SERVER_NAME = "claude-alarm";
44
45
  var CHANNEL_SERVER_VERSION = "0.1.0";
45
46
 
@@ -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;
@@ -944,6 +946,21 @@ function logStartup(context) {
944
946
  `);
945
947
  }
946
948
 
949
+ // src/shared/entry.ts
950
+ function isEntryScript(argv1, suffixes) {
951
+ if (!argv1) return false;
952
+ const normalized = argv1.replace(/\\/g, "/");
953
+ return suffixes.some((suffix) => normalized.endsWith(suffix));
954
+ }
955
+
956
+ // src/shared/hub-url.ts
957
+ import net from "net";
958
+ function hubUrlHost(host) {
959
+ if (host === "" || host === "0.0.0.0") return "127.0.0.1";
960
+ if (host === "::") return "[::1]";
961
+ return net.isIPv6(host) ? `[${host}]` : host;
962
+ }
963
+
947
964
  // src/hub/auth.ts
948
965
  import crypto from "crypto";
949
966
  var SESSION_COOKIE = "ca_session";
@@ -1009,6 +1026,7 @@ function validChoices(choices) {
1009
1026
  const valid = choices.filter((c) => typeof c?.id === "string" && typeof c?.label === "string");
1010
1027
  return valid.length ? valid : void 0;
1011
1028
  }
1029
+ var nonEmpty = (v) => typeof v === "string" && v.trim() !== "";
1012
1030
  var HubServer = class {
1013
1031
  httpServer;
1014
1032
  wssChannel;
@@ -1034,7 +1052,13 @@ var HubServer = class {
1034
1052
  token;
1035
1053
  codexEnabled;
1036
1054
  codexSupervisor;
1037
- constructor(config) {
1055
+ wssCodex;
1056
+ codexAdapters = /* @__PURE__ */ new Map();
1057
+ codexAlive = /* @__PURE__ */ new WeakMap();
1058
+ codexCallTimeoutMs;
1059
+ codexCalls = /* @__PURE__ */ new Map();
1060
+ constructor(config, options = {}) {
1061
+ this.codexCallTimeoutMs = options.codexCallTimeoutMs ?? 6e4;
1038
1062
  this.host = config?.hub?.host ?? DEFAULT_HUB_HOST;
1039
1063
  this.port = config?.hub?.port ?? DEFAULT_HUB_PORT;
1040
1064
  this.token = config?.hub?.token;
@@ -1047,7 +1071,7 @@ var HubServer = class {
1047
1071
  if (config?.webhooks) {
1048
1072
  this.notifier.configure({ webhooks: config.webhooks });
1049
1073
  }
1050
- const displayHost = this.host === "0.0.0.0" ? "127.0.0.1" : this.host;
1074
+ const displayHost = hubUrlHost(this.host);
1051
1075
  this.notifier.configure({ dashboardUrl: `http://${displayHost}:${this.port}` });
1052
1076
  const fullConfig = loadConfig();
1053
1077
  if (fullConfig.telegram?.enabled && fullConfig.telegram.botToken && fullConfig.telegram.chatId) {
@@ -1060,6 +1084,9 @@ var HubServer = class {
1060
1084
  this.wssDashboard = new WebSocketServer({ noServer: true, maxPayload: MAX_WS_PAYLOAD });
1061
1085
  this.wssDashboard.on("connection", (ws) => this.handleDashboardConnection(ws));
1062
1086
  this.wssDashboard.on("error", (err) => logger.warn(`Dashboard WebSocket server error: ${err.message}`));
1087
+ this.wssCodex = new WebSocketServer({ noServer: true, maxPayload: MAX_WS_PAYLOAD });
1088
+ this.wssCodex.on("connection", (ws, req) => this.handleCodexConnection(ws, req));
1089
+ this.wssCodex.on("error", (err) => logger.warn(`Codex WebSocket server error: ${err.message}`));
1063
1090
  this.httpServer.on("upgrade", (req, socket, head) => {
1064
1091
  socket.on("error", (err) => logger.debug(`Upgrade socket error: ${err.message}`));
1065
1092
  const url = new URL(req.url ?? "/", "http://hub");
@@ -1080,6 +1107,10 @@ var HubServer = class {
1080
1107
  this.wssDashboard.handleUpgrade(req, socket, head, (ws) => {
1081
1108
  this.wssDashboard.emit("connection", ws, req);
1082
1109
  });
1110
+ } else if (pathname === WS_PATH_CODEX) {
1111
+ this.wssCodex.handleUpgrade(req, socket, head, (ws) => {
1112
+ this.wssCodex.emit("connection", ws, req);
1113
+ });
1083
1114
  } else {
1084
1115
  socket.destroy();
1085
1116
  }
@@ -1094,7 +1125,7 @@ var HubServer = class {
1094
1125
  this.httpServer.listen(this.port, this.host, () => {
1095
1126
  this.httpServer.removeListener("error", onStartupError);
1096
1127
  this.httpServer.on("error", (err) => logger.error(`HTTP server error: ${err.message}`));
1097
- const displayHost = this.host === "0.0.0.0" ? "127.0.0.1" : this.host;
1128
+ const displayHost = hubUrlHost(this.host);
1098
1129
  logger.info(`Hub server listening on http://${displayHost}:${this.port}`);
1099
1130
  this.startCodexAdapter();
1100
1131
  resolve();
@@ -1109,10 +1140,12 @@ var HubServer = class {
1109
1140
  if (this.telegramBot) this.telegramBot.stopPolling();
1110
1141
  for (const ws of this.channelSockets.values()) ws.terminate();
1111
1142
  for (const ws of this.dashboardSockets) ws.terminate();
1143
+ for (const { ws } of this.codexAdapters.values()) ws.terminate();
1112
1144
  this.channelSockets.clear();
1113
1145
  this.dashboardSockets.clear();
1114
1146
  this.wssChannel.close();
1115
1147
  this.wssDashboard.close();
1148
+ this.wssCodex.close();
1116
1149
  this.httpServer.close(() => {
1117
1150
  logger.info("Hub server stopped");
1118
1151
  resolve();
@@ -1177,6 +1210,12 @@ var HubServer = class {
1177
1210
  this.handleApiSend(req, res);
1178
1211
  } else if (url.pathname === "/api/notify" && req.method === "POST") {
1179
1212
  this.handleApiNotify(req, res);
1213
+ } else if (url.pathname === "/api/codex/folders" && req.method === "GET") {
1214
+ this.handleCodexFolders(url, res);
1215
+ } else if (url.pathname === "/api/codex/threads" && req.method === "POST") {
1216
+ this.handleCodexCreate(req, res);
1217
+ } else if (url.pathname === "/api/codex/threads/close" && req.method === "POST") {
1218
+ this.handleCodexClose(req, res);
1180
1219
  } else if (url.pathname === "/api/webhooks" && req.method === "GET") {
1181
1220
  const config = loadConfig();
1182
1221
  this.jsonResponse(res, 200, { webhooks: config.webhooks || [] });
@@ -1440,6 +1479,114 @@ var HubServer = class {
1440
1479
  }
1441
1480
  }
1442
1481
  }
1482
+ handleCodexConnection(ws, req) {
1483
+ const isLocal = this.isLocalRequest(req);
1484
+ let adapterId;
1485
+ ws.on("error", (err) => {
1486
+ logger.warn(`Codex adapter WebSocket error: ${err.message}`);
1487
+ ws.terminate();
1488
+ });
1489
+ ws.on("pong", () => this.codexAlive.set(ws, true));
1490
+ ws.on("message", (data) => {
1491
+ let msg;
1492
+ try {
1493
+ msg = JSON.parse(data.toString());
1494
+ if (msg === null || typeof msg !== "object") {
1495
+ logger.warn("Invalid message from Codex adapter");
1496
+ return;
1497
+ }
1498
+ } catch {
1499
+ logger.warn("Invalid message from Codex adapter");
1500
+ return;
1501
+ }
1502
+ if (msg.type === "adapter_hello") {
1503
+ const a = msg.adapter;
1504
+ if (typeof a?.id !== "string" || !a.id || typeof a.host !== "string") return;
1505
+ if (adapterId && adapterId !== a.id) return;
1506
+ const holder = this.codexAdapters.get(a.id);
1507
+ if (holder && holder.ws !== ws) holder.ws.terminate();
1508
+ if (!adapterId) logger.info(`Codex adapter connected: ${a.host} (${a.id}, local: ${isLocal})`);
1509
+ adapterId = a.id;
1510
+ this.codexAdapters.set(a.id, { ws, info: { id: a.id, host: a.host, ready: a.ready === true, isLocal } });
1511
+ this.broadcastCodexAdapters();
1512
+ } else if (msg.type === "adapter_result" && adapterId) {
1513
+ const call = this.codexCalls.get(msg.requestId);
1514
+ if (!call || call.adapterId !== adapterId) return;
1515
+ call.settle(msg.ok ? { status: 200, body: msg.data } : { status: 422, body: { error: String(msg.error) } });
1516
+ }
1517
+ });
1518
+ ws.on("close", () => {
1519
+ if (!adapterId || this.codexAdapters.get(adapterId)?.ws !== ws) return;
1520
+ this.codexAdapters.delete(adapterId);
1521
+ for (const call of [...this.codexCalls.values()]) {
1522
+ if (call.adapterId === adapterId) call.settle({ status: 502, body: { error: "Codex adapter disconnected" } });
1523
+ }
1524
+ logger.info(`Codex adapter disconnected: ${adapterId}`);
1525
+ this.broadcastCodexAdapters();
1526
+ });
1527
+ }
1528
+ callCodexAdapter(adapterId, call) {
1529
+ const adapter = this.codexAdapters.get(adapterId);
1530
+ if (!adapter || adapter.ws.readyState !== WebSocket.OPEN) {
1531
+ return Promise.resolve({ status: 404, body: { error: "Codex adapter is not connected" } });
1532
+ }
1533
+ const requestId = randomUUID3();
1534
+ return new Promise((resolve) => {
1535
+ const timer = setTimeout(
1536
+ () => settle({ status: 504, body: { error: "Codex did not respond in time. The conversation may still appear." } }),
1537
+ this.codexCallTimeoutMs
1538
+ );
1539
+ const settle = (outcome) => {
1540
+ clearTimeout(timer);
1541
+ this.codexCalls.delete(requestId);
1542
+ resolve(outcome);
1543
+ };
1544
+ this.codexCalls.set(requestId, { adapterId, settle });
1545
+ adapter.ws.send(JSON.stringify({ type: "adapter_call", requestId, call }));
1546
+ });
1547
+ }
1548
+ async handleCodexFolders(url, res) {
1549
+ const adapterId = url.searchParams.get("adapterId");
1550
+ if (!nonEmpty(adapterId)) {
1551
+ this.jsonResponse(res, 400, { error: "adapterId is required" });
1552
+ return;
1553
+ }
1554
+ const out = await this.callCodexAdapter(adapterId, { kind: "folders" });
1555
+ this.jsonResponse(res, out.status, out.body);
1556
+ }
1557
+ async handleCodexCreate(req, res) {
1558
+ const body = await this.readBody(req);
1559
+ const adapterId = body?.adapterId;
1560
+ const cwd = body?.cwd;
1561
+ if (!nonEmpty(adapterId) || !nonEmpty(cwd)) {
1562
+ this.jsonResponse(res, 400, { error: "adapterId and cwd are required" });
1563
+ return;
1564
+ }
1565
+ const out = await this.callCodexAdapter(adapterId, { kind: "create", cwd });
1566
+ this.jsonResponse(res, out.status, out.body);
1567
+ }
1568
+ async handleCodexClose(req, res) {
1569
+ const body = await this.readBody(req);
1570
+ const sessionId = body?.sessionId;
1571
+ if (!nonEmpty(sessionId)) {
1572
+ this.jsonResponse(res, 400, { error: "sessionId is required" });
1573
+ return;
1574
+ }
1575
+ const session = this.sessions.get(sessionId);
1576
+ const ws = this.channelSockets.get(sessionId);
1577
+ if (session?.agentKind !== "codex" || !session.closable || ws?.readyState !== WebSocket.OPEN) {
1578
+ this.jsonResponse(res, 404, { error: "No closable Codex conversation" });
1579
+ return;
1580
+ }
1581
+ ws.send(JSON.stringify({ type: "codex_close", sessionId }));
1582
+ this.jsonResponse(res, 200, { ok: true });
1583
+ }
1584
+ codexAdapterList() {
1585
+ return [...this.codexAdapters.values()].map((a) => a.info);
1586
+ }
1587
+ broadcastCodexAdapters() {
1588
+ this.broadcastToDashboards({ type: "codex_adapters", adapters: this.codexAdapterList() });
1589
+ }
1443
1590
  // --- Dashboard WebSocket ---
1444
1591
  handleDashboardConnection(ws) {
1445
1592
  this.dashboardSockets.add(ws);
@@ -1458,6 +1605,7 @@ var HubServer = class {
1458
1605
  requests: [...this.choiceRequests.values()].map((c) => c.request)
1459
1606
  };
1460
1607
  ws.send(JSON.stringify(pendingMsg));
1608
+ ws.send(JSON.stringify({ type: "codex_adapters", adapters: this.codexAdapterList() }));
1461
1609
  ws.on("message", (data) => {
1462
1610
  try {
1463
1611
  const msg = JSON.parse(data.toString());
@@ -1545,7 +1693,8 @@ var HubServer = class {
1545
1693
  imagePath: filePath,
1546
1694
  mimeType,
1547
1695
  originalName,
1548
- content
1696
+ content,
1697
+ source: "dashboard"
1549
1698
  };
1550
1699
  channelWs.send(JSON.stringify(forwardMsg));
1551
1700
  logger.info(`Image saved and forwarded: ${filename} (${buffer.length} bytes)`);
@@ -1554,7 +1703,7 @@ var HubServer = class {
1554
1703
  fs5.unlinkSync(filePath);
1555
1704
  } catch {
1556
1705
  }
1557
- }, 5 * 60 * 1e3);
1706
+ }, 5 * 60 * 1e3).unref();
1558
1707
  }
1559
1708
  async handleWebhookSave(req, res) {
1560
1709
  const body = await this.readBody(req);
@@ -1587,7 +1736,7 @@ var HubServer = class {
1587
1736
  this.telegramBot.onImageToSession = (sessionId, imagePath, mimeType, caption) => {
1588
1737
  const channelWs = this.channelSockets.get(sessionId);
1589
1738
  if (channelWs?.readyState === WebSocket.OPEN) {
1590
- const msg = { type: "image_to_session", sessionId, imagePath, mimeType, content: caption };
1739
+ const msg = { type: "image_to_session", sessionId, imagePath, mimeType, content: caption, source: "telegram" };
1591
1740
  channelWs.send(JSON.stringify(msg));
1592
1741
  logger.info(`Telegram photo forwarded to session: ${sessionId}`);
1593
1742
  }
@@ -1730,6 +1879,14 @@ var HubServer = class {
1730
1879
  this.channelAlive.set(sessionId, false);
1731
1880
  ws.ping();
1732
1881
  }
1882
+ for (const { ws } of this.codexAdapters.values()) {
1883
+ if (this.codexAlive.get(ws) === false) {
1884
+ ws.terminate();
1885
+ continue;
1886
+ }
1887
+ this.codexAlive.set(ws, false);
1888
+ ws.ping();
1889
+ }
1733
1890
  }, 3e4);
1734
1891
  }
1735
1892
  cleanupUploads() {
@@ -1792,7 +1949,7 @@ Content-Length: 0\r
1792
1949
  });
1793
1950
  }
1794
1951
  };
1795
- if (process.argv[1] && (process.argv[1].endsWith("hub/server.js") || process.argv[1].endsWith("hub/server.ts"))) {
1952
+ if (isEntryScript(process.argv[1], ["/hub/server.js", "/hub/server.ts"])) {
1796
1953
  installCrashGuard("hub daemon");
1797
1954
  logStartup("Hub daemon");
1798
1955
  const config = loadConfig();
@@ -1942,6 +2099,7 @@ export {
1942
2099
  SessionManager,
1943
2100
  UPLOADS_DIR,
1944
2101
  WS_PATH_CHANNEL,
2102
+ WS_PATH_CODEX,
1945
2103
  WS_PATH_DASHBOARD,
1946
2104
  loadConfig,
1947
2105
  logger,