@delt/claude-alarm 1.1.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -10
- package/dist/channel/server.js.map +1 -1
- package/dist/cli.js +906 -67
- package/dist/cli.js.map +1 -1
- package/dist/codex/main.js +380 -63
- package/dist/codex/main.js.map +1 -1
- package/dist/dashboard/index.html +162 -1
- package/dist/hub/server.js +165 -8
- package/dist/hub/server.js.map +1 -1
- package/dist/index.d.ts +60 -2
- package/dist/index.js +166 -8
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/dashboard/index.html +162 -1
package/dist/codex/main.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/codex/main.ts
|
|
4
|
-
import
|
|
4
|
+
import fs6 from "fs";
|
|
5
5
|
|
|
6
6
|
// src/shared/config.ts
|
|
7
7
|
import fs from "fs";
|
|
@@ -21,6 +21,7 @@ var UPLOADS_DIR = path.join(CONFIG_DIR, "uploads");
|
|
|
21
21
|
var CODEX_PID_FILE = path.join(CONFIG_DIR, "codex.pid");
|
|
22
22
|
var CODEX_LOG_FILE = path.join(CONFIG_DIR, "codex.log");
|
|
23
23
|
var WS_PATH_CHANNEL = "/ws/channel";
|
|
24
|
+
var WS_PATH_CODEX = "/ws/codex";
|
|
24
25
|
var CHANNEL_SERVER_VERSION = "0.1.0";
|
|
25
26
|
|
|
26
27
|
// src/shared/config.ts
|
|
@@ -133,11 +134,115 @@ function logStartup(context) {
|
|
|
133
134
|
`);
|
|
134
135
|
}
|
|
135
136
|
|
|
137
|
+
// src/codex/adapter.ts
|
|
138
|
+
import fs5 from "fs";
|
|
139
|
+
import os2 from "os";
|
|
140
|
+
import path4 from "path";
|
|
141
|
+
|
|
142
|
+
// src/codex/hub-link.ts
|
|
143
|
+
import WebSocket from "ws";
|
|
144
|
+
var CodexHubLink = class {
|
|
145
|
+
constructor(hub, identity, onCall, reconnectMs = 5e3, pingTimeoutMs = 75e3) {
|
|
146
|
+
this.hub = hub;
|
|
147
|
+
this.identity = identity;
|
|
148
|
+
this.onCall = onCall;
|
|
149
|
+
this.reconnectMs = reconnectMs;
|
|
150
|
+
this.pingTimeoutMs = pingTimeoutMs;
|
|
151
|
+
}
|
|
152
|
+
ws = null;
|
|
153
|
+
retryTimer = null;
|
|
154
|
+
pingTimer = null;
|
|
155
|
+
closed = true;
|
|
156
|
+
ready = false;
|
|
157
|
+
connect() {
|
|
158
|
+
this.closed = false;
|
|
159
|
+
const query = this.hub.token ? `?token=${encodeURIComponent(this.hub.token)}` : "";
|
|
160
|
+
let ws;
|
|
161
|
+
try {
|
|
162
|
+
ws = new WebSocket(`ws://${this.hub.host}:${this.hub.port}${WS_PATH_CODEX}${query}`);
|
|
163
|
+
} catch (err) {
|
|
164
|
+
logger.debug(`Codex hub link failed: ${err.message}`);
|
|
165
|
+
this.scheduleReconnect();
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
this.ws = ws;
|
|
169
|
+
ws.on("open", () => {
|
|
170
|
+
if (this.ws !== ws) return;
|
|
171
|
+
this.armWatchdog(ws);
|
|
172
|
+
this.hello();
|
|
173
|
+
});
|
|
174
|
+
ws.on("ping", () => this.armWatchdog(ws));
|
|
175
|
+
ws.on("message", (data) => {
|
|
176
|
+
if (this.ws === ws) this.onMessage(String(data));
|
|
177
|
+
});
|
|
178
|
+
ws.on("close", () => {
|
|
179
|
+
if (this.ws !== ws) return;
|
|
180
|
+
this.clearWatchdog();
|
|
181
|
+
this.ws = null;
|
|
182
|
+
this.scheduleReconnect();
|
|
183
|
+
});
|
|
184
|
+
ws.on("error", (err) => logger.debug(`Codex hub link error: ${err.message}`));
|
|
185
|
+
}
|
|
186
|
+
setReady(ready) {
|
|
187
|
+
if (this.ready === ready) return;
|
|
188
|
+
this.ready = ready;
|
|
189
|
+
this.hello();
|
|
190
|
+
}
|
|
191
|
+
disconnect() {
|
|
192
|
+
this.closed = true;
|
|
193
|
+
this.clearWatchdog();
|
|
194
|
+
if (this.retryTimer) clearTimeout(this.retryTimer);
|
|
195
|
+
this.retryTimer = null;
|
|
196
|
+
const ws = this.ws;
|
|
197
|
+
this.ws = null;
|
|
198
|
+
ws?.close();
|
|
199
|
+
}
|
|
200
|
+
hello() {
|
|
201
|
+
this.send({ type: "adapter_hello", adapter: { ...this.identity, ready: this.ready } });
|
|
202
|
+
}
|
|
203
|
+
send(msg) {
|
|
204
|
+
if (this.ws?.readyState === WebSocket.OPEN) this.ws.send(JSON.stringify(msg));
|
|
205
|
+
}
|
|
206
|
+
onMessage(text) {
|
|
207
|
+
let msg;
|
|
208
|
+
try {
|
|
209
|
+
msg = JSON.parse(text);
|
|
210
|
+
} catch {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (!msg || typeof msg !== "object") return;
|
|
214
|
+
if (msg.type !== "adapter_call") return;
|
|
215
|
+
const { requestId, call } = msg;
|
|
216
|
+
this.onCall(call).then(
|
|
217
|
+
(data) => this.send({ type: "adapter_result", requestId, ok: true, data }),
|
|
218
|
+
(err) => this.send({ type: "adapter_result", requestId, ok: false, error: err.message })
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
clearWatchdog() {
|
|
222
|
+
if (this.pingTimer) clearTimeout(this.pingTimer);
|
|
223
|
+
this.pingTimer = null;
|
|
224
|
+
}
|
|
225
|
+
armWatchdog(ws) {
|
|
226
|
+
if (this.ws !== ws) return;
|
|
227
|
+
this.clearWatchdog();
|
|
228
|
+
this.pingTimer = setTimeout(() => {
|
|
229
|
+
if (this.ws === ws) ws.terminate();
|
|
230
|
+
}, this.pingTimeoutMs);
|
|
231
|
+
}
|
|
232
|
+
scheduleReconnect() {
|
|
233
|
+
if (this.closed || this.retryTimer) return;
|
|
234
|
+
this.retryTimer = setTimeout(() => {
|
|
235
|
+
this.retryTimer = null;
|
|
236
|
+
this.connect();
|
|
237
|
+
}, this.reconnectMs);
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
|
|
136
241
|
// src/codex/adapter.ts
|
|
137
242
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
138
243
|
|
|
139
244
|
// src/channel/hub-client.ts
|
|
140
|
-
import
|
|
245
|
+
import WebSocket2 from "ws";
|
|
141
246
|
var HubClient = class {
|
|
142
247
|
constructor(sessionId, sessionName, hubHost = DEFAULT_HUB_HOST, hubPort = DEFAULT_HUB_PORT, token2, getPeerName = () => void 0, getRegistration = () => ({})) {
|
|
143
248
|
this.sessionId = sessionId;
|
|
@@ -160,7 +265,7 @@ var HubClient = class {
|
|
|
160
265
|
const url = `ws://${this.hubHost}:${this.hubPort}${WS_PATH_CHANNEL}${tokenQuery}`;
|
|
161
266
|
logger.debug(`Connecting to hub at ${url}`);
|
|
162
267
|
try {
|
|
163
|
-
const ws = new
|
|
268
|
+
const ws = new WebSocket2(url);
|
|
164
269
|
this.ws = ws;
|
|
165
270
|
ws.on("open", () => {
|
|
166
271
|
if (this.ws !== ws) return;
|
|
@@ -200,7 +305,7 @@ var HubClient = class {
|
|
|
200
305
|
}
|
|
201
306
|
}
|
|
202
307
|
send(msg) {
|
|
203
|
-
if (this.connected && this.ws?.readyState ===
|
|
308
|
+
if (this.connected && this.ws?.readyState === WebSocket2.OPEN) {
|
|
204
309
|
this.ws.send(JSON.stringify(msg));
|
|
205
310
|
} else {
|
|
206
311
|
if (this.queue.length < 100) {
|
|
@@ -225,7 +330,7 @@ var HubClient = class {
|
|
|
225
330
|
this.connected = false;
|
|
226
331
|
}
|
|
227
332
|
reregister() {
|
|
228
|
-
if (this.connected && this.ws?.readyState ===
|
|
333
|
+
if (this.connected && this.ws?.readyState === WebSocket2.OPEN) {
|
|
229
334
|
this.ws.send(JSON.stringify(this.registration()));
|
|
230
335
|
}
|
|
231
336
|
}
|
|
@@ -259,10 +364,10 @@ import { spawn } from "child_process";
|
|
|
259
364
|
import fs3 from "fs";
|
|
260
365
|
import path3 from "path";
|
|
261
366
|
import { Duplex } from "stream";
|
|
262
|
-
import
|
|
367
|
+
import WebSocket3 from "ws";
|
|
263
368
|
function resolveCommand(command, platform = process.platform, env = process.env) {
|
|
264
369
|
if (platform !== "win32" || /[\\/]/.test(command) || path3.extname(command)) return { file: command, shell: false };
|
|
265
|
-
const file =
|
|
370
|
+
const file = findCodex(command, platform, env);
|
|
266
371
|
return file ? { file, shell: file.endsWith(".cmd") } : { file: command, shell: false };
|
|
267
372
|
}
|
|
268
373
|
function findOnPath(command, platform = process.platform, env = process.env) {
|
|
@@ -275,6 +380,15 @@ function findOnPath(command, platform = process.platform, env = process.env) {
|
|
|
275
380
|
}
|
|
276
381
|
return void 0;
|
|
277
382
|
}
|
|
383
|
+
function findCodex(command, platform = process.platform, env = process.env) {
|
|
384
|
+
const onPath = findOnPath(command, platform, env);
|
|
385
|
+
if (onPath || platform !== "win32") return onPath;
|
|
386
|
+
const candidates = [
|
|
387
|
+
env.LOCALAPPDATA && path3.join(env.LOCALAPPDATA, "Programs", "OpenAI", "Codex", "bin", `${command}.exe`),
|
|
388
|
+
env.APPDATA && path3.join(env.APPDATA, "npm", `${command}.cmd`)
|
|
389
|
+
];
|
|
390
|
+
return candidates.find((file) => !!file && fs3.existsSync(file));
|
|
391
|
+
}
|
|
278
392
|
var defaultSpawn = (command, args) => {
|
|
279
393
|
const { file, shell } = resolveCommand(command);
|
|
280
394
|
if (shell) return spawn(`"${file}" ${args.join(" ")}`, { stdio: "pipe", windowsHide: true, shell: true });
|
|
@@ -305,7 +419,7 @@ function connectProxy(command, spawnFn = defaultSpawn) {
|
|
|
305
419
|
}, ref() {
|
|
306
420
|
}, unref() {
|
|
307
421
|
} });
|
|
308
|
-
const ws = new
|
|
422
|
+
const ws = new WebSocket3("ws://localhost/", {
|
|
309
423
|
createConnection: (() => stream)
|
|
310
424
|
});
|
|
311
425
|
const close = () => {
|
|
@@ -333,6 +447,57 @@ function connectProxy(command, spawnFn = defaultSpawn) {
|
|
|
333
447
|
});
|
|
334
448
|
}
|
|
335
449
|
|
|
450
|
+
// src/codex/inputs.ts
|
|
451
|
+
import fs4 from "fs/promises";
|
|
452
|
+
|
|
453
|
+
// src/codex/mapping.ts
|
|
454
|
+
var TITLE_MAX = 30;
|
|
455
|
+
var SOURCE_LABEL = { dashboard: "Dashboard", telegram: "Telegram", api: "API" };
|
|
456
|
+
var SOURCE_PREFIX = new RegExp(`^\\[claude-alarm(?: \xB7 (?:${Object.values(SOURCE_LABEL).join("|")}))?\\] ?`);
|
|
457
|
+
function codexSessionId(threadId) {
|
|
458
|
+
return `codex:${threadId}`;
|
|
459
|
+
}
|
|
460
|
+
function isTrackable(thread) {
|
|
461
|
+
return !thread.parentThreadId && !thread.ephemeral && thread.status.type !== "notLoaded";
|
|
462
|
+
}
|
|
463
|
+
function threadTitle(thread) {
|
|
464
|
+
const name = thread.name?.trim();
|
|
465
|
+
if (name) return name;
|
|
466
|
+
const preview = (thread.preview ?? "").replace(/\s+/g, " ").trim().replace(SOURCE_PREFIX, "").trim();
|
|
467
|
+
if (preview) return preview.length > TITLE_MAX ? `${preview.slice(0, TITLE_MAX)}\u2026` : preview;
|
|
468
|
+
return thread.cwd.replace(/^.*[/\\]/, "") || thread.id.slice(0, 8);
|
|
469
|
+
}
|
|
470
|
+
function hubStatus(status) {
|
|
471
|
+
if (status.type !== "active") return "idle";
|
|
472
|
+
return status.activeFlags.some((f) => f === "waitingOnApproval" || f === "waitingOnUserInput") ? "waiting_input" : "working";
|
|
473
|
+
}
|
|
474
|
+
function withSourcePrefix(content, source) {
|
|
475
|
+
return source ? `[claude-alarm \xB7 ${SOURCE_LABEL[source]}] ${content}` : `[claude-alarm] ${content}`;
|
|
476
|
+
}
|
|
477
|
+
function finalAnswer(messages) {
|
|
478
|
+
const finals = messages.filter((m) => m.phase === "final_answer").map((m) => m.text);
|
|
479
|
+
if (finals.length) return finals.join("\n\n");
|
|
480
|
+
return messages.at(-1)?.text ?? null;
|
|
481
|
+
}
|
|
482
|
+
function cleanFolder(input) {
|
|
483
|
+
const s = input.trim();
|
|
484
|
+
return s.length >= 2 && s.startsWith('"') && s.endsWith('"') ? s.slice(1, -1).trim() : s;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// src/codex/inputs.ts
|
|
488
|
+
var IMAGE_TYPES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
|
|
489
|
+
function textInput(content, source) {
|
|
490
|
+
return [{ type: "text", text: withSourcePrefix(content, source) }];
|
|
491
|
+
}
|
|
492
|
+
async function imageInput(imagePath, mimeType, caption, source) {
|
|
493
|
+
if (!IMAGE_TYPES.has(mimeType)) throw new Error(`unsupported image type ${mimeType}`);
|
|
494
|
+
const data = await fs4.readFile(imagePath);
|
|
495
|
+
return [
|
|
496
|
+
{ type: "text", text: withSourcePrefix(caption?.trim() || "(image)", source) },
|
|
497
|
+
{ type: "image", url: `data:${mimeType};base64,${data.toString("base64")}` }
|
|
498
|
+
];
|
|
499
|
+
}
|
|
500
|
+
|
|
336
501
|
// src/codex/rpc.ts
|
|
337
502
|
import { EventEmitter } from "events";
|
|
338
503
|
var RpcError = class extends Error {
|
|
@@ -358,14 +523,14 @@ var RpcClient = class extends EventEmitter {
|
|
|
358
523
|
}
|
|
359
524
|
nextId = 1;
|
|
360
525
|
pending = /* @__PURE__ */ new Map();
|
|
361
|
-
request(method, params) {
|
|
526
|
+
request(method, params, timeoutMs = this.timeoutMs) {
|
|
362
527
|
if (this.ws.readyState !== this.ws.OPEN) return Promise.reject(new Error("connection closed"));
|
|
363
528
|
const id = this.nextId++;
|
|
364
529
|
return new Promise((resolve, reject) => {
|
|
365
|
-
const timer = setTimeout(() => {
|
|
530
|
+
const timer = timeoutMs === null ? void 0 : setTimeout(() => {
|
|
366
531
|
this.pending.delete(id);
|
|
367
532
|
reject(new Error(`${method} timed out`));
|
|
368
|
-
},
|
|
533
|
+
}, timeoutMs);
|
|
369
534
|
this.pending.set(id, { resolve, reject, timer });
|
|
370
535
|
this.ws.send(JSON.stringify(params === void 0 ? { id, method } : { id, method, params }));
|
|
371
536
|
});
|
|
@@ -476,41 +641,14 @@ ${f.diff}` : f.path).join("\n\n") : "The file list is not available here. Check
|
|
|
476
641
|
}
|
|
477
642
|
}
|
|
478
643
|
|
|
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
644
|
// src/codex/adapter.ts
|
|
509
645
|
var USER_REQUESTS = /* @__PURE__ */ new Set(["item/tool/requestUserInput", "item/permissions/requestApproval", "mcpServer/elicitation/request"]);
|
|
510
646
|
var CodexAdapter = class {
|
|
511
647
|
constructor(opts) {
|
|
512
648
|
this.opts = opts;
|
|
513
649
|
this.delay = opts.reconnectMinMs ?? 2e3;
|
|
650
|
+
this.hostName = opts.hostName ?? os2.hostname();
|
|
651
|
+
this.link = new CodexHubLink(opts.hub, { id: randomUUID2(), host: this.hostName }, (call) => this.onCall(call), opts.linkReconnectMs);
|
|
514
652
|
}
|
|
515
653
|
conn;
|
|
516
654
|
rpc;
|
|
@@ -519,12 +657,20 @@ var CodexAdapter = class {
|
|
|
519
657
|
stopped = false;
|
|
520
658
|
retryTimer;
|
|
521
659
|
delay;
|
|
660
|
+
notFoundNotice = "unsent";
|
|
661
|
+
firstConnectReported = false;
|
|
662
|
+
pins = /* @__PURE__ */ new Set();
|
|
663
|
+
ready = false;
|
|
664
|
+
hostName;
|
|
665
|
+
link;
|
|
522
666
|
start() {
|
|
523
667
|
this.stopped = false;
|
|
668
|
+
this.link.connect();
|
|
524
669
|
void this.connect();
|
|
525
670
|
}
|
|
526
671
|
stop() {
|
|
527
672
|
this.stopped = true;
|
|
673
|
+
this.link.disconnect();
|
|
528
674
|
if (this.retryTimer) clearTimeout(this.retryTimer);
|
|
529
675
|
this.retryTimer = void 0;
|
|
530
676
|
for (const id of [...this.threads.keys()]) this.drop(id);
|
|
@@ -538,7 +684,7 @@ var CodexAdapter = class {
|
|
|
538
684
|
conn.close();
|
|
539
685
|
return;
|
|
540
686
|
}
|
|
541
|
-
const live = new RpcClient(conn.ws);
|
|
687
|
+
const live = new RpcClient(conn.ws, this.opts.rpcTimeoutMs);
|
|
542
688
|
rpc = live;
|
|
543
689
|
live.on("notification", (method, params) => this.onNotification(method, params));
|
|
544
690
|
live.on("request", (id, method, params) => this.onServerRequest(id, method, params));
|
|
@@ -552,10 +698,19 @@ var CodexAdapter = class {
|
|
|
552
698
|
});
|
|
553
699
|
live.notify("initialized");
|
|
554
700
|
logger.info(`Connected to Codex daemon (${init.userAgent ?? "unknown version"})`);
|
|
701
|
+
this.reportFirstConnect({ connected: true, ...init.userAgent ? { userAgent: init.userAgent } : {} });
|
|
555
702
|
await this.discover();
|
|
703
|
+
if (this.rpc === live) this.setReady(true);
|
|
556
704
|
this.delay = this.opts.reconnectMinMs ?? 2e3;
|
|
557
705
|
} catch (err) {
|
|
706
|
+
this.setReady(false);
|
|
558
707
|
logger.warn(`Codex daemon connection failed: ${err.message}`);
|
|
708
|
+
if (err.code === "ENOENT") this.noticeNotFound();
|
|
709
|
+
this.reportFirstConnect({
|
|
710
|
+
connected: false,
|
|
711
|
+
error: err.message,
|
|
712
|
+
notFound: err.code === "ENOENT"
|
|
713
|
+
});
|
|
559
714
|
if (rpc && this.rpc === rpc) {
|
|
560
715
|
this.rpc = void 0;
|
|
561
716
|
this.conn?.close();
|
|
@@ -566,6 +721,7 @@ var CodexAdapter = class {
|
|
|
566
721
|
}
|
|
567
722
|
}
|
|
568
723
|
onDaemonLost() {
|
|
724
|
+
this.setReady(false);
|
|
569
725
|
this.rpc = void 0;
|
|
570
726
|
this.conn?.close();
|
|
571
727
|
this.conn = void 0;
|
|
@@ -581,18 +737,86 @@ var CodexAdapter = class {
|
|
|
581
737
|
}, this.delay);
|
|
582
738
|
this.delay = Math.min(this.delay * 2, this.opts.reconnectMaxMs ?? 6e4);
|
|
583
739
|
}
|
|
740
|
+
reportFirstConnect(outcome) {
|
|
741
|
+
if (this.firstConnectReported || this.stopped) return;
|
|
742
|
+
this.firstConnectReported = true;
|
|
743
|
+
try {
|
|
744
|
+
this.opts.onFirstConnect?.(outcome);
|
|
745
|
+
} catch (err) {
|
|
746
|
+
logger.debug(`First-connect observer failed: ${err.message}`);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
noticeNotFound() {
|
|
750
|
+
if (this.notFoundNotice !== "unsent") return;
|
|
751
|
+
this.notFoundNotice = "sending";
|
|
752
|
+
const { host: host2, port: port2, token: token2 } = this.opts.hub;
|
|
753
|
+
fetch(`http://${host2}:${port2}/api/notify`, {
|
|
754
|
+
method: "POST",
|
|
755
|
+
headers: { "Content-Type": "application/json", ...token2 ? { Authorization: `Bearer ${token2}` } : {} },
|
|
756
|
+
body: JSON.stringify({
|
|
757
|
+
title: "Codex not found",
|
|
758
|
+
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.`,
|
|
759
|
+
level: "warning"
|
|
760
|
+
}),
|
|
761
|
+
signal: AbortSignal.timeout(this.opts.noticeTimeoutMs ?? 5e3)
|
|
762
|
+
}).then((res) => {
|
|
763
|
+
this.notFoundNotice = res.ok ? "sent" : "unsent";
|
|
764
|
+
if (!res.ok) logger.debug(`Codex not-found notice was refused (${res.status})`);
|
|
765
|
+
}).catch((err) => {
|
|
766
|
+
this.notFoundNotice = err.name === "TimeoutError" ? "sent" : "unsent";
|
|
767
|
+
logger.debug(`Codex not-found notice failed: ${err.message}`);
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
setReady(ready) {
|
|
771
|
+
this.ready = ready;
|
|
772
|
+
this.link.setReady(ready);
|
|
773
|
+
}
|
|
774
|
+
requireDaemon() {
|
|
775
|
+
if (!this.ready || !this.rpc) throw new Error(`Codex is not connected on ${this.hostName}.`);
|
|
776
|
+
return this.rpc;
|
|
777
|
+
}
|
|
778
|
+
async onCall(call) {
|
|
779
|
+
if (call.kind === "folders") return { folders: await this.folders() };
|
|
780
|
+
return { sessionId: await this.create(call.cwd) };
|
|
781
|
+
}
|
|
782
|
+
async folders() {
|
|
783
|
+
const page = await this.requireDaemon().request("thread/list", { limit: 50, sortKey: "updated_at" });
|
|
784
|
+
return [...new Set(page.data.map((t) => t.cwd).filter(Boolean))].slice(0, 10);
|
|
785
|
+
}
|
|
786
|
+
async create(input) {
|
|
787
|
+
const rpc = this.requireDaemon();
|
|
788
|
+
const cwd = cleanFolder(input);
|
|
789
|
+
const isDir = path4.isAbsolute(cwd) && await fs5.promises.stat(cwd).then((s) => s.isDirectory(), () => false);
|
|
790
|
+
if (!isDir) throw new Error(`Folder not found on ${this.hostName}: ${cwd}`);
|
|
791
|
+
let thread;
|
|
792
|
+
try {
|
|
793
|
+
({ thread } = await rpc.request("thread/start", { cwd, sandbox: "danger-full-access", approvalPolicy: "never" }, null));
|
|
794
|
+
} catch (err) {
|
|
795
|
+
throw new Error(`Codex could not start the conversation: ${err.message}`);
|
|
796
|
+
}
|
|
797
|
+
this.pins.add(thread.id);
|
|
798
|
+
this.upsert(thread);
|
|
799
|
+
const t = this.threads.get(thread.id);
|
|
800
|
+
if (!t) {
|
|
801
|
+
this.pins.delete(thread.id);
|
|
802
|
+
throw new Error("Codex started a conversation claude-alarm cannot follow.");
|
|
803
|
+
}
|
|
804
|
+
t.subscribed = true;
|
|
805
|
+
return codexSessionId(thread.id);
|
|
806
|
+
}
|
|
584
807
|
async discover() {
|
|
808
|
+
const rpc = this.rpc;
|
|
809
|
+
const pinned = [...this.pins];
|
|
585
810
|
const ids = [];
|
|
586
811
|
let cursor;
|
|
587
812
|
do {
|
|
588
|
-
const page = await
|
|
589
|
-
"thread/loaded/list",
|
|
590
|
-
cursor ? { cursor } : {}
|
|
591
|
-
);
|
|
813
|
+
const page = await rpc.request("thread/loaded/list", cursor ? { cursor } : {});
|
|
592
814
|
ids.push(...page.data);
|
|
593
815
|
cursor = page.nextCursor;
|
|
594
816
|
} while (cursor);
|
|
595
817
|
for (const id of ids) await this.refresh(id);
|
|
818
|
+
if (this.rpc !== rpc) return;
|
|
819
|
+
for (const id of pinned) if (!ids.includes(id)) this.pins.delete(id);
|
|
596
820
|
}
|
|
597
821
|
async refresh(threadId) {
|
|
598
822
|
try {
|
|
@@ -629,6 +853,7 @@ var CodexAdapter = class {
|
|
|
629
853
|
wantSubscribed: false,
|
|
630
854
|
sync: Promise.resolve(),
|
|
631
855
|
pendingTurn: false,
|
|
856
|
+
sending: Promise.resolve(),
|
|
632
857
|
turns: /* @__PURE__ */ new Map(),
|
|
633
858
|
unrelayed: false,
|
|
634
859
|
files: /* @__PURE__ */ new Map()
|
|
@@ -642,12 +867,12 @@ var CodexAdapter = class {
|
|
|
642
867
|
const t = this.threads.get(threadId);
|
|
643
868
|
if (!t) return {};
|
|
644
869
|
const title = threadTitle(t.thread);
|
|
645
|
-
return { name: title, title, cwd: t.thread.cwd, agentKind: "codex", status: hubStatus(t.thread.status) };
|
|
870
|
+
return { name: title, title, cwd: t.thread.cwd, agentKind: "codex", status: hubStatus(t.thread.status), closable: this.pins.has(threadId) };
|
|
646
871
|
}
|
|
647
872
|
want(threadId, subscribed) {
|
|
648
873
|
const t = this.threads.get(threadId);
|
|
649
874
|
if (!t) return Promise.resolve();
|
|
650
|
-
t.wantSubscribed = subscribed;
|
|
875
|
+
t.wantSubscribed = subscribed || this.pins.has(threadId);
|
|
651
876
|
t.sync = t.sync.then(() => this.syncSubscription(t));
|
|
652
877
|
return t.sync;
|
|
653
878
|
}
|
|
@@ -687,6 +912,25 @@ var CodexAdapter = class {
|
|
|
687
912
|
t.releaseTimer = void 0;
|
|
688
913
|
t.hub.disconnect();
|
|
689
914
|
}
|
|
915
|
+
busy(t) {
|
|
916
|
+
return t.thread.status.type === "active" || t.pendingTurn;
|
|
917
|
+
}
|
|
918
|
+
async close(threadId) {
|
|
919
|
+
const t = this.threads.get(threadId);
|
|
920
|
+
if (!t || !this.pins.delete(threadId)) return;
|
|
921
|
+
t.hub.reregister();
|
|
922
|
+
if (this.busy(t)) return;
|
|
923
|
+
await this.want(threadId, false);
|
|
924
|
+
if (this.threads.get(threadId) !== t || this.busy(t) || t.wantSubscribed) return;
|
|
925
|
+
if (!t.subscribed) {
|
|
926
|
+
this.drop(threadId);
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
this.pins.add(threadId);
|
|
930
|
+
t.hub.reregister();
|
|
931
|
+
void this.want(threadId, true);
|
|
932
|
+
this.notify(threadId, "Not closed", "Codex did not release the conversation. Try closing it again.", "warning");
|
|
933
|
+
}
|
|
690
934
|
onNotification(method, params) {
|
|
691
935
|
switch (method) {
|
|
692
936
|
case "thread/started":
|
|
@@ -706,6 +950,7 @@ var CodexAdapter = class {
|
|
|
706
950
|
case "thread/closed":
|
|
707
951
|
case "thread/archived":
|
|
708
952
|
case "thread/deleted":
|
|
953
|
+
this.pins.delete(params.threadId);
|
|
709
954
|
this.drop(params.threadId);
|
|
710
955
|
break;
|
|
711
956
|
case "item/started":
|
|
@@ -730,6 +975,7 @@ var CodexAdapter = class {
|
|
|
730
975
|
return;
|
|
731
976
|
}
|
|
732
977
|
if (status.type === "notLoaded") {
|
|
978
|
+
this.pins.delete(threadId);
|
|
733
979
|
this.drop(threadId);
|
|
734
980
|
return;
|
|
735
981
|
}
|
|
@@ -763,6 +1009,7 @@ var CodexAdapter = class {
|
|
|
763
1009
|
const collected = t.turns.get(turn.id) ?? [];
|
|
764
1010
|
t.turns.delete(turn.id);
|
|
765
1011
|
t.files.clear();
|
|
1012
|
+
if (!t.thread.name?.trim() && !t.thread.preview?.trim()) void this.refresh(threadId);
|
|
766
1013
|
if (t.thread.status.type !== "active") void this.want(threadId, false);
|
|
767
1014
|
if (turn.status === "failed" || turn.error) {
|
|
768
1015
|
this.notify(threadId, "Codex task failed", turn.error?.message ?? "The task ended with an error.", "error");
|
|
@@ -778,33 +1025,75 @@ var CodexAdapter = class {
|
|
|
778
1025
|
}
|
|
779
1026
|
onHubMessage(threadId, msg) {
|
|
780
1027
|
if (msg.type === "message_to_session") {
|
|
781
|
-
|
|
1028
|
+
this.enqueue(threadId, async () => textInput(msg.content, msg.source));
|
|
782
1029
|
} else if (msg.type === "image_to_session") {
|
|
783
|
-
this.
|
|
1030
|
+
this.enqueue(threadId, () => imageInput(msg.imagePath, msg.mimeType, msg.content, msg.source));
|
|
784
1031
|
} else if (msg.type === "permission_response") {
|
|
785
1032
|
this.answer(threadId, msg.requestId, msg.choiceId);
|
|
1033
|
+
} else if (msg.type === "codex_close") {
|
|
1034
|
+
void this.close(threadId);
|
|
786
1035
|
}
|
|
787
1036
|
}
|
|
788
|
-
|
|
1037
|
+
// One message at a time per conversation, so a message right behind another sees the turn the first one started.
|
|
1038
|
+
enqueue(threadId, build) {
|
|
789
1039
|
const t = this.threads.get(threadId);
|
|
790
1040
|
if (!t) return;
|
|
791
|
-
|
|
792
|
-
|
|
1041
|
+
t.sending = t.sending.then(() => this.deliver(t, build)).catch((err) => logger.warn(`Codex delivery for ${threadId} failed: ${err.message}`));
|
|
1042
|
+
}
|
|
1043
|
+
async deliver(t, build) {
|
|
1044
|
+
const threadId = t.thread.id;
|
|
1045
|
+
if (this.threads.get(threadId) !== t) return;
|
|
1046
|
+
if (hubStatus(t.thread.status) === "waiting_input") {
|
|
1047
|
+
this.notify(threadId, "Not delivered", "Codex is waiting for an approval or input. Answer it first, then send the message again.", "warning");
|
|
793
1048
|
return;
|
|
794
1049
|
}
|
|
1050
|
+
let input;
|
|
1051
|
+
try {
|
|
1052
|
+
input = await build();
|
|
1053
|
+
} catch (err) {
|
|
1054
|
+
logger.debug(`Codex input for ${threadId} could not be built: ${err.message}`);
|
|
1055
|
+
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");
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
if (this.threads.get(threadId) !== t) return;
|
|
795
1059
|
t.pendingTurn = true;
|
|
1060
|
+
const wasUnrelayed = t.unrelayed;
|
|
796
1061
|
try {
|
|
797
|
-
|
|
1062
|
+
const rpc = this.rpc;
|
|
1063
|
+
if (!rpc) throw new Error("not connected to the Codex daemon");
|
|
798
1064
|
await this.want(threadId, true);
|
|
799
|
-
|
|
800
|
-
await this.rpc
|
|
1065
|
+
const unsubscribed = !t.subscribed;
|
|
1066
|
+
const running = await this.runningTurn(rpc, threadId);
|
|
1067
|
+
if (this.threads.get(threadId) !== t) return;
|
|
1068
|
+
if (unsubscribed) t.unrelayed = true;
|
|
1069
|
+
if (running) {
|
|
1070
|
+
await rpc.request("turn/steer", { threadId, expectedTurnId: running, input });
|
|
1071
|
+
this.notify(threadId, "Queued", "Queued: Codex will read it after its current step.", "info");
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
await rpc.request("turn/start", { threadId, input });
|
|
801
1075
|
} catch (err) {
|
|
802
1076
|
t.pendingTurn = false;
|
|
803
|
-
t.unrelayed =
|
|
1077
|
+
t.unrelayed = wasUnrelayed;
|
|
804
1078
|
this.releaseLater(t);
|
|
805
1079
|
this.notify(threadId, "Not delivered", `Codex rejected the message: ${err.message}`, "warning");
|
|
806
1080
|
}
|
|
807
1081
|
}
|
|
1082
|
+
// 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
|
+
async runningTurn(rpc, threadId) {
|
|
1084
|
+
try {
|
|
1085
|
+
const page = await rpc.request("thread/turns/list", {
|
|
1086
|
+
threadId,
|
|
1087
|
+
limit: 1,
|
|
1088
|
+
sortDirection: "desc"
|
|
1089
|
+
});
|
|
1090
|
+
const turn = page.data?.[0];
|
|
1091
|
+
return turn?.status === "inProgress" ? turn.id : void 0;
|
|
1092
|
+
} catch (err) {
|
|
1093
|
+
logger.debug(`thread/turns/list ${threadId} failed: ${err.message}`);
|
|
1094
|
+
return void 0;
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
808
1097
|
onServerRequest(rpcId, method, params) {
|
|
809
1098
|
const t = params?.threadId ? this.threads.get(params.threadId) : void 0;
|
|
810
1099
|
let view = null;
|
|
@@ -865,6 +1154,27 @@ var CodexAdapter = class {
|
|
|
865
1154
|
}
|
|
866
1155
|
};
|
|
867
1156
|
|
|
1157
|
+
// src/shared/hub-url.ts
|
|
1158
|
+
import net from "net";
|
|
1159
|
+
function hubUrlHost(host2) {
|
|
1160
|
+
if (host2 === "" || host2 === "0.0.0.0") return "127.0.0.1";
|
|
1161
|
+
if (host2 === "::") return "[::1]";
|
|
1162
|
+
return net.isIPv6(host2) ? `[${host2}]` : host2;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// src/codex/hub-target.ts
|
|
1166
|
+
function resolveAdapterHub(config2, env = process.env) {
|
|
1167
|
+
const envHost = env.CLAUDE_ALARM_HUB_HOST;
|
|
1168
|
+
const envPort = env.CLAUDE_ALARM_HUB_PORT;
|
|
1169
|
+
const envToken = env.CLAUDE_ALARM_HUB_TOKEN;
|
|
1170
|
+
return {
|
|
1171
|
+
host: hubUrlHost(envHost ?? config2.hub.host),
|
|
1172
|
+
port: envPort ? parseInt(envPort, 10) : config2.hub.port,
|
|
1173
|
+
token: envToken ?? config2.hub.token,
|
|
1174
|
+
fromEnv: { host: envHost !== void 0, port: !!envPort, token: envToken !== void 0 }
|
|
1175
|
+
};
|
|
1176
|
+
}
|
|
1177
|
+
|
|
868
1178
|
// src/codex/main.ts
|
|
869
1179
|
function isRunning(pid) {
|
|
870
1180
|
try {
|
|
@@ -880,17 +1190,24 @@ process.stdout.on("error", () => {
|
|
|
880
1190
|
process.stderr.on("error", () => {
|
|
881
1191
|
});
|
|
882
1192
|
var config = loadConfig();
|
|
883
|
-
var existing =
|
|
1193
|
+
var existing = fs6.existsSync(CODEX_PID_FILE) ? parseInt(fs6.readFileSync(CODEX_PID_FILE, "utf-8").trim(), 10) : NaN;
|
|
884
1194
|
if (!Number.isNaN(existing) && existing !== process.pid && isRunning(existing)) {
|
|
885
1195
|
logger.info(`Codex adapter already running (PID: ${existing})`);
|
|
886
1196
|
process.exit(0);
|
|
887
1197
|
}
|
|
888
|
-
|
|
1198
|
+
fs6.writeFileSync(CODEX_PID_FILE, String(process.pid), "utf-8");
|
|
889
1199
|
logStartup("Codex adapter");
|
|
890
|
-
var
|
|
891
|
-
var
|
|
892
|
-
|
|
893
|
-
|
|
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
|
+
});
|
|
894
1211
|
adapter.start();
|
|
895
1212
|
var exiting = false;
|
|
896
1213
|
var shutdown = () => {
|
|
@@ -898,7 +1215,7 @@ var shutdown = () => {
|
|
|
898
1215
|
exiting = true;
|
|
899
1216
|
adapter.stop();
|
|
900
1217
|
try {
|
|
901
|
-
if (
|
|
1218
|
+
if (fs6.readFileSync(CODEX_PID_FILE, "utf-8").trim() === String(process.pid)) fs6.unlinkSync(CODEX_PID_FILE);
|
|
902
1219
|
} catch {
|
|
903
1220
|
}
|
|
904
1221
|
process.exit(0);
|