@delt/claude-alarm 0.9.0 → 1.0.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.
@@ -0,0 +1,774 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/codex/main.ts
4
+ import fs4 from "fs";
5
+
6
+ // src/shared/config.ts
7
+ import fs from "fs";
8
+ import path2 from "path";
9
+ import { randomUUID } from "crypto";
10
+
11
+ // src/shared/constants.ts
12
+ import path from "path";
13
+ import os from "os";
14
+ var DEFAULT_HUB_HOST = "127.0.0.1";
15
+ var DEFAULT_HUB_PORT = 7900;
16
+ var CONFIG_DIR = path.join(os.homedir(), ".claude-alarm");
17
+ var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
18
+ var PID_FILE = path.join(CONFIG_DIR, "hub.pid");
19
+ var LOG_FILE = path.join(CONFIG_DIR, "hub.log");
20
+ var UPLOADS_DIR = path.join(CONFIG_DIR, "uploads");
21
+ var CODEX_PID_FILE = path.join(CONFIG_DIR, "codex.pid");
22
+ var CODEX_LOG_FILE = path.join(CONFIG_DIR, "codex.log");
23
+ var WS_PATH_CHANNEL = "/ws/channel";
24
+ var CHANNEL_SERVER_VERSION = "0.1.0";
25
+
26
+ // src/shared/config.ts
27
+ var DEFAULT_CONFIG = {
28
+ hub: {
29
+ host: DEFAULT_HUB_HOST,
30
+ port: DEFAULT_HUB_PORT
31
+ },
32
+ notifications: {
33
+ desktop: true,
34
+ sound: true
35
+ },
36
+ webhooks: []
37
+ };
38
+ function ensureConfigDir() {
39
+ if (!fs.existsSync(CONFIG_DIR)) {
40
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
41
+ }
42
+ }
43
+ function loadConfig() {
44
+ ensureConfigDir();
45
+ let config2;
46
+ if (!fs.existsSync(CONFIG_FILE)) {
47
+ config2 = { ...DEFAULT_CONFIG, hub: { ...DEFAULT_CONFIG.hub } };
48
+ } else {
49
+ try {
50
+ const raw = fs.readFileSync(CONFIG_FILE, "utf-8");
51
+ const parsed = JSON.parse(raw);
52
+ config2 = { ...DEFAULT_CONFIG, ...parsed, hub: { ...DEFAULT_CONFIG.hub, ...parsed.hub }, ...parsed.telegram ? { telegram: parsed.telegram } : {} };
53
+ } catch {
54
+ config2 = { ...DEFAULT_CONFIG, hub: { ...DEFAULT_CONFIG.hub } };
55
+ }
56
+ }
57
+ if (!config2.hub.token) {
58
+ config2.hub.token = randomUUID();
59
+ saveConfig(config2);
60
+ }
61
+ return config2;
62
+ }
63
+ function saveConfig(config2) {
64
+ ensureConfigDir();
65
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config2, null, 2), { encoding: "utf-8", mode: 384 });
66
+ }
67
+
68
+ // src/shared/crash-guard.ts
69
+ import fs2 from "fs";
70
+
71
+ // src/shared/logger.ts
72
+ var logger = {
73
+ info(msg, ...args) {
74
+ console.error(`[claude-alarm] ${msg}`, ...args);
75
+ },
76
+ warn(msg, ...args) {
77
+ console.error(`[claude-alarm WARN] ${msg}`, ...args);
78
+ },
79
+ error(msg, ...args) {
80
+ console.error(`[claude-alarm ERROR] ${msg}`, ...args);
81
+ },
82
+ debug(msg, ...args) {
83
+ if (process.env.CLAUDE_ALARM_DEBUG) {
84
+ console.error(`[claude-alarm DEBUG] ${msg}`, ...args);
85
+ }
86
+ }
87
+ };
88
+
89
+ // src/shared/crash-guard.ts
90
+ var BURST_WINDOW_MS = 6e4;
91
+ var BURST_LIMIT = 10;
92
+ function appendToLog(text) {
93
+ try {
94
+ fs2.mkdirSync(CONFIG_DIR, { recursive: true });
95
+ fs2.appendFileSync(LOG_FILE, text, "utf-8");
96
+ } catch {
97
+ }
98
+ }
99
+ function describe(err) {
100
+ if (err instanceof Error) return err.stack ?? `${err.name}: ${err.message}`;
101
+ try {
102
+ return JSON.stringify(err);
103
+ } catch {
104
+ return String(err);
105
+ }
106
+ }
107
+ function installCrashGuard(context) {
108
+ let recent = [];
109
+ const handle = (kind, err) => {
110
+ const stack = describe(err);
111
+ appendToLog(`
112
+ [${(/* @__PURE__ */ new Date()).toISOString()}] ${kind} (${context})
113
+ ${stack}
114
+ `);
115
+ logger.error(`${kind} caught by crash guard: ${stack}`);
116
+ const now = Date.now();
117
+ recent = recent.filter((t) => now - t < BURST_WINDOW_MS);
118
+ recent.push(now);
119
+ if (recent.length > BURST_LIMIT) {
120
+ const msg = `${recent.length} unhandled errors within ${BURST_WINDOW_MS / 1e3}s \u2014 exiting`;
121
+ appendToLog(`[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
122
+ `);
123
+ logger.error(msg);
124
+ process.exit(1);
125
+ }
126
+ };
127
+ process.on("uncaughtException", (err) => handle("uncaughtException", err));
128
+ process.on("unhandledRejection", (reason) => handle("unhandledRejection", reason));
129
+ }
130
+ function logStartup(context) {
131
+ appendToLog(`
132
+ [${(/* @__PURE__ */ new Date()).toISOString()}] ${context} started (pid ${process.pid})
133
+ `);
134
+ }
135
+
136
+ // src/channel/hub-client.ts
137
+ import WebSocket from "ws";
138
+ var HubClient = class {
139
+ constructor(sessionId, sessionName, hubHost = DEFAULT_HUB_HOST, hubPort = DEFAULT_HUB_PORT, token2, getPeerName = () => void 0, getRegistration = () => ({})) {
140
+ this.sessionId = sessionId;
141
+ this.sessionName = sessionName;
142
+ this.hubHost = hubHost;
143
+ this.hubPort = hubPort;
144
+ this.token = token2;
145
+ this.getPeerName = getPeerName;
146
+ this.getRegistration = getRegistration;
147
+ }
148
+ ws = null;
149
+ reconnectTimer = null;
150
+ messageHandlers = [];
151
+ queue = [];
152
+ connected = false;
153
+ closed = false;
154
+ connect() {
155
+ this.closed = false;
156
+ const tokenQuery = this.token ? `?token=${encodeURIComponent(this.token)}` : "";
157
+ const url = `ws://${this.hubHost}:${this.hubPort}${WS_PATH_CHANNEL}${tokenQuery}`;
158
+ logger.debug(`Connecting to hub at ${url}`);
159
+ try {
160
+ const ws = new WebSocket(url);
161
+ this.ws = ws;
162
+ ws.on("open", () => {
163
+ if (this.ws !== ws) return;
164
+ logger.info("Connected to hub");
165
+ this.connected = true;
166
+ this.ws.send(JSON.stringify(this.registration()));
167
+ for (const msg of this.queue) {
168
+ this.ws.send(JSON.stringify(msg));
169
+ }
170
+ this.queue = [];
171
+ });
172
+ ws.on("message", (data) => {
173
+ if (this.ws !== ws) return;
174
+ try {
175
+ const msg = JSON.parse(data.toString());
176
+ for (const handler of this.messageHandlers) {
177
+ handler(msg);
178
+ }
179
+ } catch (err) {
180
+ logger.warn("Failed to parse hub message:", err);
181
+ }
182
+ });
183
+ ws.on("close", () => {
184
+ if (this.ws !== ws) return;
185
+ logger.info("Disconnected from hub");
186
+ this.connected = false;
187
+ if (!this.closed) this.scheduleReconnect();
188
+ });
189
+ ws.on("error", (err) => {
190
+ if (this.ws !== ws) return;
191
+ logger.debug(`Hub connection error: ${err.message}`);
192
+ this.connected = false;
193
+ });
194
+ } catch {
195
+ logger.debug("Failed to connect to hub, will retry");
196
+ this.scheduleReconnect();
197
+ }
198
+ }
199
+ send(msg) {
200
+ if (this.connected && this.ws?.readyState === WebSocket.OPEN) {
201
+ this.ws.send(JSON.stringify(msg));
202
+ } else {
203
+ if (this.queue.length < 100) {
204
+ this.queue.push(msg);
205
+ }
206
+ logger.debug("Hub not connected, message queued");
207
+ }
208
+ }
209
+ onMessage(handler) {
210
+ this.messageHandlers.push(handler);
211
+ }
212
+ disconnect() {
213
+ this.closed = true;
214
+ if (this.reconnectTimer) {
215
+ clearTimeout(this.reconnectTimer);
216
+ this.reconnectTimer = null;
217
+ }
218
+ if (this.ws) {
219
+ this.ws.close();
220
+ this.ws = null;
221
+ }
222
+ this.connected = false;
223
+ }
224
+ reregister() {
225
+ if (this.connected && this.ws?.readyState === WebSocket.OPEN) {
226
+ this.ws.send(JSON.stringify(this.registration()));
227
+ }
228
+ }
229
+ registration() {
230
+ return {
231
+ type: "register",
232
+ session: {
233
+ id: this.sessionId,
234
+ name: this.sessionName,
235
+ status: "idle",
236
+ connectedAt: Date.now(),
237
+ lastActivity: Date.now(),
238
+ cwd: process.cwd(),
239
+ channelEnabled: true,
240
+ peerName: this.getPeerName(),
241
+ ...this.getRegistration()
242
+ }
243
+ };
244
+ }
245
+ scheduleReconnect() {
246
+ if (this.reconnectTimer || this.closed) return;
247
+ this.reconnectTimer = setTimeout(() => {
248
+ this.reconnectTimer = null;
249
+ this.connect();
250
+ }, 5e3);
251
+ }
252
+ };
253
+
254
+ // src/codex/transport.ts
255
+ import { spawn } from "child_process";
256
+ import fs3 from "fs";
257
+ import path3 from "path";
258
+ import { Duplex } from "stream";
259
+ import WebSocket2 from "ws";
260
+ function resolveCommand(command, platform = process.platform, env = process.env) {
261
+ if (platform !== "win32" || /[\\/]/.test(command) || path3.extname(command)) return { file: command, shell: false };
262
+ const dirs = (env.PATH ?? env.Path ?? "").split(path3.delimiter).filter(Boolean);
263
+ for (const ext of [".exe", ".cmd"]) {
264
+ for (const dir of dirs) {
265
+ const file = path3.join(dir, command + ext);
266
+ if (fs3.existsSync(file)) return { file, shell: ext === ".cmd" };
267
+ }
268
+ }
269
+ return { file: command, shell: false };
270
+ }
271
+ var defaultSpawn = (command, args) => {
272
+ const { file, shell } = resolveCommand(command);
273
+ if (shell) return spawn(`"${file}" ${args.join(" ")}`, { stdio: "pipe", windowsHide: true, shell: true });
274
+ return spawn(file, args, { stdio: "pipe", windowsHide: true });
275
+ };
276
+ function connectProxy(command, spawnFn = defaultSpawn) {
277
+ const child = spawnFn(command, ["app-server", "proxy"]);
278
+ child.stdin?.on("error", () => {
279
+ });
280
+ child.stderr?.on("data", (d) => logger.debug(`codex proxy: ${String(d).trim()}`));
281
+ const stream = new Duplex({
282
+ read() {
283
+ },
284
+ write(chunk, _encoding, callback) {
285
+ if (child.stdin?.writable) child.stdin.write(chunk, callback);
286
+ else callback();
287
+ },
288
+ final(callback) {
289
+ child.stdin?.end();
290
+ callback();
291
+ }
292
+ });
293
+ child.stdout?.on("data", (d) => stream.push(d));
294
+ child.stdout?.on("end", () => stream.push(null));
295
+ Object.assign(stream, { setNoDelay() {
296
+ }, setTimeout() {
297
+ }, setKeepAlive() {
298
+ }, ref() {
299
+ }, unref() {
300
+ } });
301
+ const ws = new WebSocket2("ws://localhost/", {
302
+ createConnection: (() => stream)
303
+ });
304
+ const close = () => {
305
+ ws.terminate();
306
+ child.kill();
307
+ };
308
+ return new Promise((resolve, reject) => {
309
+ let settled = false;
310
+ const fail = (err) => {
311
+ if (settled) return;
312
+ settled = true;
313
+ close();
314
+ reject(err);
315
+ };
316
+ child.on("error", fail);
317
+ child.once("exit", (code) => fail(new Error(`codex proxy exited (code ${code})`)));
318
+ ws.on("error", fail);
319
+ ws.once("open", () => {
320
+ if (settled) return;
321
+ settled = true;
322
+ child.on("error", (err) => logger.warn(`codex proxy error: ${err.message}`));
323
+ ws.on("error", (err) => logger.warn(`codex daemon socket error: ${err.message}`));
324
+ resolve({ ws, close });
325
+ });
326
+ });
327
+ }
328
+
329
+ // src/codex/rpc.ts
330
+ import { EventEmitter } from "events";
331
+ var RpcError = class extends Error {
332
+ constructor(code, message) {
333
+ super(message);
334
+ this.code = code;
335
+ }
336
+ };
337
+ var RpcClient = class extends EventEmitter {
338
+ constructor(ws, timeoutMs = 3e4) {
339
+ super();
340
+ this.ws = ws;
341
+ this.timeoutMs = timeoutMs;
342
+ ws.on("message", (data) => this.onMessage(String(data)));
343
+ ws.on("close", () => {
344
+ for (const p of this.pending.values()) {
345
+ clearTimeout(p.timer);
346
+ p.reject(new Error("connection closed"));
347
+ }
348
+ this.pending.clear();
349
+ this.emit("close");
350
+ });
351
+ }
352
+ nextId = 1;
353
+ pending = /* @__PURE__ */ new Map();
354
+ request(method, params) {
355
+ if (this.ws.readyState !== this.ws.OPEN) return Promise.reject(new Error("connection closed"));
356
+ const id = this.nextId++;
357
+ return new Promise((resolve, reject) => {
358
+ const timer = setTimeout(() => {
359
+ this.pending.delete(id);
360
+ reject(new Error(`${method} timed out`));
361
+ }, this.timeoutMs);
362
+ this.pending.set(id, { resolve, reject, timer });
363
+ this.ws.send(JSON.stringify(params === void 0 ? { id, method } : { id, method, params }));
364
+ });
365
+ }
366
+ notify(method, params) {
367
+ if (this.ws.readyState !== this.ws.OPEN) return;
368
+ this.ws.send(JSON.stringify(params === void 0 ? { method } : { method, params }));
369
+ }
370
+ // ws stops reading the socket for good if a 'message' listener throws.
371
+ guarded(method, fn) {
372
+ try {
373
+ fn();
374
+ } catch (err) {
375
+ logger.warn(`Codex ${method} handling failed: ${err.message}`);
376
+ }
377
+ }
378
+ onMessage(text) {
379
+ let msg;
380
+ try {
381
+ msg = JSON.parse(text);
382
+ } catch {
383
+ return;
384
+ }
385
+ if (!msg || typeof msg !== "object") return;
386
+ if (msg.method === void 0 && msg.id !== void 0) {
387
+ const p = this.pending.get(msg.id);
388
+ if (!p) return;
389
+ this.pending.delete(msg.id);
390
+ clearTimeout(p.timer);
391
+ if (msg.error) p.reject(new RpcError(msg.error.code, msg.error.message));
392
+ else p.resolve(msg.result);
393
+ } else if (msg.method !== void 0 && msg.id !== void 0) {
394
+ this.guarded(msg.method, () => this.emit("request", msg.id, msg.method, msg.params));
395
+ } else if (msg.method !== void 0) {
396
+ this.guarded(msg.method, () => this.emit("notification", msg.method, msg.params));
397
+ }
398
+ }
399
+ };
400
+
401
+ // src/codex/mapping.ts
402
+ var TITLE_MAX = 30;
403
+ var SOURCE_LABEL = { dashboard: "Dashboard", telegram: "Telegram", api: "API" };
404
+ function codexSessionId(threadId) {
405
+ return `codex:${threadId}`;
406
+ }
407
+ function isTrackable(thread) {
408
+ return !thread.parentThreadId && !thread.ephemeral && thread.status.type !== "notLoaded";
409
+ }
410
+ function threadTitle(thread) {
411
+ const name = thread.name?.trim();
412
+ if (name) return name;
413
+ const preview = (thread.preview ?? "").replace(/\s+/g, " ").trim();
414
+ if (preview) return preview.length > TITLE_MAX ? `${preview.slice(0, TITLE_MAX)}\u2026` : preview;
415
+ return thread.cwd.replace(/^.*[/\\]/, "") || thread.id.slice(0, 8);
416
+ }
417
+ function hubStatus(status) {
418
+ if (status.type !== "active") return "idle";
419
+ return status.activeFlags.some((f) => f === "waitingOnApproval" || f === "waitingOnUserInput") ? "waiting_input" : "working";
420
+ }
421
+ function withSourcePrefix(content, source) {
422
+ return source ? `[claude-alarm \xB7 ${SOURCE_LABEL[source]}] ${content}` : `[claude-alarm] ${content}`;
423
+ }
424
+ function finalAnswer(messages) {
425
+ const finals = messages.filter((m) => m.phase === "final_answer").map((m) => m.text);
426
+ if (finals.length) return finals.join("\n\n");
427
+ return messages.at(-1)?.text ?? null;
428
+ }
429
+
430
+ // src/codex/adapter.ts
431
+ var APPROVAL_REQUESTS = /* @__PURE__ */ new Set([
432
+ "item/commandExecution/requestApproval",
433
+ "item/fileChange/requestApproval",
434
+ "mcpServer/elicitation/request"
435
+ ]);
436
+ var CodexAdapter = class {
437
+ constructor(opts) {
438
+ this.opts = opts;
439
+ this.delay = opts.reconnectMinMs ?? 2e3;
440
+ }
441
+ conn;
442
+ rpc;
443
+ threads = /* @__PURE__ */ new Map();
444
+ stopped = false;
445
+ retryTimer;
446
+ delay;
447
+ start() {
448
+ this.stopped = false;
449
+ void this.connect();
450
+ }
451
+ stop() {
452
+ this.stopped = true;
453
+ if (this.retryTimer) clearTimeout(this.retryTimer);
454
+ this.retryTimer = void 0;
455
+ for (const id of [...this.threads.keys()]) this.drop(id);
456
+ this.conn?.close();
457
+ }
458
+ async connect() {
459
+ let rpc;
460
+ try {
461
+ const conn = await connectProxy(this.opts.command, this.opts.spawnFn);
462
+ if (this.stopped) {
463
+ conn.close();
464
+ return;
465
+ }
466
+ const live = new RpcClient(conn.ws);
467
+ rpc = live;
468
+ live.on("notification", (method, params) => this.onNotification(method, params));
469
+ live.on("request", (_id, method, params) => this.onServerRequest(method, params));
470
+ live.on("close", () => {
471
+ if (this.rpc === live) this.onDaemonLost();
472
+ });
473
+ this.conn = conn;
474
+ this.rpc = live;
475
+ const init = await live.request("initialize", {
476
+ clientInfo: { name: "claude-alarm", version: CHANNEL_SERVER_VERSION }
477
+ });
478
+ live.notify("initialized");
479
+ logger.info(`Connected to Codex daemon (${init.userAgent ?? "unknown version"})`);
480
+ await this.discover();
481
+ this.delay = this.opts.reconnectMinMs ?? 2e3;
482
+ } catch (err) {
483
+ logger.warn(`Codex daemon connection failed: ${err.message}`);
484
+ if (rpc && this.rpc === rpc) {
485
+ this.rpc = void 0;
486
+ this.conn?.close();
487
+ this.conn = void 0;
488
+ }
489
+ for (const id of [...this.threads.keys()]) this.drop(id);
490
+ this.scheduleRetry();
491
+ }
492
+ }
493
+ onDaemonLost() {
494
+ this.rpc = void 0;
495
+ this.conn?.close();
496
+ this.conn = void 0;
497
+ for (const id of [...this.threads.keys()]) this.drop(id);
498
+ if (!this.stopped) logger.warn("Codex daemon connection lost");
499
+ this.scheduleRetry();
500
+ }
501
+ scheduleRetry() {
502
+ if (this.stopped || this.retryTimer) return;
503
+ this.retryTimer = setTimeout(() => {
504
+ this.retryTimer = void 0;
505
+ void this.connect();
506
+ }, this.delay);
507
+ this.delay = Math.min(this.delay * 2, this.opts.reconnectMaxMs ?? 6e4);
508
+ }
509
+ async discover() {
510
+ const ids = [];
511
+ let cursor;
512
+ do {
513
+ const page = await this.rpc.request(
514
+ "thread/loaded/list",
515
+ cursor ? { cursor } : {}
516
+ );
517
+ ids.push(...page.data);
518
+ cursor = page.nextCursor;
519
+ } while (cursor);
520
+ for (const id of ids) await this.refresh(id);
521
+ }
522
+ async refresh(threadId) {
523
+ try {
524
+ const { thread } = await this.rpc.request("thread/read", { threadId, includeTurns: false });
525
+ this.upsert(thread);
526
+ } catch (err) {
527
+ logger.debug(`thread/read ${threadId} failed: ${err.message}`);
528
+ }
529
+ }
530
+ upsert(thread) {
531
+ if (!isTrackable(thread)) {
532
+ this.drop(thread.id);
533
+ return;
534
+ }
535
+ const existing2 = this.threads.get(thread.id);
536
+ if (existing2) {
537
+ existing2.thread = thread;
538
+ existing2.hub.reregister();
539
+ } else {
540
+ const { host: host2, port: port2, token: token2 } = this.opts.hub;
541
+ const hub = new HubClient(
542
+ codexSessionId(thread.id),
543
+ threadTitle(thread),
544
+ host2,
545
+ port2,
546
+ token2,
547
+ () => void 0,
548
+ () => this.registration(thread.id)
549
+ );
550
+ this.threads.set(thread.id, {
551
+ thread,
552
+ hub,
553
+ subscribed: false,
554
+ wantSubscribed: false,
555
+ sync: Promise.resolve(),
556
+ pendingTurn: false,
557
+ turns: /* @__PURE__ */ new Map(),
558
+ unrelayed: false
559
+ });
560
+ hub.onMessage((msg) => this.onHubMessage(thread.id, msg));
561
+ hub.connect();
562
+ }
563
+ void this.want(thread.id, thread.status.type === "active");
564
+ }
565
+ registration(threadId) {
566
+ const t = this.threads.get(threadId);
567
+ if (!t) return {};
568
+ const title = threadTitle(t.thread);
569
+ return { name: title, title, cwd: t.thread.cwd, agentKind: "codex", status: hubStatus(t.thread.status) };
570
+ }
571
+ want(threadId, subscribed) {
572
+ const t = this.threads.get(threadId);
573
+ if (!t) return Promise.resolve();
574
+ t.wantSubscribed = subscribed;
575
+ t.sync = t.sync.then(() => this.syncSubscription(t));
576
+ return t.sync;
577
+ }
578
+ async syncSubscription(t) {
579
+ const threadId = t.thread.id;
580
+ if (!this.rpc || this.threads.get(threadId) !== t || t.subscribed === t.wantSubscribed) return;
581
+ try {
582
+ if (t.wantSubscribed) {
583
+ await this.rpc.request("thread/resume", { threadId, excludeTurns: true });
584
+ t.subscribed = true;
585
+ t.unrelayed = false;
586
+ } else {
587
+ await this.rpc.request("thread/unsubscribe", { threadId });
588
+ t.subscribed = false;
589
+ }
590
+ } catch (err) {
591
+ logger.debug(`subscription sync for ${threadId} deferred: ${err.message}`);
592
+ }
593
+ }
594
+ releaseLater(t) {
595
+ clearTimeout(t.releaseTimer);
596
+ t.releaseTimer = setTimeout(() => {
597
+ t.releaseTimer = void 0;
598
+ if (this.threads.get(t.thread.id) === t && t.thread.status.type !== "active" && !t.pendingTurn) {
599
+ void this.want(t.thread.id, false);
600
+ }
601
+ }, this.opts.idleReleaseMs ?? 1e3);
602
+ }
603
+ drop(threadId) {
604
+ const t = this.threads.get(threadId);
605
+ if (!t) return;
606
+ this.threads.delete(threadId);
607
+ clearTimeout(t.releaseTimer);
608
+ t.releaseTimer = void 0;
609
+ t.hub.disconnect();
610
+ }
611
+ onNotification(method, params) {
612
+ switch (method) {
613
+ case "thread/started":
614
+ this.upsert(params.thread);
615
+ break;
616
+ case "thread/status/changed":
617
+ this.onStatus(params.threadId, params.status);
618
+ break;
619
+ case "thread/name/updated": {
620
+ const t = this.threads.get(params.threadId);
621
+ if (t) {
622
+ t.thread.name = params.threadName;
623
+ t.hub.reregister();
624
+ }
625
+ break;
626
+ }
627
+ case "thread/closed":
628
+ case "thread/archived":
629
+ case "thread/deleted":
630
+ this.drop(params.threadId);
631
+ break;
632
+ case "item/completed":
633
+ if (params.item?.type === "agentMessage") this.collect(params.threadId, params.turnId, params.item);
634
+ break;
635
+ case "turn/completed":
636
+ this.onTurnCompleted(params.threadId, params.turn);
637
+ break;
638
+ }
639
+ }
640
+ onStatus(threadId, status) {
641
+ const t = this.threads.get(threadId);
642
+ if (!t) {
643
+ if (status.type !== "notLoaded") void this.refresh(threadId);
644
+ return;
645
+ }
646
+ if (status.type === "notLoaded") {
647
+ this.drop(threadId);
648
+ return;
649
+ }
650
+ t.thread.status = status;
651
+ t.pendingTurn = false;
652
+ t.hub.send({ type: "status", sessionId: codexSessionId(threadId), status: hubStatus(status) });
653
+ if (status.type === "systemError") this.notify(threadId, "Codex error", "The Codex conversation hit a system error.", "error");
654
+ if (status.type === "active") {
655
+ void this.want(threadId, true);
656
+ return;
657
+ }
658
+ if (t.unrelayed) {
659
+ t.unrelayed = false;
660
+ this.notify(threadId, "Reply not relayed", "Codex finished, but its reply could not be relayed here. Check the Codex window.", "warning");
661
+ }
662
+ this.releaseLater(t);
663
+ }
664
+ collect(threadId, turnId, item) {
665
+ const t = this.threads.get(threadId);
666
+ if (!t) return;
667
+ const list = t.turns.get(turnId) ?? [];
668
+ list.push({ text: item.text, phase: item.phase });
669
+ t.turns.set(turnId, list);
670
+ }
671
+ onTurnCompleted(threadId, turn) {
672
+ const t = this.threads.get(threadId);
673
+ if (!t) return;
674
+ const collected = t.turns.get(turn.id) ?? [];
675
+ t.turns.delete(turn.id);
676
+ if (t.thread.status.type !== "active") void this.want(threadId, false);
677
+ if (turn.status === "failed" || turn.error) {
678
+ this.notify(threadId, "Codex task failed", turn.error?.message ?? "The task ended with an error.", "error");
679
+ return;
680
+ }
681
+ if (turn.status === "interrupted") {
682
+ this.notify(threadId, "Codex task stopped", "The task was interrupted.", "info");
683
+ return;
684
+ }
685
+ const fromTurn = (turn.items ?? []).filter((i) => i.type === "agentMessage");
686
+ const text = finalAnswer(collected.length ? collected : fromTurn);
687
+ if (text) t.hub.send({ type: "reply", sessionId: codexSessionId(threadId), content: text });
688
+ }
689
+ onHubMessage(threadId, msg) {
690
+ if (msg.type === "message_to_session") {
691
+ void this.sendTurn(threadId, msg.content, msg.source);
692
+ } else if (msg.type === "image_to_session") {
693
+ this.notify(threadId, "Not delivered", "Codex sessions do not accept images yet.", "warning");
694
+ }
695
+ }
696
+ async sendTurn(threadId, content, source) {
697
+ const t = this.threads.get(threadId);
698
+ if (!t) return;
699
+ if (t.thread.status.type !== "idle" || t.pendingTurn) {
700
+ this.notify(threadId, "Not delivered", "Codex is busy, so the message was not delivered. Send it again when the task finishes.", "warning");
701
+ return;
702
+ }
703
+ t.pendingTurn = true;
704
+ try {
705
+ if (!this.rpc) throw new Error("not connected to the Codex daemon");
706
+ await this.want(threadId, true);
707
+ t.unrelayed = !t.subscribed;
708
+ await this.rpc.request("turn/start", { threadId, input: [{ type: "text", text: withSourcePrefix(content, source) }] });
709
+ } catch (err) {
710
+ t.pendingTurn = false;
711
+ t.unrelayed = false;
712
+ this.releaseLater(t);
713
+ this.notify(threadId, "Not delivered", `Codex rejected the message: ${err.message}`, "warning");
714
+ }
715
+ }
716
+ onServerRequest(method, params) {
717
+ if (!APPROVAL_REQUESTS.has(method) || !params?.threadId) {
718
+ logger.debug(`Ignoring Codex server request ${method}`);
719
+ return;
720
+ }
721
+ const detail = params.commandActions?.[0]?.command ?? params.command ?? params.reason ?? params.message ?? method;
722
+ this.notify(params.threadId, "Codex approval needed", `Approve or decline in Codex: ${String(detail).slice(0, 300)}`, "warning");
723
+ }
724
+ notify(threadId, title, message, level) {
725
+ this.threads.get(threadId)?.hub.send({ type: "notify", sessionId: codexSessionId(threadId), title, message, level });
726
+ }
727
+ };
728
+
729
+ // src/codex/main.ts
730
+ function isRunning(pid) {
731
+ try {
732
+ process.kill(pid, 0);
733
+ return true;
734
+ } catch {
735
+ return false;
736
+ }
737
+ }
738
+ installCrashGuard("codex adapter");
739
+ process.stdout.on("error", () => {
740
+ });
741
+ process.stderr.on("error", () => {
742
+ });
743
+ var config = loadConfig();
744
+ var existing = fs4.existsSync(CODEX_PID_FILE) ? parseInt(fs4.readFileSync(CODEX_PID_FILE, "utf-8").trim(), 10) : NaN;
745
+ if (!Number.isNaN(existing) && existing !== process.pid && isRunning(existing)) {
746
+ logger.info(`Codex adapter already running (PID: ${existing})`);
747
+ process.exit(0);
748
+ }
749
+ fs4.writeFileSync(CODEX_PID_FILE, String(process.pid), "utf-8");
750
+ logStartup("Codex adapter");
751
+ var host = process.env.CLAUDE_ALARM_HUB_HOST ?? (config.hub.host === "0.0.0.0" ? DEFAULT_HUB_HOST : config.hub.host);
752
+ var port = process.env.CLAUDE_ALARM_HUB_PORT ? parseInt(process.env.CLAUDE_ALARM_HUB_PORT, 10) : config.hub.port;
753
+ var token = process.env.CLAUDE_ALARM_HUB_TOKEN ?? config.hub.token;
754
+ var adapter = new CodexAdapter({ command: config.codex?.command ?? "codex", hub: { host, port, token } });
755
+ adapter.start();
756
+ var exiting = false;
757
+ var shutdown = () => {
758
+ if (exiting) return;
759
+ exiting = true;
760
+ adapter.stop();
761
+ try {
762
+ if (fs4.readFileSync(CODEX_PID_FILE, "utf-8").trim() === String(process.pid)) fs4.unlinkSync(CODEX_PID_FILE);
763
+ } catch {
764
+ }
765
+ process.exit(0);
766
+ };
767
+ process.on("SIGINT", shutdown);
768
+ process.on("SIGTERM", shutdown);
769
+ if (process.argv.includes("--watch-stdin")) {
770
+ process.stdin.on("end", shutdown);
771
+ process.stdin.on("close", shutdown);
772
+ process.stdin.resume();
773
+ }
774
+ //# sourceMappingURL=main.js.map