@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/README.md +17 -10
- package/dist/channel/server.js.map +1 -1
- package/dist/cli.js +906 -67
- package/dist/cli.js.map +1 -1
- package/dist/codex/main.js +380 -63
- package/dist/codex/main.js.map +1 -1
- package/dist/dashboard/index.html +162 -1
- package/dist/hub/server.js +165 -8
- package/dist/hub/server.js.map +1 -1
- package/dist/index.d.ts +60 -2
- package/dist/index.js +166 -8
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/dashboard/index.html +162 -1
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
|
|
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 =
|
|
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(
|
|
917
|
-
const m = /\.([a-z0-9]+)$/i.exec(
|
|
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
|
|
980
|
+
import path6 from "path";
|
|
966
981
|
function resolveAdapterScript(baseDir) {
|
|
967
|
-
return [
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1377
|
+
path7.join(__dirname, "..", "dashboard", "index.html"),
|
|
1327
1378
|
// from dist/hub/
|
|
1328
|
-
|
|
1379
|
+
path7.join(__dirname, "dashboard", "index.html"),
|
|
1329
1380
|
// from dist/ (bundled index.js)
|
|
1330
|
-
|
|
1381
|
+
path7.join(__dirname, "..", "..", "src", "dashboard", "index.html"),
|
|
1331
1382
|
// from dist/hub/ -> src/
|
|
1332
|
-
|
|
1383
|
+
path7.join(__dirname, "..", "src", "dashboard", "index.html"),
|
|
1333
1384
|
// from dist/ -> src/
|
|
1334
|
-
|
|
1385
|
+
path7.join(process.cwd(), "dist", "dashboard", "index.html"),
|
|
1335
1386
|
// from cwd
|
|
1336
|
-
|
|
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 =
|
|
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(
|
|
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]
|
|
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
|
|
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 ||
|
|
2080
|
-
const dir =
|
|
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(
|
|
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
|
|
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 ??
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
-
|
|
2460
|
-
|
|
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
|
|
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://${
|
|
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 =
|
|
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 =
|
|
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
|
|
3429
|
+
const displayHost = hubUrlHost(host);
|
|
2628
3430
|
let hubRunning = false;
|
|
2629
3431
|
try {
|
|
2630
|
-
const res = await fetch(`http://${
|
|
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,
|
|
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
|
|
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
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
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
|
-
|
|
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();
|