@delt/claude-alarm 1.1.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -10
- package/dist/channel/server.js.map +1 -1
- package/dist/cli.js +1258 -180
- package/dist/cli.js.map +1 -1
- package/dist/codex/main.js +660 -106
- package/dist/codex/main.js.map +1 -1
- package/dist/dashboard/index.html +176 -1
- package/dist/hub/server.js +308 -56
- package/dist/hub/server.js.map +1 -1
- package/dist/index.d.ts +71 -3
- package/dist/index.js +309 -56
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/dashboard/index.html +176 -1
package/dist/codex/main.js
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// src/codex/main.ts
|
|
4
|
-
import fs4 from "fs";
|
|
5
|
-
|
|
6
3
|
// src/shared/config.ts
|
|
7
4
|
import fs from "fs";
|
|
8
5
|
import path2 from "path";
|
|
@@ -21,6 +18,7 @@ var UPLOADS_DIR = path.join(CONFIG_DIR, "uploads");
|
|
|
21
18
|
var CODEX_PID_FILE = path.join(CONFIG_DIR, "codex.pid");
|
|
22
19
|
var CODEX_LOG_FILE = path.join(CONFIG_DIR, "codex.log");
|
|
23
20
|
var WS_PATH_CHANNEL = "/ws/channel";
|
|
21
|
+
var WS_PATH_CODEX = "/ws/codex";
|
|
24
22
|
var CHANNEL_SERVER_VERSION = "0.1.0";
|
|
25
23
|
|
|
26
24
|
// src/shared/config.ts
|
|
@@ -133,18 +131,122 @@ function logStartup(context) {
|
|
|
133
131
|
`);
|
|
134
132
|
}
|
|
135
133
|
|
|
134
|
+
// src/codex/adapter.ts
|
|
135
|
+
import fs5 from "fs";
|
|
136
|
+
import os2 from "os";
|
|
137
|
+
import path4 from "path";
|
|
138
|
+
|
|
139
|
+
// src/codex/hub-link.ts
|
|
140
|
+
import WebSocket from "ws";
|
|
141
|
+
var CodexHubLink = class {
|
|
142
|
+
constructor(hub, identity, onCall, reconnectMs = 5e3, pingTimeoutMs = 75e3) {
|
|
143
|
+
this.hub = hub;
|
|
144
|
+
this.identity = identity;
|
|
145
|
+
this.onCall = onCall;
|
|
146
|
+
this.reconnectMs = reconnectMs;
|
|
147
|
+
this.pingTimeoutMs = pingTimeoutMs;
|
|
148
|
+
}
|
|
149
|
+
ws = null;
|
|
150
|
+
retryTimer = null;
|
|
151
|
+
pingTimer = null;
|
|
152
|
+
closed = true;
|
|
153
|
+
ready = false;
|
|
154
|
+
connect() {
|
|
155
|
+
this.closed = false;
|
|
156
|
+
const query = this.hub.token ? `?token=${encodeURIComponent(this.hub.token)}` : "";
|
|
157
|
+
let ws;
|
|
158
|
+
try {
|
|
159
|
+
ws = new WebSocket(`ws://${this.hub.host}:${this.hub.port}${WS_PATH_CODEX}${query}`);
|
|
160
|
+
} catch (err) {
|
|
161
|
+
logger.debug(`Codex hub link failed: ${err.message}`);
|
|
162
|
+
this.scheduleReconnect();
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
this.ws = ws;
|
|
166
|
+
ws.on("open", () => {
|
|
167
|
+
if (this.ws !== ws) return;
|
|
168
|
+
this.armWatchdog(ws);
|
|
169
|
+
this.hello();
|
|
170
|
+
});
|
|
171
|
+
ws.on("ping", () => this.armWatchdog(ws));
|
|
172
|
+
ws.on("message", (data) => {
|
|
173
|
+
if (this.ws === ws) this.onMessage(String(data));
|
|
174
|
+
});
|
|
175
|
+
ws.on("close", () => {
|
|
176
|
+
if (this.ws !== ws) return;
|
|
177
|
+
this.clearWatchdog();
|
|
178
|
+
this.ws = null;
|
|
179
|
+
this.scheduleReconnect();
|
|
180
|
+
});
|
|
181
|
+
ws.on("error", (err) => logger.debug(`Codex hub link error: ${err.message}`));
|
|
182
|
+
}
|
|
183
|
+
setReady(ready) {
|
|
184
|
+
if (this.ready === ready) return;
|
|
185
|
+
this.ready = ready;
|
|
186
|
+
this.hello();
|
|
187
|
+
}
|
|
188
|
+
disconnect() {
|
|
189
|
+
this.closed = true;
|
|
190
|
+
this.clearWatchdog();
|
|
191
|
+
if (this.retryTimer) clearTimeout(this.retryTimer);
|
|
192
|
+
this.retryTimer = null;
|
|
193
|
+
const ws = this.ws;
|
|
194
|
+
this.ws = null;
|
|
195
|
+
ws?.close();
|
|
196
|
+
}
|
|
197
|
+
hello() {
|
|
198
|
+
this.send({ type: "adapter_hello", adapter: { ...this.identity, ready: this.ready } });
|
|
199
|
+
}
|
|
200
|
+
send(msg) {
|
|
201
|
+
if (this.ws?.readyState === WebSocket.OPEN) this.ws.send(JSON.stringify(msg));
|
|
202
|
+
}
|
|
203
|
+
onMessage(text) {
|
|
204
|
+
let msg;
|
|
205
|
+
try {
|
|
206
|
+
msg = JSON.parse(text);
|
|
207
|
+
} catch {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (!msg || typeof msg !== "object") return;
|
|
211
|
+
if (msg.type !== "adapter_call") return;
|
|
212
|
+
const { requestId, call } = msg;
|
|
213
|
+
this.onCall(call).then(
|
|
214
|
+
(data) => this.send({ type: "adapter_result", requestId, ok: true, data }),
|
|
215
|
+
(err) => this.send({ type: "adapter_result", requestId, ok: false, error: err.message })
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
clearWatchdog() {
|
|
219
|
+
if (this.pingTimer) clearTimeout(this.pingTimer);
|
|
220
|
+
this.pingTimer = null;
|
|
221
|
+
}
|
|
222
|
+
armWatchdog(ws) {
|
|
223
|
+
if (this.ws !== ws) return;
|
|
224
|
+
this.clearWatchdog();
|
|
225
|
+
this.pingTimer = setTimeout(() => {
|
|
226
|
+
if (this.ws === ws) ws.terminate();
|
|
227
|
+
}, this.pingTimeoutMs);
|
|
228
|
+
}
|
|
229
|
+
scheduleReconnect() {
|
|
230
|
+
if (this.closed || this.retryTimer) return;
|
|
231
|
+
this.retryTimer = setTimeout(() => {
|
|
232
|
+
this.retryTimer = null;
|
|
233
|
+
this.connect();
|
|
234
|
+
}, this.reconnectMs);
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
136
238
|
// src/codex/adapter.ts
|
|
137
239
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
138
240
|
|
|
139
241
|
// src/channel/hub-client.ts
|
|
140
|
-
import
|
|
242
|
+
import WebSocket2 from "ws";
|
|
141
243
|
var HubClient = class {
|
|
142
|
-
constructor(sessionId, sessionName, hubHost = DEFAULT_HUB_HOST, hubPort = DEFAULT_HUB_PORT,
|
|
244
|
+
constructor(sessionId, sessionName, hubHost = DEFAULT_HUB_HOST, hubPort = DEFAULT_HUB_PORT, token, getPeerName = () => void 0, getRegistration = () => ({})) {
|
|
143
245
|
this.sessionId = sessionId;
|
|
144
246
|
this.sessionName = sessionName;
|
|
145
247
|
this.hubHost = hubHost;
|
|
146
248
|
this.hubPort = hubPort;
|
|
147
|
-
this.token =
|
|
249
|
+
this.token = token;
|
|
148
250
|
this.getPeerName = getPeerName;
|
|
149
251
|
this.getRegistration = getRegistration;
|
|
150
252
|
}
|
|
@@ -160,7 +262,7 @@ var HubClient = class {
|
|
|
160
262
|
const url = `ws://${this.hubHost}:${this.hubPort}${WS_PATH_CHANNEL}${tokenQuery}`;
|
|
161
263
|
logger.debug(`Connecting to hub at ${url}`);
|
|
162
264
|
try {
|
|
163
|
-
const ws = new
|
|
265
|
+
const ws = new WebSocket2(url);
|
|
164
266
|
this.ws = ws;
|
|
165
267
|
ws.on("open", () => {
|
|
166
268
|
if (this.ws !== ws) return;
|
|
@@ -200,7 +302,7 @@ var HubClient = class {
|
|
|
200
302
|
}
|
|
201
303
|
}
|
|
202
304
|
send(msg) {
|
|
203
|
-
if (this.connected && this.ws?.readyState ===
|
|
305
|
+
if (this.connected && this.ws?.readyState === WebSocket2.OPEN) {
|
|
204
306
|
this.ws.send(JSON.stringify(msg));
|
|
205
307
|
} else {
|
|
206
308
|
if (this.queue.length < 100) {
|
|
@@ -225,7 +327,7 @@ var HubClient = class {
|
|
|
225
327
|
this.connected = false;
|
|
226
328
|
}
|
|
227
329
|
reregister() {
|
|
228
|
-
if (this.connected && this.ws?.readyState ===
|
|
330
|
+
if (this.connected && this.ws?.readyState === WebSocket2.OPEN) {
|
|
229
331
|
this.ws.send(JSON.stringify(this.registration()));
|
|
230
332
|
}
|
|
231
333
|
}
|
|
@@ -259,10 +361,11 @@ import { spawn } from "child_process";
|
|
|
259
361
|
import fs3 from "fs";
|
|
260
362
|
import path3 from "path";
|
|
261
363
|
import { Duplex } from "stream";
|
|
262
|
-
import
|
|
364
|
+
import WebSocket3 from "ws";
|
|
263
365
|
function resolveCommand(command, platform = process.platform, env = process.env) {
|
|
264
|
-
if (platform !== "win32"
|
|
265
|
-
|
|
366
|
+
if (platform !== "win32") return { file: command, shell: false };
|
|
367
|
+
if (/[\\/]/.test(command) || path3.extname(command)) return { file: command, shell: /\.(cmd|bat)$/i.test(command) };
|
|
368
|
+
const file = findCodex(command, platform, env);
|
|
266
369
|
return file ? { file, shell: file.endsWith(".cmd") } : { file: command, shell: false };
|
|
267
370
|
}
|
|
268
371
|
function findOnPath(command, platform = process.platform, env = process.env) {
|
|
@@ -275,12 +378,22 @@ function findOnPath(command, platform = process.platform, env = process.env) {
|
|
|
275
378
|
}
|
|
276
379
|
return void 0;
|
|
277
380
|
}
|
|
381
|
+
function findCodex(command, platform = process.platform, env = process.env) {
|
|
382
|
+
const onPath = findOnPath(command, platform, env);
|
|
383
|
+
if (onPath || platform !== "win32") return onPath;
|
|
384
|
+
const candidates = [
|
|
385
|
+
env.LOCALAPPDATA && path3.join(env.LOCALAPPDATA, "Programs", "OpenAI", "Codex", "bin", `${command}.exe`),
|
|
386
|
+
env.APPDATA && path3.join(env.APPDATA, "npm", `${command}.cmd`)
|
|
387
|
+
];
|
|
388
|
+
return candidates.find((file) => !!file && fs3.existsSync(file));
|
|
389
|
+
}
|
|
278
390
|
var defaultSpawn = (command, args) => {
|
|
279
391
|
const { file, shell } = resolveCommand(command);
|
|
280
392
|
if (shell) return spawn(`"${file}" ${args.join(" ")}`, { stdio: "pipe", windowsHide: true, shell: true });
|
|
281
393
|
return spawn(file, args, { stdio: "pipe", windowsHide: true });
|
|
282
394
|
};
|
|
283
|
-
|
|
395
|
+
var HANDSHAKE_TIMEOUT_MS = 1e4;
|
|
396
|
+
function connectProxy(command, spawnFn = defaultSpawn, timeoutMs = HANDSHAKE_TIMEOUT_MS) {
|
|
284
397
|
const child = spawnFn(command, ["app-server", "proxy"]);
|
|
285
398
|
child.stdin?.on("error", () => {
|
|
286
399
|
});
|
|
@@ -305,7 +418,7 @@ function connectProxy(command, spawnFn = defaultSpawn) {
|
|
|
305
418
|
}, ref() {
|
|
306
419
|
}, unref() {
|
|
307
420
|
} });
|
|
308
|
-
const ws = new
|
|
421
|
+
const ws = new WebSocket3("ws://localhost/", {
|
|
309
422
|
createConnection: (() => stream)
|
|
310
423
|
});
|
|
311
424
|
const close = () => {
|
|
@@ -314,9 +427,11 @@ function connectProxy(command, spawnFn = defaultSpawn) {
|
|
|
314
427
|
};
|
|
315
428
|
return new Promise((resolve, reject) => {
|
|
316
429
|
let settled = false;
|
|
430
|
+
const timer = setTimeout(() => fail(new Error(`codex daemon did not answer within ${timeoutMs / 1e3}s`)), timeoutMs);
|
|
317
431
|
const fail = (err) => {
|
|
318
432
|
if (settled) return;
|
|
319
433
|
settled = true;
|
|
434
|
+
clearTimeout(timer);
|
|
320
435
|
close();
|
|
321
436
|
reject(err);
|
|
322
437
|
};
|
|
@@ -326,6 +441,7 @@ function connectProxy(command, spawnFn = defaultSpawn) {
|
|
|
326
441
|
ws.once("open", () => {
|
|
327
442
|
if (settled) return;
|
|
328
443
|
settled = true;
|
|
444
|
+
clearTimeout(timer);
|
|
329
445
|
child.on("error", (err) => logger.warn(`codex proxy error: ${err.message}`));
|
|
330
446
|
ws.on("error", (err) => logger.warn(`codex daemon socket error: ${err.message}`));
|
|
331
447
|
resolve({ ws, close });
|
|
@@ -333,6 +449,57 @@ function connectProxy(command, spawnFn = defaultSpawn) {
|
|
|
333
449
|
});
|
|
334
450
|
}
|
|
335
451
|
|
|
452
|
+
// src/codex/inputs.ts
|
|
453
|
+
import fs4 from "fs/promises";
|
|
454
|
+
|
|
455
|
+
// src/codex/mapping.ts
|
|
456
|
+
var TITLE_MAX = 30;
|
|
457
|
+
var SOURCE_LABEL = { dashboard: "Dashboard", telegram: "Telegram", api: "API" };
|
|
458
|
+
var SOURCE_PREFIX = new RegExp(`^\\[claude-alarm(?: \xB7 (?:${Object.values(SOURCE_LABEL).join("|")}))?\\] ?`);
|
|
459
|
+
function codexSessionId(threadId) {
|
|
460
|
+
return `codex:${threadId}`;
|
|
461
|
+
}
|
|
462
|
+
function isTrackable(thread) {
|
|
463
|
+
return !thread.parentThreadId && !thread.ephemeral && thread.status.type !== "notLoaded";
|
|
464
|
+
}
|
|
465
|
+
function threadTitle(thread) {
|
|
466
|
+
const name = thread.name?.trim();
|
|
467
|
+
if (name) return name;
|
|
468
|
+
const preview = (thread.preview ?? "").replace(/\s+/g, " ").trim().replace(SOURCE_PREFIX, "").trim();
|
|
469
|
+
if (preview) return preview.length > TITLE_MAX ? `${preview.slice(0, TITLE_MAX)}\u2026` : preview;
|
|
470
|
+
return thread.cwd.replace(/^.*[/\\]/, "") || thread.id.slice(0, 8);
|
|
471
|
+
}
|
|
472
|
+
function hubStatus(status) {
|
|
473
|
+
if (status.type !== "active") return "idle";
|
|
474
|
+
return status.activeFlags.some((f) => f === "waitingOnApproval" || f === "waitingOnUserInput") ? "waiting_input" : "working";
|
|
475
|
+
}
|
|
476
|
+
function withSourcePrefix(content, source) {
|
|
477
|
+
return source ? `[claude-alarm \xB7 ${SOURCE_LABEL[source]}] ${content}` : `[claude-alarm] ${content}`;
|
|
478
|
+
}
|
|
479
|
+
function finalAnswer(messages) {
|
|
480
|
+
const finals = messages.filter((m) => m.phase === "final_answer").map((m) => m.text);
|
|
481
|
+
if (finals.length) return finals.join("\n\n");
|
|
482
|
+
return messages.at(-1)?.text ?? null;
|
|
483
|
+
}
|
|
484
|
+
function cleanFolder(input) {
|
|
485
|
+
const s = input.trim();
|
|
486
|
+
return s.length >= 2 && s.startsWith('"') && s.endsWith('"') ? s.slice(1, -1).trim() : s;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// src/codex/inputs.ts
|
|
490
|
+
var IMAGE_TYPES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
|
|
491
|
+
function textInput(content, source) {
|
|
492
|
+
return [{ type: "text", text: withSourcePrefix(content, source) }];
|
|
493
|
+
}
|
|
494
|
+
async function imageInput(imagePath, mimeType, caption, source) {
|
|
495
|
+
if (!IMAGE_TYPES.has(mimeType)) throw new Error(`unsupported image type ${mimeType}`);
|
|
496
|
+
const data = await fs4.readFile(imagePath);
|
|
497
|
+
return [
|
|
498
|
+
{ type: "text", text: withSourcePrefix(caption?.trim() || "(image)", source) },
|
|
499
|
+
{ type: "image", url: `data:${mimeType};base64,${data.toString("base64")}` }
|
|
500
|
+
];
|
|
501
|
+
}
|
|
502
|
+
|
|
336
503
|
// src/codex/rpc.ts
|
|
337
504
|
import { EventEmitter } from "events";
|
|
338
505
|
var RpcError = class extends Error {
|
|
@@ -358,14 +525,14 @@ var RpcClient = class extends EventEmitter {
|
|
|
358
525
|
}
|
|
359
526
|
nextId = 1;
|
|
360
527
|
pending = /* @__PURE__ */ new Map();
|
|
361
|
-
request(method, params) {
|
|
528
|
+
request(method, params, timeoutMs = this.timeoutMs) {
|
|
362
529
|
if (this.ws.readyState !== this.ws.OPEN) return Promise.reject(new Error("connection closed"));
|
|
363
530
|
const id = this.nextId++;
|
|
364
531
|
return new Promise((resolve, reject) => {
|
|
365
|
-
const timer = setTimeout(() => {
|
|
532
|
+
const timer = timeoutMs === null ? void 0 : setTimeout(() => {
|
|
366
533
|
this.pending.delete(id);
|
|
367
534
|
reject(new Error(`${method} timed out`));
|
|
368
|
-
},
|
|
535
|
+
}, timeoutMs);
|
|
369
536
|
this.pending.set(id, { resolve, reject, timer });
|
|
370
537
|
this.ws.send(JSON.stringify(params === void 0 ? { id, method } : { id, method, params }));
|
|
371
538
|
});
|
|
@@ -476,41 +643,14 @@ ${f.diff}` : f.path).join("\n\n") : "The file list is not available here. Check
|
|
|
476
643
|
}
|
|
477
644
|
}
|
|
478
645
|
|
|
479
|
-
// src/codex/mapping.ts
|
|
480
|
-
var TITLE_MAX = 30;
|
|
481
|
-
var SOURCE_LABEL = { dashboard: "Dashboard", telegram: "Telegram", api: "API" };
|
|
482
|
-
function codexSessionId(threadId) {
|
|
483
|
-
return `codex:${threadId}`;
|
|
484
|
-
}
|
|
485
|
-
function isTrackable(thread) {
|
|
486
|
-
return !thread.parentThreadId && !thread.ephemeral && thread.status.type !== "notLoaded";
|
|
487
|
-
}
|
|
488
|
-
function threadTitle(thread) {
|
|
489
|
-
const name = thread.name?.trim();
|
|
490
|
-
if (name) return name;
|
|
491
|
-
const preview = (thread.preview ?? "").replace(/\s+/g, " ").trim();
|
|
492
|
-
if (preview) return preview.length > TITLE_MAX ? `${preview.slice(0, TITLE_MAX)}\u2026` : preview;
|
|
493
|
-
return thread.cwd.replace(/^.*[/\\]/, "") || thread.id.slice(0, 8);
|
|
494
|
-
}
|
|
495
|
-
function hubStatus(status) {
|
|
496
|
-
if (status.type !== "active") return "idle";
|
|
497
|
-
return status.activeFlags.some((f) => f === "waitingOnApproval" || f === "waitingOnUserInput") ? "waiting_input" : "working";
|
|
498
|
-
}
|
|
499
|
-
function withSourcePrefix(content, source) {
|
|
500
|
-
return source ? `[claude-alarm \xB7 ${SOURCE_LABEL[source]}] ${content}` : `[claude-alarm] ${content}`;
|
|
501
|
-
}
|
|
502
|
-
function finalAnswer(messages) {
|
|
503
|
-
const finals = messages.filter((m) => m.phase === "final_answer").map((m) => m.text);
|
|
504
|
-
if (finals.length) return finals.join("\n\n");
|
|
505
|
-
return messages.at(-1)?.text ?? null;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
646
|
// src/codex/adapter.ts
|
|
509
647
|
var USER_REQUESTS = /* @__PURE__ */ new Set(["item/tool/requestUserInput", "item/permissions/requestApproval", "mcpServer/elicitation/request"]);
|
|
510
648
|
var CodexAdapter = class {
|
|
511
649
|
constructor(opts) {
|
|
512
650
|
this.opts = opts;
|
|
513
651
|
this.delay = opts.reconnectMinMs ?? 2e3;
|
|
652
|
+
this.hostName = opts.hostName ?? os2.hostname();
|
|
653
|
+
this.link = new CodexHubLink(opts.hub, { id: randomUUID2(), host: this.hostName }, (call) => this.onCall(call), opts.linkReconnectMs);
|
|
514
654
|
}
|
|
515
655
|
conn;
|
|
516
656
|
rpc;
|
|
@@ -519,12 +659,20 @@ var CodexAdapter = class {
|
|
|
519
659
|
stopped = false;
|
|
520
660
|
retryTimer;
|
|
521
661
|
delay;
|
|
662
|
+
notFoundNotice = "unsent";
|
|
663
|
+
firstConnectReported = false;
|
|
664
|
+
pins = /* @__PURE__ */ new Set();
|
|
665
|
+
ready = false;
|
|
666
|
+
hostName;
|
|
667
|
+
link;
|
|
522
668
|
start() {
|
|
523
669
|
this.stopped = false;
|
|
670
|
+
this.link.connect();
|
|
524
671
|
void this.connect();
|
|
525
672
|
}
|
|
526
673
|
stop() {
|
|
527
674
|
this.stopped = true;
|
|
675
|
+
this.link.disconnect();
|
|
528
676
|
if (this.retryTimer) clearTimeout(this.retryTimer);
|
|
529
677
|
this.retryTimer = void 0;
|
|
530
678
|
for (const id of [...this.threads.keys()]) this.drop(id);
|
|
@@ -538,7 +686,7 @@ var CodexAdapter = class {
|
|
|
538
686
|
conn.close();
|
|
539
687
|
return;
|
|
540
688
|
}
|
|
541
|
-
const live = new RpcClient(conn.ws);
|
|
689
|
+
const live = new RpcClient(conn.ws, this.opts.rpcTimeoutMs);
|
|
542
690
|
rpc = live;
|
|
543
691
|
live.on("notification", (method, params) => this.onNotification(method, params));
|
|
544
692
|
live.on("request", (id, method, params) => this.onServerRequest(id, method, params));
|
|
@@ -552,10 +700,19 @@ var CodexAdapter = class {
|
|
|
552
700
|
});
|
|
553
701
|
live.notify("initialized");
|
|
554
702
|
logger.info(`Connected to Codex daemon (${init.userAgent ?? "unknown version"})`);
|
|
703
|
+
this.reportFirstConnect({ connected: true, ...init.userAgent ? { userAgent: init.userAgent } : {} });
|
|
555
704
|
await this.discover();
|
|
705
|
+
if (this.rpc === live) this.setReady(true);
|
|
556
706
|
this.delay = this.opts.reconnectMinMs ?? 2e3;
|
|
557
707
|
} catch (err) {
|
|
708
|
+
this.setReady(false);
|
|
558
709
|
logger.warn(`Codex daemon connection failed: ${err.message}`);
|
|
710
|
+
if (err.code === "ENOENT") this.noticeNotFound();
|
|
711
|
+
this.reportFirstConnect({
|
|
712
|
+
connected: false,
|
|
713
|
+
error: err.message,
|
|
714
|
+
notFound: err.code === "ENOENT"
|
|
715
|
+
});
|
|
559
716
|
if (rpc && this.rpc === rpc) {
|
|
560
717
|
this.rpc = void 0;
|
|
561
718
|
this.conn?.close();
|
|
@@ -566,6 +723,7 @@ var CodexAdapter = class {
|
|
|
566
723
|
}
|
|
567
724
|
}
|
|
568
725
|
onDaemonLost() {
|
|
726
|
+
this.setReady(false);
|
|
569
727
|
this.rpc = void 0;
|
|
570
728
|
this.conn?.close();
|
|
571
729
|
this.conn = void 0;
|
|
@@ -581,18 +739,86 @@ var CodexAdapter = class {
|
|
|
581
739
|
}, this.delay);
|
|
582
740
|
this.delay = Math.min(this.delay * 2, this.opts.reconnectMaxMs ?? 6e4);
|
|
583
741
|
}
|
|
742
|
+
reportFirstConnect(outcome) {
|
|
743
|
+
if (this.firstConnectReported || this.stopped) return;
|
|
744
|
+
this.firstConnectReported = true;
|
|
745
|
+
try {
|
|
746
|
+
this.opts.onFirstConnect?.(outcome);
|
|
747
|
+
} catch (err) {
|
|
748
|
+
logger.debug(`First-connect observer failed: ${err.message}`);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
noticeNotFound() {
|
|
752
|
+
if (this.notFoundNotice !== "unsent") return;
|
|
753
|
+
this.notFoundNotice = "sending";
|
|
754
|
+
const { host, port, token } = this.opts.hub;
|
|
755
|
+
fetch(`http://${host}:${port}/api/notify`, {
|
|
756
|
+
method: "POST",
|
|
757
|
+
headers: { "Content-Type": "application/json", ...token ? { Authorization: `Bearer ${token}` } : {} },
|
|
758
|
+
body: JSON.stringify({
|
|
759
|
+
title: "Codex not found",
|
|
760
|
+
message: `The Codex adapter cannot find "${this.opts.command}". Open a new terminal and restart the hub, or set "codex.command" in ~/.claude-alarm/config.json.`,
|
|
761
|
+
level: "warning"
|
|
762
|
+
}),
|
|
763
|
+
signal: AbortSignal.timeout(this.opts.noticeTimeoutMs ?? 5e3)
|
|
764
|
+
}).then((res) => {
|
|
765
|
+
this.notFoundNotice = res.ok ? "sent" : "unsent";
|
|
766
|
+
if (!res.ok) logger.debug(`Codex not-found notice was refused (${res.status})`);
|
|
767
|
+
}).catch((err) => {
|
|
768
|
+
this.notFoundNotice = err.name === "TimeoutError" ? "sent" : "unsent";
|
|
769
|
+
logger.debug(`Codex not-found notice failed: ${err.message}`);
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
setReady(ready) {
|
|
773
|
+
this.ready = ready;
|
|
774
|
+
this.link.setReady(ready);
|
|
775
|
+
}
|
|
776
|
+
requireDaemon() {
|
|
777
|
+
if (!this.ready || !this.rpc) throw new Error(`Codex is not connected on ${this.hostName}.`);
|
|
778
|
+
return this.rpc;
|
|
779
|
+
}
|
|
780
|
+
async onCall(call) {
|
|
781
|
+
if (call.kind === "folders") return { folders: await this.folders() };
|
|
782
|
+
return { sessionId: await this.create(call.cwd) };
|
|
783
|
+
}
|
|
784
|
+
async folders() {
|
|
785
|
+
const page = await this.requireDaemon().request("thread/list", { limit: 50, sortKey: "updated_at" });
|
|
786
|
+
return [...new Set(page.data.map((t) => t.cwd).filter(Boolean))].slice(0, 10);
|
|
787
|
+
}
|
|
788
|
+
async create(input) {
|
|
789
|
+
const rpc = this.requireDaemon();
|
|
790
|
+
const cwd = cleanFolder(input);
|
|
791
|
+
const isDir = path4.isAbsolute(cwd) && await fs5.promises.stat(cwd).then((s) => s.isDirectory(), () => false);
|
|
792
|
+
if (!isDir) throw new Error(`Folder not found on ${this.hostName}: ${cwd}`);
|
|
793
|
+
let thread;
|
|
794
|
+
try {
|
|
795
|
+
({ thread } = await rpc.request("thread/start", { cwd, sandbox: "danger-full-access", approvalPolicy: "never" }, null));
|
|
796
|
+
} catch (err) {
|
|
797
|
+
throw new Error(`Codex could not start the conversation: ${err.message}`);
|
|
798
|
+
}
|
|
799
|
+
this.pins.add(thread.id);
|
|
800
|
+
this.upsert(thread);
|
|
801
|
+
const t = this.threads.get(thread.id);
|
|
802
|
+
if (!t) {
|
|
803
|
+
this.pins.delete(thread.id);
|
|
804
|
+
throw new Error("Codex started a conversation claude-alarm cannot follow.");
|
|
805
|
+
}
|
|
806
|
+
t.subscribed = true;
|
|
807
|
+
return codexSessionId(thread.id);
|
|
808
|
+
}
|
|
584
809
|
async discover() {
|
|
810
|
+
const rpc = this.rpc;
|
|
811
|
+
const pinned = [...this.pins];
|
|
585
812
|
const ids = [];
|
|
586
813
|
let cursor;
|
|
587
814
|
do {
|
|
588
|
-
const page = await
|
|
589
|
-
"thread/loaded/list",
|
|
590
|
-
cursor ? { cursor } : {}
|
|
591
|
-
);
|
|
815
|
+
const page = await rpc.request("thread/loaded/list", cursor ? { cursor } : {});
|
|
592
816
|
ids.push(...page.data);
|
|
593
817
|
cursor = page.nextCursor;
|
|
594
818
|
} while (cursor);
|
|
595
819
|
for (const id of ids) await this.refresh(id);
|
|
820
|
+
if (this.rpc !== rpc) return;
|
|
821
|
+
for (const id of pinned) if (!ids.includes(id)) this.pins.delete(id);
|
|
596
822
|
}
|
|
597
823
|
async refresh(threadId) {
|
|
598
824
|
try {
|
|
@@ -607,18 +833,18 @@ var CodexAdapter = class {
|
|
|
607
833
|
this.drop(thread.id);
|
|
608
834
|
return;
|
|
609
835
|
}
|
|
610
|
-
const
|
|
611
|
-
if (
|
|
612
|
-
|
|
613
|
-
|
|
836
|
+
const existing = this.threads.get(thread.id);
|
|
837
|
+
if (existing) {
|
|
838
|
+
existing.thread = thread;
|
|
839
|
+
existing.hub.reregister();
|
|
614
840
|
} else {
|
|
615
|
-
const { host
|
|
841
|
+
const { host, port, token } = this.opts.hub;
|
|
616
842
|
const hub = new HubClient(
|
|
617
843
|
codexSessionId(thread.id),
|
|
618
844
|
threadTitle(thread),
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
845
|
+
host,
|
|
846
|
+
port,
|
|
847
|
+
token,
|
|
622
848
|
() => void 0,
|
|
623
849
|
() => this.registration(thread.id)
|
|
624
850
|
);
|
|
@@ -629,6 +855,7 @@ var CodexAdapter = class {
|
|
|
629
855
|
wantSubscribed: false,
|
|
630
856
|
sync: Promise.resolve(),
|
|
631
857
|
pendingTurn: false,
|
|
858
|
+
sending: Promise.resolve(),
|
|
632
859
|
turns: /* @__PURE__ */ new Map(),
|
|
633
860
|
unrelayed: false,
|
|
634
861
|
files: /* @__PURE__ */ new Map()
|
|
@@ -642,12 +869,12 @@ var CodexAdapter = class {
|
|
|
642
869
|
const t = this.threads.get(threadId);
|
|
643
870
|
if (!t) return {};
|
|
644
871
|
const title = threadTitle(t.thread);
|
|
645
|
-
return { name: title, title, cwd: t.thread.cwd, agentKind: "codex", status: hubStatus(t.thread.status) };
|
|
872
|
+
return { name: title, title, cwd: t.thread.cwd, agentKind: "codex", status: hubStatus(t.thread.status), closable: this.pins.has(threadId) };
|
|
646
873
|
}
|
|
647
874
|
want(threadId, subscribed) {
|
|
648
875
|
const t = this.threads.get(threadId);
|
|
649
876
|
if (!t) return Promise.resolve();
|
|
650
|
-
t.wantSubscribed = subscribed;
|
|
877
|
+
t.wantSubscribed = subscribed || this.pins.has(threadId);
|
|
651
878
|
t.sync = t.sync.then(() => this.syncSubscription(t));
|
|
652
879
|
return t.sync;
|
|
653
880
|
}
|
|
@@ -665,6 +892,7 @@ var CodexAdapter = class {
|
|
|
665
892
|
}
|
|
666
893
|
} catch (err) {
|
|
667
894
|
logger.debug(`subscription sync for ${threadId} deferred: ${err.message}`);
|
|
895
|
+
if (t.wantSubscribed && !t.subscribed && t.thread.status.type === "active") t.unrelayed = true;
|
|
668
896
|
}
|
|
669
897
|
}
|
|
670
898
|
releaseLater(t) {
|
|
@@ -687,6 +915,25 @@ var CodexAdapter = class {
|
|
|
687
915
|
t.releaseTimer = void 0;
|
|
688
916
|
t.hub.disconnect();
|
|
689
917
|
}
|
|
918
|
+
busy(t) {
|
|
919
|
+
return t.thread.status.type === "active" || t.pendingTurn;
|
|
920
|
+
}
|
|
921
|
+
async close(threadId) {
|
|
922
|
+
const t = this.threads.get(threadId);
|
|
923
|
+
if (!t || !this.pins.delete(threadId)) return;
|
|
924
|
+
t.hub.reregister();
|
|
925
|
+
if (this.busy(t)) return;
|
|
926
|
+
await this.want(threadId, false);
|
|
927
|
+
if (this.threads.get(threadId) !== t || this.busy(t) || t.wantSubscribed) return;
|
|
928
|
+
if (!t.subscribed) {
|
|
929
|
+
this.drop(threadId);
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
this.pins.add(threadId);
|
|
933
|
+
t.hub.reregister();
|
|
934
|
+
void this.want(threadId, true);
|
|
935
|
+
this.notify(threadId, "Not closed", "Codex did not release the conversation. Try closing it again.", "warning");
|
|
936
|
+
}
|
|
690
937
|
onNotification(method, params) {
|
|
691
938
|
switch (method) {
|
|
692
939
|
case "thread/started":
|
|
@@ -706,6 +953,7 @@ var CodexAdapter = class {
|
|
|
706
953
|
case "thread/closed":
|
|
707
954
|
case "thread/archived":
|
|
708
955
|
case "thread/deleted":
|
|
956
|
+
this.pins.delete(params.threadId);
|
|
709
957
|
this.drop(params.threadId);
|
|
710
958
|
break;
|
|
711
959
|
case "item/started":
|
|
@@ -730,6 +978,7 @@ var CodexAdapter = class {
|
|
|
730
978
|
return;
|
|
731
979
|
}
|
|
732
980
|
if (status.type === "notLoaded") {
|
|
981
|
+
this.pins.delete(threadId);
|
|
733
982
|
this.drop(threadId);
|
|
734
983
|
return;
|
|
735
984
|
}
|
|
@@ -763,6 +1012,7 @@ var CodexAdapter = class {
|
|
|
763
1012
|
const collected = t.turns.get(turn.id) ?? [];
|
|
764
1013
|
t.turns.delete(turn.id);
|
|
765
1014
|
t.files.clear();
|
|
1015
|
+
if (!t.thread.name?.trim() && !t.thread.preview?.trim()) void this.refresh(threadId);
|
|
766
1016
|
if (t.thread.status.type !== "active") void this.want(threadId, false);
|
|
767
1017
|
if (turn.status === "failed" || turn.error) {
|
|
768
1018
|
this.notify(threadId, "Codex task failed", turn.error?.message ?? "The task ended with an error.", "error");
|
|
@@ -778,33 +1028,75 @@ var CodexAdapter = class {
|
|
|
778
1028
|
}
|
|
779
1029
|
onHubMessage(threadId, msg) {
|
|
780
1030
|
if (msg.type === "message_to_session") {
|
|
781
|
-
|
|
1031
|
+
this.enqueue(threadId, async () => textInput(msg.content, msg.source));
|
|
782
1032
|
} else if (msg.type === "image_to_session") {
|
|
783
|
-
this.
|
|
1033
|
+
this.enqueue(threadId, () => imageInput(msg.imagePath, msg.mimeType, msg.content, msg.source));
|
|
784
1034
|
} else if (msg.type === "permission_response") {
|
|
785
1035
|
this.answer(threadId, msg.requestId, msg.choiceId);
|
|
1036
|
+
} else if (msg.type === "codex_close") {
|
|
1037
|
+
void this.close(threadId);
|
|
786
1038
|
}
|
|
787
1039
|
}
|
|
788
|
-
|
|
1040
|
+
// One message at a time per conversation, so a message right behind another sees the turn the first one started.
|
|
1041
|
+
enqueue(threadId, build) {
|
|
789
1042
|
const t = this.threads.get(threadId);
|
|
790
1043
|
if (!t) return;
|
|
791
|
-
|
|
792
|
-
|
|
1044
|
+
t.sending = t.sending.then(() => this.deliver(t, build)).catch((err) => logger.warn(`Codex delivery for ${threadId} failed: ${err.message}`));
|
|
1045
|
+
}
|
|
1046
|
+
async deliver(t, build) {
|
|
1047
|
+
const threadId = t.thread.id;
|
|
1048
|
+
if (this.threads.get(threadId) !== t) return;
|
|
1049
|
+
if (hubStatus(t.thread.status) === "waiting_input") {
|
|
1050
|
+
this.notify(threadId, "Not delivered", "Codex is waiting for an approval or input. Answer it first, then send the message again.", "warning");
|
|
793
1051
|
return;
|
|
794
1052
|
}
|
|
1053
|
+
let input;
|
|
1054
|
+
try {
|
|
1055
|
+
input = await build();
|
|
1056
|
+
} catch (err) {
|
|
1057
|
+
logger.debug(`Codex input for ${threadId} could not be built: ${err.message}`);
|
|
1058
|
+
this.notify(threadId, "Not delivered", "The image could not be read here, so it was not delivered. Codex may be running on another PC.", "warning");
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
if (this.threads.get(threadId) !== t) return;
|
|
795
1062
|
t.pendingTurn = true;
|
|
1063
|
+
const wasUnrelayed = t.unrelayed;
|
|
796
1064
|
try {
|
|
797
|
-
|
|
1065
|
+
const rpc = this.rpc;
|
|
1066
|
+
if (!rpc) throw new Error("not connected to the Codex daemon");
|
|
798
1067
|
await this.want(threadId, true);
|
|
799
|
-
|
|
800
|
-
await this.rpc
|
|
1068
|
+
const unsubscribed = !t.subscribed;
|
|
1069
|
+
const running = await this.runningTurn(rpc, threadId);
|
|
1070
|
+
if (this.threads.get(threadId) !== t) return;
|
|
1071
|
+
if (unsubscribed) t.unrelayed = true;
|
|
1072
|
+
if (running) {
|
|
1073
|
+
await rpc.request("turn/steer", { threadId, expectedTurnId: running, input });
|
|
1074
|
+
this.notify(threadId, "Queued", "Queued: Codex will read it after its current step.", "info");
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
await rpc.request("turn/start", { threadId, input });
|
|
801
1078
|
} catch (err) {
|
|
802
1079
|
t.pendingTurn = false;
|
|
803
|
-
t.unrelayed =
|
|
1080
|
+
t.unrelayed = wasUnrelayed;
|
|
804
1081
|
this.releaseLater(t);
|
|
805
1082
|
this.notify(threadId, "Not delivered", `Codex rejected the message: ${err.message}`, "warning");
|
|
806
1083
|
}
|
|
807
1084
|
}
|
|
1085
|
+
// The list is unavailable before a new conversation's first turn; starting a turn is safe then, since turn/start steers a running turn.
|
|
1086
|
+
async runningTurn(rpc, threadId) {
|
|
1087
|
+
try {
|
|
1088
|
+
const page = await rpc.request("thread/turns/list", {
|
|
1089
|
+
threadId,
|
|
1090
|
+
limit: 1,
|
|
1091
|
+
sortDirection: "desc"
|
|
1092
|
+
});
|
|
1093
|
+
const turn = page.data?.[0];
|
|
1094
|
+
return turn?.status === "inProgress" ? turn.id : void 0;
|
|
1095
|
+
} catch (err) {
|
|
1096
|
+
logger.debug(`thread/turns/list ${threadId} failed: ${err.message}`);
|
|
1097
|
+
return void 0;
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
808
1100
|
onServerRequest(rpcId, method, params) {
|
|
809
1101
|
const t = params?.threadId ? this.threads.get(params.threadId) : void 0;
|
|
810
1102
|
let view = null;
|
|
@@ -819,7 +1111,7 @@ var CodexAdapter = class {
|
|
|
819
1111
|
}
|
|
820
1112
|
if (!t || !view) {
|
|
821
1113
|
if (t && (broken || USER_REQUESTS.has(method))) {
|
|
822
|
-
this.
|
|
1114
|
+
this.waitingInCodex(t.thread.id);
|
|
823
1115
|
} else {
|
|
824
1116
|
logger.debug(`Ignoring Codex server request ${method}`);
|
|
825
1117
|
}
|
|
@@ -829,17 +1121,26 @@ var CodexAdapter = class {
|
|
|
829
1121
|
if (a.threadId === t.thread.id && a.rpcId === rpcId) return;
|
|
830
1122
|
}
|
|
831
1123
|
const requestId = randomUUID2();
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
1124
|
+
try {
|
|
1125
|
+
this.approvals.set(requestId, { threadId: t.thread.id, rpcId, turnId: params.turnId, choices: view.choices, answered: false });
|
|
1126
|
+
t.hub.send({
|
|
1127
|
+
type: "permission_request",
|
|
1128
|
+
sessionId: codexSessionId(t.thread.id),
|
|
1129
|
+
requestId,
|
|
1130
|
+
toolName: view.toolName,
|
|
1131
|
+
description: view.description,
|
|
1132
|
+
inputPreview: view.inputPreview,
|
|
1133
|
+
timestamp: Date.now(),
|
|
1134
|
+
choices: view.choices.map((c, i) => ({ id: String(i), label: c.label }))
|
|
1135
|
+
});
|
|
1136
|
+
} catch (err) {
|
|
1137
|
+
this.approvals.delete(requestId);
|
|
1138
|
+
logger.warn(`Codex ${method} could not be relayed: ${err.message}`);
|
|
1139
|
+
this.waitingInCodex(t.thread.id);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
waitingInCodex(threadId) {
|
|
1143
|
+
this.notify(threadId, "Codex is waiting", "Codex asked for input that claude-alarm cannot relay. Handle it in Codex.", "warning");
|
|
843
1144
|
}
|
|
844
1145
|
answer(threadId, requestId, choiceId) {
|
|
845
1146
|
const a = this.approvals.get(requestId);
|
|
@@ -865,49 +1166,302 @@ var CodexAdapter = class {
|
|
|
865
1166
|
}
|
|
866
1167
|
};
|
|
867
1168
|
|
|
868
|
-
// src/
|
|
869
|
-
|
|
1169
|
+
// src/shared/hub-url.ts
|
|
1170
|
+
import net from "net";
|
|
1171
|
+
function hubUrlHost(host) {
|
|
1172
|
+
if (host === "" || host === "0.0.0.0") return "127.0.0.1";
|
|
1173
|
+
if (host === "::") return "[::1]";
|
|
1174
|
+
return net.isIPv6(host) ? `[${host}]` : host;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
// src/codex/hub-target.ts
|
|
1178
|
+
function resolveAdapterHub(config2, env = process.env) {
|
|
1179
|
+
const envHost = env.CLAUDE_ALARM_HUB_HOST;
|
|
1180
|
+
const envPort = env.CLAUDE_ALARM_HUB_PORT;
|
|
1181
|
+
const envToken = env.CLAUDE_ALARM_HUB_TOKEN;
|
|
1182
|
+
return {
|
|
1183
|
+
host: hubUrlHost(envHost ?? config2.hub.host),
|
|
1184
|
+
port: envPort ? parseInt(envPort, 10) : config2.hub.port,
|
|
1185
|
+
token: envToken ?? config2.hub.token,
|
|
1186
|
+
fromEnv: { host: envHost !== void 0, port: !!envPort, token: envToken !== void 0 }
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
// src/codex/instance-lock.ts
|
|
1191
|
+
import crypto from "crypto";
|
|
1192
|
+
import fs6 from "fs";
|
|
1193
|
+
import net2 from "net";
|
|
1194
|
+
import path5 from "path";
|
|
1195
|
+
var CONTROL_PROTOCOL = 1;
|
|
1196
|
+
var STALE_GUARD_MS = 1e4;
|
|
1197
|
+
var STOP_PROOF_LABEL = "claude-alarm codex stop";
|
|
1198
|
+
var MAX_LINE_BYTES = 4096;
|
|
1199
|
+
var REQUEST_IDLE_MS = 2e3;
|
|
1200
|
+
var MAX_SOCKET_PATH_BYTES = 103;
|
|
1201
|
+
function stopProof(token) {
|
|
1202
|
+
return crypto.createHmac("sha256", token).update(STOP_PROOF_LABEL).digest("hex");
|
|
1203
|
+
}
|
|
1204
|
+
function controlEndpoint(configDir, platform = process.platform) {
|
|
1205
|
+
if (platform !== "win32") return path5.posix.join(configDir, "codex.sock");
|
|
1206
|
+
let dir;
|
|
870
1207
|
try {
|
|
871
|
-
|
|
872
|
-
return true;
|
|
1208
|
+
dir = fs6.realpathSync.native(configDir);
|
|
873
1209
|
} catch {
|
|
874
|
-
|
|
1210
|
+
dir = path5.win32.resolve(configDir);
|
|
1211
|
+
}
|
|
1212
|
+
const hash = crypto.createHash("sha256").update(dir.toLowerCase()).digest("hex").slice(0, 16);
|
|
1213
|
+
return `\\\\.\\pipe\\claude-alarm-codex-${hash}`;
|
|
1214
|
+
}
|
|
1215
|
+
function ask(endpoint2, request, timeoutMs) {
|
|
1216
|
+
return new Promise((resolve) => {
|
|
1217
|
+
let buffer = "";
|
|
1218
|
+
let settled = false;
|
|
1219
|
+
const socket = net2.connect(endpoint2);
|
|
1220
|
+
const finish = (answer) => {
|
|
1221
|
+
if (settled) return;
|
|
1222
|
+
settled = true;
|
|
1223
|
+
clearTimeout(timer);
|
|
1224
|
+
socket.destroy();
|
|
1225
|
+
resolve(answer);
|
|
1226
|
+
};
|
|
1227
|
+
const timer = setTimeout(() => finish({ state: "unknown", error: `no answer within ${timeoutMs}ms` }), timeoutMs);
|
|
1228
|
+
socket.setEncoding("utf8");
|
|
1229
|
+
socket.on("connect", () => socket.write(`${JSON.stringify(request)}
|
|
1230
|
+
`));
|
|
1231
|
+
socket.on("data", (chunk) => {
|
|
1232
|
+
buffer += chunk;
|
|
1233
|
+
const end = buffer.indexOf("\n");
|
|
1234
|
+
if (end < 0) {
|
|
1235
|
+
if (Buffer.byteLength(buffer) > MAX_LINE_BYTES) finish({ state: "unknown", error: "reply too long" });
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
try {
|
|
1239
|
+
finish({ reply: JSON.parse(buffer.slice(0, end)) });
|
|
1240
|
+
} catch {
|
|
1241
|
+
finish({ state: "unknown", error: "unreadable reply" });
|
|
1242
|
+
}
|
|
1243
|
+
});
|
|
1244
|
+
socket.on("end", () => finish({ state: "unknown", error: "closed without a reply" }));
|
|
1245
|
+
socket.on("error", (err) => {
|
|
1246
|
+
if (err.code === "ENOENT" || err.code === "ECONNREFUSED") finish({ state: "absent" });
|
|
1247
|
+
else finish({ state: "unknown", error: err.code ?? err.message });
|
|
1248
|
+
});
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
async function queryOwner(endpoint2, timeoutMs = 1e3) {
|
|
1252
|
+
const answer = await ask(endpoint2, { type: "status" }, timeoutMs);
|
|
1253
|
+
if (!("reply" in answer)) return answer;
|
|
1254
|
+
const reply = answer.reply;
|
|
1255
|
+
if (reply?.type === "status" && reply.protocol === CONTROL_PROTOCOL && Number.isInteger(reply.pid)) {
|
|
1256
|
+
return { state: "running", pid: reply.pid };
|
|
875
1257
|
}
|
|
1258
|
+
return { state: "unknown", error: "unexpected reply" };
|
|
876
1259
|
}
|
|
1260
|
+
var line = (message) => `${JSON.stringify(message)}
|
|
1261
|
+
`;
|
|
1262
|
+
function controlServer(owner) {
|
|
1263
|
+
const expectedProof = Buffer.from(stopProof(owner.token));
|
|
1264
|
+
return net2.createServer((socket) => {
|
|
1265
|
+
let buffer = "";
|
|
1266
|
+
socket.setEncoding("utf8");
|
|
1267
|
+
socket.on("error", () => {
|
|
1268
|
+
});
|
|
1269
|
+
const idle = setTimeout(() => socket.destroy(), REQUEST_IDLE_MS);
|
|
1270
|
+
socket.on("close", () => clearTimeout(idle));
|
|
1271
|
+
const onData = (chunk) => {
|
|
1272
|
+
buffer += chunk;
|
|
1273
|
+
const end = buffer.indexOf("\n");
|
|
1274
|
+
if (end < 0) {
|
|
1275
|
+
if (Buffer.byteLength(buffer) > MAX_LINE_BYTES) socket.destroy();
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
socket.off("data", onData);
|
|
1279
|
+
clearTimeout(idle);
|
|
1280
|
+
let request = null;
|
|
1281
|
+
try {
|
|
1282
|
+
request = JSON.parse(buffer.slice(0, end));
|
|
1283
|
+
} catch {
|
|
1284
|
+
}
|
|
1285
|
+
if (request?.type === "status") {
|
|
1286
|
+
socket.end(line({ type: "status", protocol: CONTROL_PROTOCOL, pid: owner.pid }));
|
|
1287
|
+
} else if (request?.type === "stop") {
|
|
1288
|
+
const proof = typeof request.proof === "string" ? Buffer.from(request.proof) : void 0;
|
|
1289
|
+
if (owner.token !== "" && proof && proof.length === expectedProof.length && crypto.timingSafeEqual(proof, expectedProof)) {
|
|
1290
|
+
socket.end(line({ type: "stopping", pid: owner.pid }), () => owner.onStop());
|
|
1291
|
+
} else {
|
|
1292
|
+
socket.end(line({ type: "error", error: "unauthorized" }));
|
|
1293
|
+
}
|
|
1294
|
+
} else {
|
|
1295
|
+
socket.end(line({ type: "error", error: "unknown request" }));
|
|
1296
|
+
}
|
|
1297
|
+
};
|
|
1298
|
+
socket.on("data", onData);
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
function listenControl(endpoint2, owner) {
|
|
1302
|
+
const server = controlServer(owner);
|
|
1303
|
+
return new Promise((resolve) => {
|
|
1304
|
+
const onError = (err) => {
|
|
1305
|
+
resolve(err.code === "EADDRINUSE" ? { kind: "busy" } : { kind: "unknown", error: err.code ?? err.message });
|
|
1306
|
+
};
|
|
1307
|
+
server.once("error", onError);
|
|
1308
|
+
server.listen(endpoint2, () => {
|
|
1309
|
+
server.off("error", onError);
|
|
1310
|
+
server.on("error", () => {
|
|
1311
|
+
});
|
|
1312
|
+
resolve({ kind: "owner", close: () => new Promise((done) => server.close(() => done())) });
|
|
1313
|
+
});
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
function settle(attempt, busy) {
|
|
1317
|
+
return attempt.kind === "busy" ? { kind: "unknown", error: busy } : attempt;
|
|
1318
|
+
}
|
|
1319
|
+
async function acquireLock(endpoint2, owner, deps = {}) {
|
|
1320
|
+
const platform = deps.platform ?? process.platform;
|
|
1321
|
+
const listen = deps.listen ?? listenControl;
|
|
1322
|
+
const query = deps.queryOwner ?? ((e) => queryOwner(e));
|
|
1323
|
+
if (platform !== "win32" && Buffer.byteLength(endpoint2) > MAX_SOCKET_PATH_BYTES) {
|
|
1324
|
+
return { kind: "unknown", error: `control socket path is too long: ${endpoint2}` };
|
|
1325
|
+
}
|
|
1326
|
+
const first = await listen(endpoint2, owner);
|
|
1327
|
+
if (first.kind !== "busy") return first;
|
|
1328
|
+
const state = await query(endpoint2);
|
|
1329
|
+
if (state.state === "running") return { kind: "held", pid: state.pid };
|
|
1330
|
+
if (state.state === "unknown") return { kind: "unknown", error: state.error };
|
|
1331
|
+
if (platform === "win32") return settle(await listen(endpoint2, owner), "the control pipe is in use but does not answer");
|
|
1332
|
+
return recoverSocket(endpoint2, owner, listen, query, deps.fs ?? fs6, deps.now ?? Date.now);
|
|
1333
|
+
}
|
|
1334
|
+
async function recoverSocket(endpoint2, owner, listen, query, f, now) {
|
|
1335
|
+
const guard = `${endpoint2}.lock`;
|
|
1336
|
+
const blocked = takeGuard(guard, f, now);
|
|
1337
|
+
if (blocked) return { kind: "unknown", error: blocked };
|
|
1338
|
+
try {
|
|
1339
|
+
const state = await query(endpoint2);
|
|
1340
|
+
if (state.state === "running") return { kind: "held", pid: state.pid };
|
|
1341
|
+
if (state.state === "unknown") return { kind: "unknown", error: state.error };
|
|
1342
|
+
try {
|
|
1343
|
+
f.unlinkSync(endpoint2);
|
|
1344
|
+
} catch (err) {
|
|
1345
|
+
if (err.code !== "ENOENT") return { kind: "unknown", error: err.message };
|
|
1346
|
+
}
|
|
1347
|
+
return settle(await listen(endpoint2, owner), "the control socket is in use but does not answer");
|
|
1348
|
+
} finally {
|
|
1349
|
+
try {
|
|
1350
|
+
f.rmdirSync(guard);
|
|
1351
|
+
} catch {
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
function takeGuard(guard, f, now) {
|
|
1356
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
1357
|
+
try {
|
|
1358
|
+
f.mkdirSync(guard);
|
|
1359
|
+
return void 0;
|
|
1360
|
+
} catch (err) {
|
|
1361
|
+
if (err.code !== "EEXIST") return err.message;
|
|
1362
|
+
}
|
|
1363
|
+
let age;
|
|
1364
|
+
try {
|
|
1365
|
+
age = now() - f.statSync(guard).mtimeMs;
|
|
1366
|
+
} catch {
|
|
1367
|
+
continue;
|
|
1368
|
+
}
|
|
1369
|
+
if (age <= STALE_GUARD_MS) return "another Codex adapter is starting";
|
|
1370
|
+
try {
|
|
1371
|
+
f.rmdirSync(guard);
|
|
1372
|
+
} catch {
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
return "another Codex adapter is starting";
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
// src/codex/main.ts
|
|
1379
|
+
var WAIT_MIN_MS = 2e3;
|
|
1380
|
+
var WAIT_MAX_MS = 3e4;
|
|
877
1381
|
installCrashGuard("codex adapter");
|
|
878
1382
|
process.stdout.on("error", () => {
|
|
879
1383
|
});
|
|
880
1384
|
process.stderr.on("error", () => {
|
|
881
1385
|
});
|
|
882
1386
|
var config = loadConfig();
|
|
883
|
-
var
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
}
|
|
888
|
-
fs4.writeFileSync(CODEX_PID_FILE, String(process.pid), "utf-8");
|
|
889
|
-
logStartup("Codex adapter");
|
|
890
|
-
var host = process.env.CLAUDE_ALARM_HUB_HOST ?? (config.hub.host === "0.0.0.0" ? DEFAULT_HUB_HOST : config.hub.host);
|
|
891
|
-
var port = process.env.CLAUDE_ALARM_HUB_PORT ? parseInt(process.env.CLAUDE_ALARM_HUB_PORT, 10) : config.hub.port;
|
|
892
|
-
var token = process.env.CLAUDE_ALARM_HUB_TOKEN ?? config.hub.token;
|
|
893
|
-
var adapter = new CodexAdapter({ command: config.codex?.command ?? "codex", hub: { host, port, token } });
|
|
894
|
-
adapter.start();
|
|
1387
|
+
var supervised = process.argv.includes("--watch-stdin");
|
|
1388
|
+
var endpoint = controlEndpoint(CONFIG_DIR);
|
|
1389
|
+
var adapter;
|
|
1390
|
+
var release;
|
|
895
1391
|
var exiting = false;
|
|
896
1392
|
var shutdown = () => {
|
|
897
1393
|
if (exiting) return;
|
|
898
1394
|
exiting = true;
|
|
899
|
-
adapter
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
process.exit(0);
|
|
1395
|
+
adapter?.stop();
|
|
1396
|
+
const exit = () => process.exit(0);
|
|
1397
|
+
setTimeout(exit, 3e3).unref();
|
|
1398
|
+
if (release) release().then(exit, exit);
|
|
1399
|
+
else exit();
|
|
905
1400
|
};
|
|
906
1401
|
process.on("SIGINT", shutdown);
|
|
907
1402
|
process.on("SIGTERM", shutdown);
|
|
908
|
-
if (
|
|
1403
|
+
if (supervised) {
|
|
909
1404
|
process.stdin.on("end", shutdown);
|
|
910
1405
|
process.stdin.on("close", shutdown);
|
|
911
1406
|
process.stdin.resume();
|
|
912
1407
|
}
|
|
1408
|
+
function send(message, then) {
|
|
1409
|
+
if (!process.send || !process.connected) {
|
|
1410
|
+
then?.();
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1413
|
+
process.send(message, void 0, void 0, (err) => {
|
|
1414
|
+
if (err) logger.debug(`Report to the parent not delivered: ${err.message}`);
|
|
1415
|
+
then?.();
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1418
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
1419
|
+
async function takeLock() {
|
|
1420
|
+
let delay = WAIT_MIN_MS;
|
|
1421
|
+
let waitReason;
|
|
1422
|
+
for (; ; ) {
|
|
1423
|
+
const lock = await acquireLock(endpoint, {
|
|
1424
|
+
pid: process.pid,
|
|
1425
|
+
token: config.hub.token ?? "",
|
|
1426
|
+
onStop: () => {
|
|
1427
|
+
if (supervised) logger.warn("Stopped by claude-alarm codex stop; the hub will not start the Codex adapter again until the hub restarts");
|
|
1428
|
+
shutdown();
|
|
1429
|
+
}
|
|
1430
|
+
});
|
|
1431
|
+
if (lock.kind === "owner") {
|
|
1432
|
+
release = lock.close;
|
|
1433
|
+
if (waitReason !== void 0) logger.info("Codex adapter took over: the other adapter has stopped");
|
|
1434
|
+
return true;
|
|
1435
|
+
}
|
|
1436
|
+
if (!supervised) {
|
|
1437
|
+
if (lock.kind === "held") {
|
|
1438
|
+
logger.info(`Codex adapter already running (PID: ${lock.pid})`);
|
|
1439
|
+
send({ type: "codex-already-running", pid: lock.pid }, () => process.exit(0));
|
|
1440
|
+
} else {
|
|
1441
|
+
logger.error(`Codex adapter cannot start: ${lock.error}`);
|
|
1442
|
+
send({ type: "codex-lock-failed", error: lock.error }, () => process.exit(1));
|
|
1443
|
+
}
|
|
1444
|
+
return false;
|
|
1445
|
+
}
|
|
1446
|
+
const reason = lock.kind === "held" ? `held:${lock.pid}` : `unknown:${lock.error}`;
|
|
1447
|
+
if (reason !== waitReason) {
|
|
1448
|
+
waitReason = reason;
|
|
1449
|
+
if (lock.kind === "held") logger.info(`Another Codex adapter is running (PID: ${lock.pid}); this one takes over when it stops`);
|
|
1450
|
+
else logger.warn(`Codex adapter control endpoint is unavailable (${lock.error}); retrying`);
|
|
1451
|
+
}
|
|
1452
|
+
await sleep(delay);
|
|
1453
|
+
delay = Math.min(delay * 2, WAIT_MAX_MS);
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
void takeLock().then((owned) => {
|
|
1457
|
+
if (!owned) return;
|
|
1458
|
+
logStartup("Codex adapter");
|
|
1459
|
+
const { host, port, token } = resolveAdapterHub(config);
|
|
1460
|
+
adapter = new CodexAdapter({
|
|
1461
|
+
command: config.codex?.command ?? "codex",
|
|
1462
|
+
hub: { host, port, token },
|
|
1463
|
+
onFirstConnect: (outcome) => send({ type: "codex-first-connect", ...outcome })
|
|
1464
|
+
});
|
|
1465
|
+
adapter.start();
|
|
1466
|
+
});
|
|
913
1467
|
//# sourceMappingURL=main.js.map
|