@delt/claude-alarm 1.3.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/dist/cli.js +178 -49
- package/dist/cli.js.map +1 -1
- package/dist/codex/main.js +161 -32
- package/dist/codex/main.js.map +1 -1
- package/dist/dashboard/index.html +77 -46
- package/dist/hub/server.js +56 -31
- package/dist/hub/server.js.map +1 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.js +56 -31
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/dashboard/index.html +77 -46
package/dist/codex/main.js
CHANGED
|
@@ -357,7 +357,7 @@ var HubClient = class {
|
|
|
357
357
|
};
|
|
358
358
|
|
|
359
359
|
// src/codex/transport.ts
|
|
360
|
-
import { spawn } from "child_process";
|
|
360
|
+
import { execFile, spawn } from "child_process";
|
|
361
361
|
import fs3 from "fs";
|
|
362
362
|
import path3 from "path";
|
|
363
363
|
import { Duplex } from "stream";
|
|
@@ -387,14 +387,116 @@ function findCodex(command, platform = process.platform, env = process.env) {
|
|
|
387
387
|
];
|
|
388
388
|
return candidates.find((file) => !!file && fs3.existsSync(file));
|
|
389
389
|
}
|
|
390
|
+
var shellSpawns = /* @__PURE__ */ new WeakSet();
|
|
391
|
+
var spawnedThroughShell = (child) => shellSpawns.has(child);
|
|
390
392
|
var defaultSpawn = (command, args) => {
|
|
391
393
|
const { file, shell } = resolveCommand(command);
|
|
392
|
-
if (shell) return spawn(
|
|
393
|
-
|
|
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;
|
|
394
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));
|
|
419
|
+
};
|
|
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();
|
|
395
496
|
var HANDSHAKE_TIMEOUT_MS = 1e4;
|
|
396
|
-
function connectProxy(command, spawnFn = defaultSpawn, timeoutMs = HANDSHAKE_TIMEOUT_MS) {
|
|
497
|
+
function connectProxy(command, spawnFn = defaultSpawn, timeoutMs = HANDSHAKE_TIMEOUT_MS, killTreeFn = killTree) {
|
|
397
498
|
const child = spawnFn(command, ["app-server", "proxy"]);
|
|
499
|
+
void killTreeFn.track?.(child);
|
|
398
500
|
child.stdin?.on("error", () => {
|
|
399
501
|
});
|
|
400
502
|
child.stderr?.on("data", (d) => logger.debug(`codex proxy: ${String(d).trim()}`));
|
|
@@ -421,10 +523,12 @@ function connectProxy(command, spawnFn = defaultSpawn, timeoutMs = HANDSHAKE_TIM
|
|
|
421
523
|
const ws = new WebSocket3("ws://localhost/", {
|
|
422
524
|
createConnection: (() => stream)
|
|
423
525
|
});
|
|
424
|
-
|
|
526
|
+
let closing;
|
|
527
|
+
const close = () => closing ??= (async () => {
|
|
425
528
|
ws.terminate();
|
|
426
|
-
child.
|
|
427
|
-
|
|
529
|
+
child.stdin?.end();
|
|
530
|
+
await killTreeFn(child);
|
|
531
|
+
})();
|
|
428
532
|
return new Promise((resolve, reject) => {
|
|
429
533
|
let settled = false;
|
|
430
534
|
const timer = setTimeout(() => fail(new Error(`codex daemon did not answer within ${timeoutMs / 1e3}s`)), timeoutMs);
|
|
@@ -432,8 +536,7 @@ function connectProxy(command, spawnFn = defaultSpawn, timeoutMs = HANDSHAKE_TIM
|
|
|
432
536
|
if (settled) return;
|
|
433
537
|
settled = true;
|
|
434
538
|
clearTimeout(timer);
|
|
435
|
-
close();
|
|
436
|
-
reject(err);
|
|
539
|
+
void close().then(() => reject(err), () => reject(err));
|
|
437
540
|
};
|
|
438
541
|
child.on("error", fail);
|
|
439
542
|
child.once("exit", (code) => fail(new Error(`codex proxy exited (code ${code})`)));
|
|
@@ -444,6 +547,7 @@ function connectProxy(command, spawnFn = defaultSpawn, timeoutMs = HANDSHAKE_TIM
|
|
|
444
547
|
clearTimeout(timer);
|
|
445
548
|
child.on("error", (err) => logger.warn(`codex proxy error: ${err.message}`));
|
|
446
549
|
ws.on("error", (err) => logger.warn(`codex daemon socket error: ${err.message}`));
|
|
550
|
+
void killTreeFn.track?.(child);
|
|
447
551
|
resolve({ ws, close });
|
|
448
552
|
});
|
|
449
553
|
});
|
|
@@ -653,6 +757,8 @@ var CodexAdapter = class {
|
|
|
653
757
|
this.link = new CodexHubLink(opts.hub, { id: randomUUID2(), host: this.hostName }, (call) => this.onCall(call), opts.linkReconnectMs);
|
|
654
758
|
}
|
|
655
759
|
conn;
|
|
760
|
+
connecting;
|
|
761
|
+
closing = /* @__PURE__ */ new Set();
|
|
656
762
|
rpc;
|
|
657
763
|
threads = /* @__PURE__ */ new Map();
|
|
658
764
|
approvals = /* @__PURE__ */ new Map();
|
|
@@ -668,22 +774,29 @@ var CodexAdapter = class {
|
|
|
668
774
|
start() {
|
|
669
775
|
this.stopped = false;
|
|
670
776
|
this.link.connect();
|
|
671
|
-
|
|
777
|
+
this.connecting = this.connect();
|
|
672
778
|
}
|
|
673
|
-
stop() {
|
|
779
|
+
async stop() {
|
|
674
780
|
this.stopped = true;
|
|
675
781
|
this.link.disconnect();
|
|
676
782
|
if (this.retryTimer) clearTimeout(this.retryTimer);
|
|
677
783
|
this.retryTimer = void 0;
|
|
678
784
|
for (const id of [...this.threads.keys()]) this.drop(id);
|
|
679
|
-
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);
|
|
680
793
|
}
|
|
681
794
|
async connect() {
|
|
682
795
|
let rpc;
|
|
683
796
|
try {
|
|
684
797
|
const conn = await connectProxy(this.opts.command, this.opts.spawnFn);
|
|
685
798
|
if (this.stopped) {
|
|
686
|
-
|
|
799
|
+
this.closeConn(conn);
|
|
687
800
|
return;
|
|
688
801
|
}
|
|
689
802
|
const live = new RpcClient(conn.ws, this.opts.rpcTimeoutMs);
|
|
@@ -715,7 +828,7 @@ var CodexAdapter = class {
|
|
|
715
828
|
});
|
|
716
829
|
if (rpc && this.rpc === rpc) {
|
|
717
830
|
this.rpc = void 0;
|
|
718
|
-
this.conn
|
|
831
|
+
this.closeConn(this.conn);
|
|
719
832
|
this.conn = void 0;
|
|
720
833
|
}
|
|
721
834
|
for (const id of [...this.threads.keys()]) this.drop(id);
|
|
@@ -725,7 +838,7 @@ var CodexAdapter = class {
|
|
|
725
838
|
onDaemonLost() {
|
|
726
839
|
this.setReady(false);
|
|
727
840
|
this.rpc = void 0;
|
|
728
|
-
this.conn
|
|
841
|
+
this.closeConn(this.conn);
|
|
729
842
|
this.conn = void 0;
|
|
730
843
|
for (const id of [...this.threads.keys()]) this.drop(id);
|
|
731
844
|
if (!this.stopped) logger.warn("Codex daemon connection lost");
|
|
@@ -735,7 +848,7 @@ var CodexAdapter = class {
|
|
|
735
848
|
if (this.stopped || this.retryTimer) return;
|
|
736
849
|
this.retryTimer = setTimeout(() => {
|
|
737
850
|
this.retryTimer = void 0;
|
|
738
|
-
|
|
851
|
+
this.connecting = this.connect();
|
|
739
852
|
}, this.delay);
|
|
740
853
|
this.delay = Math.min(this.delay * 2, this.opts.reconnectMaxMs ?? 6e4);
|
|
741
854
|
}
|
|
@@ -1028,9 +1141,9 @@ var CodexAdapter = class {
|
|
|
1028
1141
|
}
|
|
1029
1142
|
onHubMessage(threadId, msg) {
|
|
1030
1143
|
if (msg.type === "message_to_session") {
|
|
1031
|
-
this.enqueue(threadId, async () => textInput(msg.content, msg.source));
|
|
1144
|
+
this.enqueue(threadId, async () => textInput(msg.content, msg.source), msg.source);
|
|
1032
1145
|
} else if (msg.type === "image_to_session") {
|
|
1033
|
-
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);
|
|
1034
1147
|
} else if (msg.type === "permission_response") {
|
|
1035
1148
|
this.answer(threadId, msg.requestId, msg.choiceId);
|
|
1036
1149
|
} else if (msg.type === "codex_close") {
|
|
@@ -1038,16 +1151,16 @@ var CodexAdapter = class {
|
|
|
1038
1151
|
}
|
|
1039
1152
|
}
|
|
1040
1153
|
// One message at a time per conversation, so a message right behind another sees the turn the first one started.
|
|
1041
|
-
enqueue(threadId, build) {
|
|
1154
|
+
enqueue(threadId, build, source) {
|
|
1042
1155
|
const t = this.threads.get(threadId);
|
|
1043
1156
|
if (!t) return;
|
|
1044
|
-
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}`));
|
|
1045
1158
|
}
|
|
1046
|
-
async deliver(t, build) {
|
|
1159
|
+
async deliver(t, build, source) {
|
|
1047
1160
|
const threadId = t.thread.id;
|
|
1048
1161
|
if (this.threads.get(threadId) !== t) return;
|
|
1049
|
-
if (
|
|
1050
|
-
this.
|
|
1162
|
+
if (this.waitingForAnswer(t)) {
|
|
1163
|
+
this.refuseWhileWaiting(threadId);
|
|
1051
1164
|
return;
|
|
1052
1165
|
}
|
|
1053
1166
|
let input;
|
|
@@ -1068,20 +1181,37 @@ var CodexAdapter = class {
|
|
|
1068
1181
|
const unsubscribed = !t.subscribed;
|
|
1069
1182
|
const running = await this.runningTurn(rpc, threadId);
|
|
1070
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
|
+
}
|
|
1071
1189
|
if (unsubscribed) t.unrelayed = true;
|
|
1072
1190
|
if (running) {
|
|
1073
1191
|
await rpc.request("turn/steer", { threadId, expectedTurnId: running, input });
|
|
1074
|
-
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);
|
|
1075
1193
|
return;
|
|
1076
1194
|
}
|
|
1077
1195
|
await rpc.request("turn/start", { threadId, input });
|
|
1078
1196
|
} catch (err) {
|
|
1079
|
-
t
|
|
1080
|
-
t.unrelayed = wasUnrelayed;
|
|
1081
|
-
this.releaseLater(t);
|
|
1197
|
+
this.abandonDelivery(t, wasUnrelayed);
|
|
1082
1198
|
this.notify(threadId, "Not delivered", `Codex rejected the message: ${err.message}`, "warning");
|
|
1083
1199
|
}
|
|
1084
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
|
+
}
|
|
1085
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.
|
|
1086
1216
|
async runningTurn(rpc, threadId) {
|
|
1087
1217
|
try {
|
|
@@ -1161,8 +1291,8 @@ var CodexAdapter = class {
|
|
|
1161
1291
|
this.approvals.delete(requestId);
|
|
1162
1292
|
this.threads.get(a.threadId)?.hub.send({ type: "permission_resolved", sessionId: codexSessionId(a.threadId), requestId, state });
|
|
1163
1293
|
}
|
|
1164
|
-
notify(threadId, title, message, level) {
|
|
1165
|
-
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 } : {} });
|
|
1166
1296
|
}
|
|
1167
1297
|
};
|
|
1168
1298
|
|
|
@@ -1392,11 +1522,10 @@ var exiting = false;
|
|
|
1392
1522
|
var shutdown = () => {
|
|
1393
1523
|
if (exiting) return;
|
|
1394
1524
|
exiting = true;
|
|
1395
|
-
adapter?.stop();
|
|
1525
|
+
const stopped = adapter?.stop();
|
|
1396
1526
|
const exit = () => process.exit(0);
|
|
1397
1527
|
setTimeout(exit, 3e3).unref();
|
|
1398
|
-
|
|
1399
|
-
else exit();
|
|
1528
|
+
Promise.all([stopped, release?.()]).then(exit, exit);
|
|
1400
1529
|
};
|
|
1401
1530
|
process.on("SIGINT", shutdown);
|
|
1402
1531
|
process.on("SIGTERM", shutdown);
|