@delt/claude-alarm 1.2.0 → 1.4.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 +2 -1
- package/dist/cli.js +574 -206
- package/dist/cli.js.map +1 -1
- package/dist/codex/main.js +459 -93
- package/dist/codex/main.js.map +1 -1
- package/dist/dashboard/index.html +86 -41
- package/dist/hub/server.js +189 -69
- package/dist/hub/server.js.map +1 -1
- package/dist/index.d.ts +19 -3
- package/dist/index.js +189 -69
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/dashboard/index.html +86 -41
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 fs6 from "fs";
|
|
5
|
-
|
|
6
3
|
// src/shared/config.ts
|
|
7
4
|
import fs from "fs";
|
|
8
5
|
import path2 from "path";
|
|
@@ -244,12 +241,12 @@ import { randomUUID as randomUUID2 } from "crypto";
|
|
|
244
241
|
// src/channel/hub-client.ts
|
|
245
242
|
import WebSocket2 from "ws";
|
|
246
243
|
var HubClient = class {
|
|
247
|
-
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 = () => ({})) {
|
|
248
245
|
this.sessionId = sessionId;
|
|
249
246
|
this.sessionName = sessionName;
|
|
250
247
|
this.hubHost = hubHost;
|
|
251
248
|
this.hubPort = hubPort;
|
|
252
|
-
this.token =
|
|
249
|
+
this.token = token;
|
|
253
250
|
this.getPeerName = getPeerName;
|
|
254
251
|
this.getRegistration = getRegistration;
|
|
255
252
|
}
|
|
@@ -360,13 +357,14 @@ var HubClient = class {
|
|
|
360
357
|
};
|
|
361
358
|
|
|
362
359
|
// src/codex/transport.ts
|
|
363
|
-
import { spawn } from "child_process";
|
|
360
|
+
import { execFile, spawn } from "child_process";
|
|
364
361
|
import fs3 from "fs";
|
|
365
362
|
import path3 from "path";
|
|
366
363
|
import { Duplex } from "stream";
|
|
367
364
|
import WebSocket3 from "ws";
|
|
368
365
|
function resolveCommand(command, platform = process.platform, env = process.env) {
|
|
369
|
-
if (platform !== "win32"
|
|
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) };
|
|
370
368
|
const file = findCodex(command, platform, env);
|
|
371
369
|
return file ? { file, shell: file.endsWith(".cmd") } : { file: command, shell: false };
|
|
372
370
|
}
|
|
@@ -389,13 +387,116 @@ function findCodex(command, platform = process.platform, env = process.env) {
|
|
|
389
387
|
];
|
|
390
388
|
return candidates.find((file) => !!file && fs3.existsSync(file));
|
|
391
389
|
}
|
|
390
|
+
var shellSpawns = /* @__PURE__ */ new WeakSet();
|
|
391
|
+
var spawnedThroughShell = (child) => shellSpawns.has(child);
|
|
392
392
|
var defaultSpawn = (command, args) => {
|
|
393
393
|
const { file, shell } = resolveCommand(command);
|
|
394
|
-
if (shell) return spawn(
|
|
395
|
-
|
|
394
|
+
if (!shell) return spawn(file, args, { stdio: "pipe", windowsHide: true });
|
|
395
|
+
const child = spawn(`"${file}" ${args.join(" ")}`, { stdio: "pipe", windowsHide: true, shell: true });
|
|
396
|
+
shellSpawns.add(child);
|
|
397
|
+
return child;
|
|
398
|
+
};
|
|
399
|
+
var queryProcesses = () => new Promise((resolve, reject) => {
|
|
400
|
+
const script = "@(Get-CimInstance Win32_Process | ForEach-Object { [pscustomobject]@{ pid = [int]$_.ProcessId; parentPid = [int]$_.ParentProcessId; creationTime = $_.CreationDate.ToUniversalTime().ToString('o') } }) | ConvertTo-Json -Compress";
|
|
401
|
+
execFile("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], { windowsHide: true, timeout: 5e3, maxBuffer: 4 * 1024 * 1024 }, (err, stdout) => {
|
|
402
|
+
if (err) {
|
|
403
|
+
reject(err);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
try {
|
|
407
|
+
const rows = JSON.parse(stdout);
|
|
408
|
+
if (!Array.isArray(rows) || rows.some((row) => !Number.isInteger(row.pid) || !Number.isInteger(row.parentPid) || typeof row.creationTime !== "string")) {
|
|
409
|
+
throw new Error("invalid process snapshot");
|
|
410
|
+
}
|
|
411
|
+
resolve(rows);
|
|
412
|
+
} catch (error) {
|
|
413
|
+
reject(error);
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
var runFile = (file, args, options, callback) => {
|
|
418
|
+
execFile(file, args, options, (err) => callback(err));
|
|
396
419
|
};
|
|
397
|
-
function
|
|
420
|
+
function descendantsOf(rootPid, rows) {
|
|
421
|
+
const root = rows.find((row) => row.pid === rootPid);
|
|
422
|
+
if (!root) return void 0;
|
|
423
|
+
const found = [];
|
|
424
|
+
const seen = /* @__PURE__ */ new Set([root.pid]);
|
|
425
|
+
let level = [root];
|
|
426
|
+
for (let depth = 1; level.length; depth++) {
|
|
427
|
+
const next = [];
|
|
428
|
+
for (const parent of level) {
|
|
429
|
+
for (const row of rows) {
|
|
430
|
+
if (row.parentPid !== parent.pid || seen.has(row.pid) || row.creationTime < parent.creationTime) continue;
|
|
431
|
+
seen.add(row.pid);
|
|
432
|
+
next.push(row);
|
|
433
|
+
found.push({ ...row, depth });
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
level = next;
|
|
437
|
+
}
|
|
438
|
+
return found;
|
|
439
|
+
}
|
|
440
|
+
var exited = (child) => child.exitCode !== null || child.signalCode !== null;
|
|
441
|
+
function treeKiller(platform = process.platform, run = runFile, query = queryProcesses, throughShell = spawnedThroughShell) {
|
|
442
|
+
const recorded = /* @__PURE__ */ new WeakMap();
|
|
443
|
+
const stop = (pid) => new Promise((resolve) => {
|
|
444
|
+
run("taskkill", ["/PID", String(pid), "/F"], { windowsHide: true }, (err) => {
|
|
445
|
+
if (err) logger.debug(`taskkill ${pid} failed: ${err.message}`);
|
|
446
|
+
resolve();
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
const stopDeepestFirst = async (descendants) => {
|
|
450
|
+
for (const descendant of [...descendants].sort((a, b) => b.depth - a.depth)) await stop(descendant.pid);
|
|
451
|
+
};
|
|
452
|
+
const snapshot = async () => {
|
|
453
|
+
try {
|
|
454
|
+
return await query();
|
|
455
|
+
} catch (err) {
|
|
456
|
+
logger.debug(`codex process query failed: ${String(err)}`);
|
|
457
|
+
return void 0;
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
const isShellTree = (child) => platform === "win32" && child.pid !== void 0 && throughShell(child);
|
|
461
|
+
const killer = async (child) => {
|
|
462
|
+
if (!isShellTree(child)) {
|
|
463
|
+
if (!exited(child)) child.kill();
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
if (!exited(child)) {
|
|
467
|
+
const rows = await snapshot();
|
|
468
|
+
if (!exited(child)) {
|
|
469
|
+
const descendants2 = rows && descendantsOf(child.pid, rows);
|
|
470
|
+
if (descendants2) await stopDeepestFirst(descendants2);
|
|
471
|
+
child.kill();
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
const descendants = await recorded.get(child) ?? [];
|
|
476
|
+
if (!descendants.length) return;
|
|
477
|
+
const current = await snapshot();
|
|
478
|
+
if (!current) return;
|
|
479
|
+
await stopDeepestFirst(descendants.filter((d) => current.some((row) => row.pid === d.pid && row.creationTime === d.creationTime)));
|
|
480
|
+
};
|
|
481
|
+
killer.track = async (child) => {
|
|
482
|
+
if (!isShellTree(child) || exited(child)) return;
|
|
483
|
+
const earlier = recorded.get(child);
|
|
484
|
+
const latest = (async () => {
|
|
485
|
+
const rows = await snapshot();
|
|
486
|
+
const found = rows && !exited(child) ? descendantsOf(child.pid, rows) ?? [] : [];
|
|
487
|
+
const before = await earlier ?? [];
|
|
488
|
+
return [...before, ...found.filter((d) => !before.some((b) => b.pid === d.pid && b.creationTime === d.creationTime))];
|
|
489
|
+
})();
|
|
490
|
+
recorded.set(child, latest);
|
|
491
|
+
await latest;
|
|
492
|
+
};
|
|
493
|
+
return killer;
|
|
494
|
+
}
|
|
495
|
+
var killTree = treeKiller();
|
|
496
|
+
var HANDSHAKE_TIMEOUT_MS = 1e4;
|
|
497
|
+
function connectProxy(command, spawnFn = defaultSpawn, timeoutMs = HANDSHAKE_TIMEOUT_MS, killTreeFn = killTree) {
|
|
398
498
|
const child = spawnFn(command, ["app-server", "proxy"]);
|
|
499
|
+
void killTreeFn.track?.(child);
|
|
399
500
|
child.stdin?.on("error", () => {
|
|
400
501
|
});
|
|
401
502
|
child.stderr?.on("data", (d) => logger.debug(`codex proxy: ${String(d).trim()}`));
|
|
@@ -422,17 +523,20 @@ function connectProxy(command, spawnFn = defaultSpawn) {
|
|
|
422
523
|
const ws = new WebSocket3("ws://localhost/", {
|
|
423
524
|
createConnection: (() => stream)
|
|
424
525
|
});
|
|
425
|
-
|
|
526
|
+
let closing;
|
|
527
|
+
const close = () => closing ??= (async () => {
|
|
426
528
|
ws.terminate();
|
|
427
|
-
child.
|
|
428
|
-
|
|
529
|
+
child.stdin?.end();
|
|
530
|
+
await killTreeFn(child);
|
|
531
|
+
})();
|
|
429
532
|
return new Promise((resolve, reject) => {
|
|
430
533
|
let settled = false;
|
|
534
|
+
const timer = setTimeout(() => fail(new Error(`codex daemon did not answer within ${timeoutMs / 1e3}s`)), timeoutMs);
|
|
431
535
|
const fail = (err) => {
|
|
432
536
|
if (settled) return;
|
|
433
537
|
settled = true;
|
|
434
|
-
|
|
435
|
-
reject(err);
|
|
538
|
+
clearTimeout(timer);
|
|
539
|
+
void close().then(() => reject(err), () => reject(err));
|
|
436
540
|
};
|
|
437
541
|
child.on("error", fail);
|
|
438
542
|
child.once("exit", (code) => fail(new Error(`codex proxy exited (code ${code})`)));
|
|
@@ -440,8 +544,10 @@ function connectProxy(command, spawnFn = defaultSpawn) {
|
|
|
440
544
|
ws.once("open", () => {
|
|
441
545
|
if (settled) return;
|
|
442
546
|
settled = true;
|
|
547
|
+
clearTimeout(timer);
|
|
443
548
|
child.on("error", (err) => logger.warn(`codex proxy error: ${err.message}`));
|
|
444
549
|
ws.on("error", (err) => logger.warn(`codex daemon socket error: ${err.message}`));
|
|
550
|
+
void killTreeFn.track?.(child);
|
|
445
551
|
resolve({ ws, close });
|
|
446
552
|
});
|
|
447
553
|
});
|
|
@@ -651,6 +757,8 @@ var CodexAdapter = class {
|
|
|
651
757
|
this.link = new CodexHubLink(opts.hub, { id: randomUUID2(), host: this.hostName }, (call) => this.onCall(call), opts.linkReconnectMs);
|
|
652
758
|
}
|
|
653
759
|
conn;
|
|
760
|
+
connecting;
|
|
761
|
+
closing = /* @__PURE__ */ new Set();
|
|
654
762
|
rpc;
|
|
655
763
|
threads = /* @__PURE__ */ new Map();
|
|
656
764
|
approvals = /* @__PURE__ */ new Map();
|
|
@@ -666,22 +774,29 @@ var CodexAdapter = class {
|
|
|
666
774
|
start() {
|
|
667
775
|
this.stopped = false;
|
|
668
776
|
this.link.connect();
|
|
669
|
-
|
|
777
|
+
this.connecting = this.connect();
|
|
670
778
|
}
|
|
671
|
-
stop() {
|
|
779
|
+
async stop() {
|
|
672
780
|
this.stopped = true;
|
|
673
781
|
this.link.disconnect();
|
|
674
782
|
if (this.retryTimer) clearTimeout(this.retryTimer);
|
|
675
783
|
this.retryTimer = void 0;
|
|
676
784
|
for (const id of [...this.threads.keys()]) this.drop(id);
|
|
677
|
-
this.conn
|
|
785
|
+
this.closeConn(this.conn);
|
|
786
|
+
await this.connecting;
|
|
787
|
+
await Promise.all(this.closing);
|
|
788
|
+
}
|
|
789
|
+
closeConn(conn) {
|
|
790
|
+
if (!conn) return;
|
|
791
|
+
const closing = conn.close().finally(() => this.closing.delete(closing));
|
|
792
|
+
this.closing.add(closing);
|
|
678
793
|
}
|
|
679
794
|
async connect() {
|
|
680
795
|
let rpc;
|
|
681
796
|
try {
|
|
682
797
|
const conn = await connectProxy(this.opts.command, this.opts.spawnFn);
|
|
683
798
|
if (this.stopped) {
|
|
684
|
-
|
|
799
|
+
this.closeConn(conn);
|
|
685
800
|
return;
|
|
686
801
|
}
|
|
687
802
|
const live = new RpcClient(conn.ws, this.opts.rpcTimeoutMs);
|
|
@@ -713,7 +828,7 @@ var CodexAdapter = class {
|
|
|
713
828
|
});
|
|
714
829
|
if (rpc && this.rpc === rpc) {
|
|
715
830
|
this.rpc = void 0;
|
|
716
|
-
this.conn
|
|
831
|
+
this.closeConn(this.conn);
|
|
717
832
|
this.conn = void 0;
|
|
718
833
|
}
|
|
719
834
|
for (const id of [...this.threads.keys()]) this.drop(id);
|
|
@@ -723,7 +838,7 @@ var CodexAdapter = class {
|
|
|
723
838
|
onDaemonLost() {
|
|
724
839
|
this.setReady(false);
|
|
725
840
|
this.rpc = void 0;
|
|
726
|
-
this.conn
|
|
841
|
+
this.closeConn(this.conn);
|
|
727
842
|
this.conn = void 0;
|
|
728
843
|
for (const id of [...this.threads.keys()]) this.drop(id);
|
|
729
844
|
if (!this.stopped) logger.warn("Codex daemon connection lost");
|
|
@@ -733,7 +848,7 @@ var CodexAdapter = class {
|
|
|
733
848
|
if (this.stopped || this.retryTimer) return;
|
|
734
849
|
this.retryTimer = setTimeout(() => {
|
|
735
850
|
this.retryTimer = void 0;
|
|
736
|
-
|
|
851
|
+
this.connecting = this.connect();
|
|
737
852
|
}, this.delay);
|
|
738
853
|
this.delay = Math.min(this.delay * 2, this.opts.reconnectMaxMs ?? 6e4);
|
|
739
854
|
}
|
|
@@ -749,10 +864,10 @@ var CodexAdapter = class {
|
|
|
749
864
|
noticeNotFound() {
|
|
750
865
|
if (this.notFoundNotice !== "unsent") return;
|
|
751
866
|
this.notFoundNotice = "sending";
|
|
752
|
-
const { host
|
|
753
|
-
fetch(`http://${
|
|
867
|
+
const { host, port, token } = this.opts.hub;
|
|
868
|
+
fetch(`http://${host}:${port}/api/notify`, {
|
|
754
869
|
method: "POST",
|
|
755
|
-
headers: { "Content-Type": "application/json", ...
|
|
870
|
+
headers: { "Content-Type": "application/json", ...token ? { Authorization: `Bearer ${token}` } : {} },
|
|
756
871
|
body: JSON.stringify({
|
|
757
872
|
title: "Codex not found",
|
|
758
873
|
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.`,
|
|
@@ -831,18 +946,18 @@ var CodexAdapter = class {
|
|
|
831
946
|
this.drop(thread.id);
|
|
832
947
|
return;
|
|
833
948
|
}
|
|
834
|
-
const
|
|
835
|
-
if (
|
|
836
|
-
|
|
837
|
-
|
|
949
|
+
const existing = this.threads.get(thread.id);
|
|
950
|
+
if (existing) {
|
|
951
|
+
existing.thread = thread;
|
|
952
|
+
existing.hub.reregister();
|
|
838
953
|
} else {
|
|
839
|
-
const { host
|
|
954
|
+
const { host, port, token } = this.opts.hub;
|
|
840
955
|
const hub = new HubClient(
|
|
841
956
|
codexSessionId(thread.id),
|
|
842
957
|
threadTitle(thread),
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
958
|
+
host,
|
|
959
|
+
port,
|
|
960
|
+
token,
|
|
846
961
|
() => void 0,
|
|
847
962
|
() => this.registration(thread.id)
|
|
848
963
|
);
|
|
@@ -890,6 +1005,7 @@ var CodexAdapter = class {
|
|
|
890
1005
|
}
|
|
891
1006
|
} catch (err) {
|
|
892
1007
|
logger.debug(`subscription sync for ${threadId} deferred: ${err.message}`);
|
|
1008
|
+
if (t.wantSubscribed && !t.subscribed && t.thread.status.type === "active") t.unrelayed = true;
|
|
893
1009
|
}
|
|
894
1010
|
}
|
|
895
1011
|
releaseLater(t) {
|
|
@@ -1025,9 +1141,9 @@ var CodexAdapter = class {
|
|
|
1025
1141
|
}
|
|
1026
1142
|
onHubMessage(threadId, msg) {
|
|
1027
1143
|
if (msg.type === "message_to_session") {
|
|
1028
|
-
this.enqueue(threadId, async () => textInput(msg.content, msg.source));
|
|
1144
|
+
this.enqueue(threadId, async () => textInput(msg.content, msg.source), msg.source);
|
|
1029
1145
|
} else if (msg.type === "image_to_session") {
|
|
1030
|
-
this.enqueue(threadId, () => imageInput(msg.imagePath, msg.mimeType, msg.content, msg.source));
|
|
1146
|
+
this.enqueue(threadId, () => imageInput(msg.imagePath, msg.mimeType, msg.content, msg.source), msg.source);
|
|
1031
1147
|
} else if (msg.type === "permission_response") {
|
|
1032
1148
|
this.answer(threadId, msg.requestId, msg.choiceId);
|
|
1033
1149
|
} else if (msg.type === "codex_close") {
|
|
@@ -1035,16 +1151,16 @@ var CodexAdapter = class {
|
|
|
1035
1151
|
}
|
|
1036
1152
|
}
|
|
1037
1153
|
// One message at a time per conversation, so a message right behind another sees the turn the first one started.
|
|
1038
|
-
enqueue(threadId, build) {
|
|
1154
|
+
enqueue(threadId, build, source) {
|
|
1039
1155
|
const t = this.threads.get(threadId);
|
|
1040
1156
|
if (!t) return;
|
|
1041
|
-
t.sending = t.sending.then(() => this.deliver(t, build)).catch((err) => logger.warn(`Codex delivery for ${threadId} failed: ${err.message}`));
|
|
1157
|
+
t.sending = t.sending.then(() => this.deliver(t, build, source)).catch((err) => logger.warn(`Codex delivery for ${threadId} failed: ${err.message}`));
|
|
1042
1158
|
}
|
|
1043
|
-
async deliver(t, build) {
|
|
1159
|
+
async deliver(t, build, source) {
|
|
1044
1160
|
const threadId = t.thread.id;
|
|
1045
1161
|
if (this.threads.get(threadId) !== t) return;
|
|
1046
|
-
if (
|
|
1047
|
-
this.
|
|
1162
|
+
if (this.waitingForAnswer(t)) {
|
|
1163
|
+
this.refuseWhileWaiting(threadId);
|
|
1048
1164
|
return;
|
|
1049
1165
|
}
|
|
1050
1166
|
let input;
|
|
@@ -1065,20 +1181,37 @@ var CodexAdapter = class {
|
|
|
1065
1181
|
const unsubscribed = !t.subscribed;
|
|
1066
1182
|
const running = await this.runningTurn(rpc, threadId);
|
|
1067
1183
|
if (this.threads.get(threadId) !== t) return;
|
|
1184
|
+
if (this.waitingForAnswer(t)) {
|
|
1185
|
+
this.abandonDelivery(t, wasUnrelayed);
|
|
1186
|
+
this.refuseWhileWaiting(threadId);
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1068
1189
|
if (unsubscribed) t.unrelayed = true;
|
|
1069
1190
|
if (running) {
|
|
1070
1191
|
await rpc.request("turn/steer", { threadId, expectedTurnId: running, input });
|
|
1071
|
-
this.notify(threadId, "Queued", "Queued: Codex will read it after its current step.", "info");
|
|
1192
|
+
this.notify(threadId, "Queued", "Queued: Codex will read it after its current step.", "info", source);
|
|
1072
1193
|
return;
|
|
1073
1194
|
}
|
|
1074
1195
|
await rpc.request("turn/start", { threadId, input });
|
|
1075
1196
|
} catch (err) {
|
|
1076
|
-
t
|
|
1077
|
-
t.unrelayed = wasUnrelayed;
|
|
1078
|
-
this.releaseLater(t);
|
|
1197
|
+
this.abandonDelivery(t, wasUnrelayed);
|
|
1079
1198
|
this.notify(threadId, "Not delivered", `Codex rejected the message: ${err.message}`, "warning");
|
|
1080
1199
|
}
|
|
1081
1200
|
}
|
|
1201
|
+
abandonDelivery(t, wasUnrelayed) {
|
|
1202
|
+
t.pendingTurn = false;
|
|
1203
|
+
t.unrelayed = wasUnrelayed;
|
|
1204
|
+
this.releaseLater(t);
|
|
1205
|
+
}
|
|
1206
|
+
// The approval request itself can arrive before the status broadcast that marks the thread as waiting.
|
|
1207
|
+
waitingForAnswer(t) {
|
|
1208
|
+
if (hubStatus(t.thread.status) === "waiting_input") return true;
|
|
1209
|
+
for (const a of this.approvals.values()) if (a.threadId === t.thread.id && !a.answered) return true;
|
|
1210
|
+
return false;
|
|
1211
|
+
}
|
|
1212
|
+
refuseWhileWaiting(threadId) {
|
|
1213
|
+
this.notify(threadId, "Not delivered", "Codex is waiting for an approval or input. Answer it first, then send the message again.", "warning");
|
|
1214
|
+
}
|
|
1082
1215
|
// The list is unavailable before a new conversation's first turn; starting a turn is safe then, since turn/start steers a running turn.
|
|
1083
1216
|
async runningTurn(rpc, threadId) {
|
|
1084
1217
|
try {
|
|
@@ -1108,7 +1241,7 @@ var CodexAdapter = class {
|
|
|
1108
1241
|
}
|
|
1109
1242
|
if (!t || !view) {
|
|
1110
1243
|
if (t && (broken || USER_REQUESTS.has(method))) {
|
|
1111
|
-
this.
|
|
1244
|
+
this.waitingInCodex(t.thread.id);
|
|
1112
1245
|
} else {
|
|
1113
1246
|
logger.debug(`Ignoring Codex server request ${method}`);
|
|
1114
1247
|
}
|
|
@@ -1118,17 +1251,26 @@ var CodexAdapter = class {
|
|
|
1118
1251
|
if (a.threadId === t.thread.id && a.rpcId === rpcId) return;
|
|
1119
1252
|
}
|
|
1120
1253
|
const requestId = randomUUID2();
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1254
|
+
try {
|
|
1255
|
+
this.approvals.set(requestId, { threadId: t.thread.id, rpcId, turnId: params.turnId, choices: view.choices, answered: false });
|
|
1256
|
+
t.hub.send({
|
|
1257
|
+
type: "permission_request",
|
|
1258
|
+
sessionId: codexSessionId(t.thread.id),
|
|
1259
|
+
requestId,
|
|
1260
|
+
toolName: view.toolName,
|
|
1261
|
+
description: view.description,
|
|
1262
|
+
inputPreview: view.inputPreview,
|
|
1263
|
+
timestamp: Date.now(),
|
|
1264
|
+
choices: view.choices.map((c, i) => ({ id: String(i), label: c.label }))
|
|
1265
|
+
});
|
|
1266
|
+
} catch (err) {
|
|
1267
|
+
this.approvals.delete(requestId);
|
|
1268
|
+
logger.warn(`Codex ${method} could not be relayed: ${err.message}`);
|
|
1269
|
+
this.waitingInCodex(t.thread.id);
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
waitingInCodex(threadId) {
|
|
1273
|
+
this.notify(threadId, "Codex is waiting", "Codex asked for input that claude-alarm cannot relay. Handle it in Codex.", "warning");
|
|
1132
1274
|
}
|
|
1133
1275
|
answer(threadId, requestId, choiceId) {
|
|
1134
1276
|
const a = this.approvals.get(requestId);
|
|
@@ -1149,17 +1291,17 @@ var CodexAdapter = class {
|
|
|
1149
1291
|
this.approvals.delete(requestId);
|
|
1150
1292
|
this.threads.get(a.threadId)?.hub.send({ type: "permission_resolved", sessionId: codexSessionId(a.threadId), requestId, state });
|
|
1151
1293
|
}
|
|
1152
|
-
notify(threadId, title, message, level) {
|
|
1153
|
-
this.threads.get(threadId)?.hub.send({ type: "notify", sessionId: codexSessionId(threadId), title, message, level });
|
|
1294
|
+
notify(threadId, title, message, level, to) {
|
|
1295
|
+
this.threads.get(threadId)?.hub.send({ type: "notify", sessionId: codexSessionId(threadId), title, message, level, ...to ? { to } : {} });
|
|
1154
1296
|
}
|
|
1155
1297
|
};
|
|
1156
1298
|
|
|
1157
1299
|
// src/shared/hub-url.ts
|
|
1158
1300
|
import net from "net";
|
|
1159
|
-
function hubUrlHost(
|
|
1160
|
-
if (
|
|
1161
|
-
if (
|
|
1162
|
-
return net.isIPv6(
|
|
1301
|
+
function hubUrlHost(host) {
|
|
1302
|
+
if (host === "" || host === "0.0.0.0") return "127.0.0.1";
|
|
1303
|
+
if (host === "::") return "[::1]";
|
|
1304
|
+
return net.isIPv6(host) ? `[${host}]` : host;
|
|
1163
1305
|
}
|
|
1164
1306
|
|
|
1165
1307
|
// src/codex/hub-target.ts
|
|
@@ -1175,56 +1317,280 @@ function resolveAdapterHub(config2, env = process.env) {
|
|
|
1175
1317
|
};
|
|
1176
1318
|
}
|
|
1177
1319
|
|
|
1178
|
-
// src/codex/
|
|
1179
|
-
|
|
1320
|
+
// src/codex/instance-lock.ts
|
|
1321
|
+
import crypto from "crypto";
|
|
1322
|
+
import fs6 from "fs";
|
|
1323
|
+
import net2 from "net";
|
|
1324
|
+
import path5 from "path";
|
|
1325
|
+
var CONTROL_PROTOCOL = 1;
|
|
1326
|
+
var STALE_GUARD_MS = 1e4;
|
|
1327
|
+
var STOP_PROOF_LABEL = "claude-alarm codex stop";
|
|
1328
|
+
var MAX_LINE_BYTES = 4096;
|
|
1329
|
+
var REQUEST_IDLE_MS = 2e3;
|
|
1330
|
+
var MAX_SOCKET_PATH_BYTES = 103;
|
|
1331
|
+
function stopProof(token) {
|
|
1332
|
+
return crypto.createHmac("sha256", token).update(STOP_PROOF_LABEL).digest("hex");
|
|
1333
|
+
}
|
|
1334
|
+
function controlEndpoint(configDir, platform = process.platform) {
|
|
1335
|
+
if (platform !== "win32") return path5.posix.join(configDir, "codex.sock");
|
|
1336
|
+
let dir;
|
|
1180
1337
|
try {
|
|
1181
|
-
|
|
1182
|
-
return true;
|
|
1338
|
+
dir = fs6.realpathSync.native(configDir);
|
|
1183
1339
|
} catch {
|
|
1184
|
-
|
|
1340
|
+
dir = path5.win32.resolve(configDir);
|
|
1185
1341
|
}
|
|
1342
|
+
const hash = crypto.createHash("sha256").update(dir.toLowerCase()).digest("hex").slice(0, 16);
|
|
1343
|
+
return `\\\\.\\pipe\\claude-alarm-codex-${hash}`;
|
|
1186
1344
|
}
|
|
1345
|
+
function ask(endpoint2, request, timeoutMs) {
|
|
1346
|
+
return new Promise((resolve) => {
|
|
1347
|
+
let buffer = "";
|
|
1348
|
+
let settled = false;
|
|
1349
|
+
const socket = net2.connect(endpoint2);
|
|
1350
|
+
const finish = (answer) => {
|
|
1351
|
+
if (settled) return;
|
|
1352
|
+
settled = true;
|
|
1353
|
+
clearTimeout(timer);
|
|
1354
|
+
socket.destroy();
|
|
1355
|
+
resolve(answer);
|
|
1356
|
+
};
|
|
1357
|
+
const timer = setTimeout(() => finish({ state: "unknown", error: `no answer within ${timeoutMs}ms` }), timeoutMs);
|
|
1358
|
+
socket.setEncoding("utf8");
|
|
1359
|
+
socket.on("connect", () => socket.write(`${JSON.stringify(request)}
|
|
1360
|
+
`));
|
|
1361
|
+
socket.on("data", (chunk) => {
|
|
1362
|
+
buffer += chunk;
|
|
1363
|
+
const end = buffer.indexOf("\n");
|
|
1364
|
+
if (end < 0) {
|
|
1365
|
+
if (Buffer.byteLength(buffer) > MAX_LINE_BYTES) finish({ state: "unknown", error: "reply too long" });
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1368
|
+
try {
|
|
1369
|
+
finish({ reply: JSON.parse(buffer.slice(0, end)) });
|
|
1370
|
+
} catch {
|
|
1371
|
+
finish({ state: "unknown", error: "unreadable reply" });
|
|
1372
|
+
}
|
|
1373
|
+
});
|
|
1374
|
+
socket.on("end", () => finish({ state: "unknown", error: "closed without a reply" }));
|
|
1375
|
+
socket.on("error", (err) => {
|
|
1376
|
+
if (err.code === "ENOENT" || err.code === "ECONNREFUSED") finish({ state: "absent" });
|
|
1377
|
+
else finish({ state: "unknown", error: err.code ?? err.message });
|
|
1378
|
+
});
|
|
1379
|
+
});
|
|
1380
|
+
}
|
|
1381
|
+
async function queryOwner(endpoint2, timeoutMs = 1e3) {
|
|
1382
|
+
const answer = await ask(endpoint2, { type: "status" }, timeoutMs);
|
|
1383
|
+
if (!("reply" in answer)) return answer;
|
|
1384
|
+
const reply = answer.reply;
|
|
1385
|
+
if (reply?.type === "status" && reply.protocol === CONTROL_PROTOCOL && Number.isInteger(reply.pid)) {
|
|
1386
|
+
return { state: "running", pid: reply.pid };
|
|
1387
|
+
}
|
|
1388
|
+
return { state: "unknown", error: "unexpected reply" };
|
|
1389
|
+
}
|
|
1390
|
+
var line = (message) => `${JSON.stringify(message)}
|
|
1391
|
+
`;
|
|
1392
|
+
function controlServer(owner) {
|
|
1393
|
+
const expectedProof = Buffer.from(stopProof(owner.token));
|
|
1394
|
+
return net2.createServer((socket) => {
|
|
1395
|
+
let buffer = "";
|
|
1396
|
+
socket.setEncoding("utf8");
|
|
1397
|
+
socket.on("error", () => {
|
|
1398
|
+
});
|
|
1399
|
+
const idle = setTimeout(() => socket.destroy(), REQUEST_IDLE_MS);
|
|
1400
|
+
socket.on("close", () => clearTimeout(idle));
|
|
1401
|
+
const onData = (chunk) => {
|
|
1402
|
+
buffer += chunk;
|
|
1403
|
+
const end = buffer.indexOf("\n");
|
|
1404
|
+
if (end < 0) {
|
|
1405
|
+
if (Buffer.byteLength(buffer) > MAX_LINE_BYTES) socket.destroy();
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
socket.off("data", onData);
|
|
1409
|
+
clearTimeout(idle);
|
|
1410
|
+
let request = null;
|
|
1411
|
+
try {
|
|
1412
|
+
request = JSON.parse(buffer.slice(0, end));
|
|
1413
|
+
} catch {
|
|
1414
|
+
}
|
|
1415
|
+
if (request?.type === "status") {
|
|
1416
|
+
socket.end(line({ type: "status", protocol: CONTROL_PROTOCOL, pid: owner.pid }));
|
|
1417
|
+
} else if (request?.type === "stop") {
|
|
1418
|
+
const proof = typeof request.proof === "string" ? Buffer.from(request.proof) : void 0;
|
|
1419
|
+
if (owner.token !== "" && proof && proof.length === expectedProof.length && crypto.timingSafeEqual(proof, expectedProof)) {
|
|
1420
|
+
socket.end(line({ type: "stopping", pid: owner.pid }), () => owner.onStop());
|
|
1421
|
+
} else {
|
|
1422
|
+
socket.end(line({ type: "error", error: "unauthorized" }));
|
|
1423
|
+
}
|
|
1424
|
+
} else {
|
|
1425
|
+
socket.end(line({ type: "error", error: "unknown request" }));
|
|
1426
|
+
}
|
|
1427
|
+
};
|
|
1428
|
+
socket.on("data", onData);
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1431
|
+
function listenControl(endpoint2, owner) {
|
|
1432
|
+
const server = controlServer(owner);
|
|
1433
|
+
return new Promise((resolve) => {
|
|
1434
|
+
const onError = (err) => {
|
|
1435
|
+
resolve(err.code === "EADDRINUSE" ? { kind: "busy" } : { kind: "unknown", error: err.code ?? err.message });
|
|
1436
|
+
};
|
|
1437
|
+
server.once("error", onError);
|
|
1438
|
+
server.listen(endpoint2, () => {
|
|
1439
|
+
server.off("error", onError);
|
|
1440
|
+
server.on("error", () => {
|
|
1441
|
+
});
|
|
1442
|
+
resolve({ kind: "owner", close: () => new Promise((done) => server.close(() => done())) });
|
|
1443
|
+
});
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
function settle(attempt, busy) {
|
|
1447
|
+
return attempt.kind === "busy" ? { kind: "unknown", error: busy } : attempt;
|
|
1448
|
+
}
|
|
1449
|
+
async function acquireLock(endpoint2, owner, deps = {}) {
|
|
1450
|
+
const platform = deps.platform ?? process.platform;
|
|
1451
|
+
const listen = deps.listen ?? listenControl;
|
|
1452
|
+
const query = deps.queryOwner ?? ((e) => queryOwner(e));
|
|
1453
|
+
if (platform !== "win32" && Buffer.byteLength(endpoint2) > MAX_SOCKET_PATH_BYTES) {
|
|
1454
|
+
return { kind: "unknown", error: `control socket path is too long: ${endpoint2}` };
|
|
1455
|
+
}
|
|
1456
|
+
const first = await listen(endpoint2, owner);
|
|
1457
|
+
if (first.kind !== "busy") return first;
|
|
1458
|
+
const state = await query(endpoint2);
|
|
1459
|
+
if (state.state === "running") return { kind: "held", pid: state.pid };
|
|
1460
|
+
if (state.state === "unknown") return { kind: "unknown", error: state.error };
|
|
1461
|
+
if (platform === "win32") return settle(await listen(endpoint2, owner), "the control pipe is in use but does not answer");
|
|
1462
|
+
return recoverSocket(endpoint2, owner, listen, query, deps.fs ?? fs6, deps.now ?? Date.now);
|
|
1463
|
+
}
|
|
1464
|
+
async function recoverSocket(endpoint2, owner, listen, query, f, now) {
|
|
1465
|
+
const guard = `${endpoint2}.lock`;
|
|
1466
|
+
const blocked = takeGuard(guard, f, now);
|
|
1467
|
+
if (blocked) return { kind: "unknown", error: blocked };
|
|
1468
|
+
try {
|
|
1469
|
+
const state = await query(endpoint2);
|
|
1470
|
+
if (state.state === "running") return { kind: "held", pid: state.pid };
|
|
1471
|
+
if (state.state === "unknown") return { kind: "unknown", error: state.error };
|
|
1472
|
+
try {
|
|
1473
|
+
f.unlinkSync(endpoint2);
|
|
1474
|
+
} catch (err) {
|
|
1475
|
+
if (err.code !== "ENOENT") return { kind: "unknown", error: err.message };
|
|
1476
|
+
}
|
|
1477
|
+
return settle(await listen(endpoint2, owner), "the control socket is in use but does not answer");
|
|
1478
|
+
} finally {
|
|
1479
|
+
try {
|
|
1480
|
+
f.rmdirSync(guard);
|
|
1481
|
+
} catch {
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
function takeGuard(guard, f, now) {
|
|
1486
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
1487
|
+
try {
|
|
1488
|
+
f.mkdirSync(guard);
|
|
1489
|
+
return void 0;
|
|
1490
|
+
} catch (err) {
|
|
1491
|
+
if (err.code !== "EEXIST") return err.message;
|
|
1492
|
+
}
|
|
1493
|
+
let age;
|
|
1494
|
+
try {
|
|
1495
|
+
age = now() - f.statSync(guard).mtimeMs;
|
|
1496
|
+
} catch {
|
|
1497
|
+
continue;
|
|
1498
|
+
}
|
|
1499
|
+
if (age <= STALE_GUARD_MS) return "another Codex adapter is starting";
|
|
1500
|
+
try {
|
|
1501
|
+
f.rmdirSync(guard);
|
|
1502
|
+
} catch {
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
return "another Codex adapter is starting";
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
// src/codex/main.ts
|
|
1509
|
+
var WAIT_MIN_MS = 2e3;
|
|
1510
|
+
var WAIT_MAX_MS = 3e4;
|
|
1187
1511
|
installCrashGuard("codex adapter");
|
|
1188
1512
|
process.stdout.on("error", () => {
|
|
1189
1513
|
});
|
|
1190
1514
|
process.stderr.on("error", () => {
|
|
1191
1515
|
});
|
|
1192
1516
|
var config = loadConfig();
|
|
1193
|
-
var
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
}
|
|
1198
|
-
fs6.writeFileSync(CODEX_PID_FILE, String(process.pid), "utf-8");
|
|
1199
|
-
logStartup("Codex adapter");
|
|
1200
|
-
var { host, port, token } = resolveAdapterHub(config);
|
|
1201
|
-
var adapter = new CodexAdapter({
|
|
1202
|
-
command: config.codex?.command ?? "codex",
|
|
1203
|
-
hub: { host, port, token },
|
|
1204
|
-
onFirstConnect: (outcome) => {
|
|
1205
|
-
if (!process.send || !process.connected) return;
|
|
1206
|
-
process.send({ type: "codex-first-connect", ...outcome }, void 0, void 0, (err) => {
|
|
1207
|
-
if (err) logger.debug(`First-connect report not delivered: ${err.message}`);
|
|
1208
|
-
});
|
|
1209
|
-
}
|
|
1210
|
-
});
|
|
1211
|
-
adapter.start();
|
|
1517
|
+
var supervised = process.argv.includes("--watch-stdin");
|
|
1518
|
+
var endpoint = controlEndpoint(CONFIG_DIR);
|
|
1519
|
+
var adapter;
|
|
1520
|
+
var release;
|
|
1212
1521
|
var exiting = false;
|
|
1213
1522
|
var shutdown = () => {
|
|
1214
1523
|
if (exiting) return;
|
|
1215
1524
|
exiting = true;
|
|
1216
|
-
adapter
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
}
|
|
1221
|
-
process.exit(0);
|
|
1525
|
+
const stopped = adapter?.stop();
|
|
1526
|
+
const exit = () => process.exit(0);
|
|
1527
|
+
setTimeout(exit, 3e3).unref();
|
|
1528
|
+
Promise.all([stopped, release?.()]).then(exit, exit);
|
|
1222
1529
|
};
|
|
1223
1530
|
process.on("SIGINT", shutdown);
|
|
1224
1531
|
process.on("SIGTERM", shutdown);
|
|
1225
|
-
if (
|
|
1532
|
+
if (supervised) {
|
|
1226
1533
|
process.stdin.on("end", shutdown);
|
|
1227
1534
|
process.stdin.on("close", shutdown);
|
|
1228
1535
|
process.stdin.resume();
|
|
1229
1536
|
}
|
|
1537
|
+
function send(message, then) {
|
|
1538
|
+
if (!process.send || !process.connected) {
|
|
1539
|
+
then?.();
|
|
1540
|
+
return;
|
|
1541
|
+
}
|
|
1542
|
+
process.send(message, void 0, void 0, (err) => {
|
|
1543
|
+
if (err) logger.debug(`Report to the parent not delivered: ${err.message}`);
|
|
1544
|
+
then?.();
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
1548
|
+
async function takeLock() {
|
|
1549
|
+
let delay = WAIT_MIN_MS;
|
|
1550
|
+
let waitReason;
|
|
1551
|
+
for (; ; ) {
|
|
1552
|
+
const lock = await acquireLock(endpoint, {
|
|
1553
|
+
pid: process.pid,
|
|
1554
|
+
token: config.hub.token ?? "",
|
|
1555
|
+
onStop: () => {
|
|
1556
|
+
if (supervised) logger.warn("Stopped by claude-alarm codex stop; the hub will not start the Codex adapter again until the hub restarts");
|
|
1557
|
+
shutdown();
|
|
1558
|
+
}
|
|
1559
|
+
});
|
|
1560
|
+
if (lock.kind === "owner") {
|
|
1561
|
+
release = lock.close;
|
|
1562
|
+
if (waitReason !== void 0) logger.info("Codex adapter took over: the other adapter has stopped");
|
|
1563
|
+
return true;
|
|
1564
|
+
}
|
|
1565
|
+
if (!supervised) {
|
|
1566
|
+
if (lock.kind === "held") {
|
|
1567
|
+
logger.info(`Codex adapter already running (PID: ${lock.pid})`);
|
|
1568
|
+
send({ type: "codex-already-running", pid: lock.pid }, () => process.exit(0));
|
|
1569
|
+
} else {
|
|
1570
|
+
logger.error(`Codex adapter cannot start: ${lock.error}`);
|
|
1571
|
+
send({ type: "codex-lock-failed", error: lock.error }, () => process.exit(1));
|
|
1572
|
+
}
|
|
1573
|
+
return false;
|
|
1574
|
+
}
|
|
1575
|
+
const reason = lock.kind === "held" ? `held:${lock.pid}` : `unknown:${lock.error}`;
|
|
1576
|
+
if (reason !== waitReason) {
|
|
1577
|
+
waitReason = reason;
|
|
1578
|
+
if (lock.kind === "held") logger.info(`Another Codex adapter is running (PID: ${lock.pid}); this one takes over when it stops`);
|
|
1579
|
+
else logger.warn(`Codex adapter control endpoint is unavailable (${lock.error}); retrying`);
|
|
1580
|
+
}
|
|
1581
|
+
await sleep(delay);
|
|
1582
|
+
delay = Math.min(delay * 2, WAIT_MAX_MS);
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
void takeLock().then((owned) => {
|
|
1586
|
+
if (!owned) return;
|
|
1587
|
+
logStartup("Codex adapter");
|
|
1588
|
+
const { host, port, token } = resolveAdapterHub(config);
|
|
1589
|
+
adapter = new CodexAdapter({
|
|
1590
|
+
command: config.codex?.command ?? "codex",
|
|
1591
|
+
hub: { host, port, token },
|
|
1592
|
+
onFirstConnect: (outcome) => send({ type: "codex-first-connect", ...outcome })
|
|
1593
|
+
});
|
|
1594
|
+
adapter.start();
|
|
1595
|
+
});
|
|
1230
1596
|
//# sourceMappingURL=main.js.map
|