@delt/claude-alarm 1.1.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -10
- package/dist/channel/server.js.map +1 -1
- package/dist/cli.js +1258 -180
- package/dist/cli.js.map +1 -1
- package/dist/codex/main.js +660 -106
- package/dist/codex/main.js.map +1 -1
- package/dist/dashboard/index.html +176 -1
- package/dist/hub/server.js +308 -56
- package/dist/hub/server.js.map +1 -1
- package/dist/index.d.ts +71 -3
- package/dist/index.js +309 -56
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/dashboard/index.html +176 -1
package/dist/cli.js
CHANGED
|
@@ -12,7 +12,7 @@ var __export = (target, all) => {
|
|
|
12
12
|
// src/shared/constants.ts
|
|
13
13
|
import path from "path";
|
|
14
14
|
import os from "os";
|
|
15
|
-
var DEFAULT_HUB_HOST, DEFAULT_HUB_PORT, CONFIG_DIR, CONFIG_FILE, PID_FILE, LOG_FILE, UPLOADS_DIR, CODEX_PID_FILE, CODEX_LOG_FILE, WS_PATH_CHANNEL, WS_PATH_DASHBOARD, CHANNEL_SERVER_NAME, CHANNEL_SERVER_VERSION;
|
|
15
|
+
var DEFAULT_HUB_HOST, DEFAULT_HUB_PORT, CONFIG_DIR, CONFIG_FILE, PID_FILE, LOG_FILE, UPLOADS_DIR, CODEX_PID_FILE, CODEX_LOG_FILE, WS_PATH_CHANNEL, WS_PATH_DASHBOARD, WS_PATH_CODEX, CHANNEL_SERVER_NAME, CHANNEL_SERVER_VERSION;
|
|
16
16
|
var init_constants = __esm({
|
|
17
17
|
"src/shared/constants.ts"() {
|
|
18
18
|
"use strict";
|
|
@@ -27,6 +27,7 @@ var init_constants = __esm({
|
|
|
27
27
|
CODEX_LOG_FILE = path.join(CONFIG_DIR, "codex.log");
|
|
28
28
|
WS_PATH_CHANNEL = "/ws/channel";
|
|
29
29
|
WS_PATH_DASHBOARD = "/ws/dashboard";
|
|
30
|
+
WS_PATH_CODEX = "/ws/codex";
|
|
30
31
|
CHANNEL_SERVER_NAME = "claude-alarm";
|
|
31
32
|
CHANNEL_SERVER_VERSION = "0.1.0";
|
|
32
33
|
}
|
|
@@ -201,6 +202,19 @@ var init_crash_guard = __esm({
|
|
|
201
202
|
}
|
|
202
203
|
});
|
|
203
204
|
|
|
205
|
+
// src/shared/hub-url.ts
|
|
206
|
+
import net from "net";
|
|
207
|
+
function hubUrlHost(host) {
|
|
208
|
+
if (host === "" || host === "0.0.0.0") return "127.0.0.1";
|
|
209
|
+
if (host === "::") return "[::1]";
|
|
210
|
+
return net.isIPv6(host) ? `[${host}]` : host;
|
|
211
|
+
}
|
|
212
|
+
var init_hub_url = __esm({
|
|
213
|
+
"src/shared/hub-url.ts"() {
|
|
214
|
+
"use strict";
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
204
218
|
// src/shared/peer-name.ts
|
|
205
219
|
function isValidPeerName(name) {
|
|
206
220
|
return typeof name === "string" && name.trim() !== "" && SAFE_PEER_NAME.test(name);
|
|
@@ -351,6 +365,7 @@ var init_notifier = __esm({
|
|
|
351
365
|
execFile(
|
|
352
366
|
"powershell",
|
|
353
367
|
["-Command", '(Get-ItemProperty -Path "HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\PushNotifications" -Name ToastEnabled -ErrorAction SilentlyContinue).ToastEnabled'],
|
|
368
|
+
{ windowsHide: true },
|
|
354
369
|
(err, stdout) => {
|
|
355
370
|
if (err) {
|
|
356
371
|
resolve(true);
|
|
@@ -368,7 +383,7 @@ var init_notifier = __esm({
|
|
|
368
383
|
logger.warn("Windows notifications are disabled. Opening notification settings...");
|
|
369
384
|
logger.warn("Please enable notifications for this app, then try again.");
|
|
370
385
|
if (process.platform === "win32") {
|
|
371
|
-
execFile("powershell", ["-Command", "Start-Process ms-settings:notifications"]);
|
|
386
|
+
execFile("powershell", ["-Command", "Start-Process ms-settings:notifications"], { windowsHide: true });
|
|
372
387
|
}
|
|
373
388
|
setTimeout(() => {
|
|
374
389
|
this.notificationSettingsOpened = false;
|
|
@@ -424,10 +439,13 @@ var init_permission_key = __esm({
|
|
|
424
439
|
});
|
|
425
440
|
|
|
426
441
|
// src/hub/telegram.ts
|
|
427
|
-
import
|
|
428
|
-
import
|
|
442
|
+
import fs5 from "fs";
|
|
443
|
+
import path6 from "path";
|
|
429
444
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
430
|
-
|
|
445
|
+
function visibleLength(html) {
|
|
446
|
+
return html.replace(/<[^>]*>/g, "").replace(/&(?:amp|lt|gt);/g, "&").length;
|
|
447
|
+
}
|
|
448
|
+
var TELEGRAM_API, MAX_CHOICE_MESSAGES, MAX_SELECTIONS, MAX_VISIBLE_CHARS, TRUNCATED, MAX_PHOTO_BYTES, TelegramBot;
|
|
431
449
|
var init_telegram = __esm({
|
|
432
450
|
"src/hub/telegram.ts"() {
|
|
433
451
|
"use strict";
|
|
@@ -437,6 +455,10 @@ var init_telegram = __esm({
|
|
|
437
455
|
init_permission_key();
|
|
438
456
|
TELEGRAM_API = "https://api.telegram.org/bot";
|
|
439
457
|
MAX_CHOICE_MESSAGES = 200;
|
|
458
|
+
MAX_SELECTIONS = 20;
|
|
459
|
+
MAX_VISIBLE_CHARS = 4e3;
|
|
460
|
+
TRUNCATED = "\u2026(truncated)";
|
|
461
|
+
MAX_PHOTO_BYTES = 10 * 1024 * 1024;
|
|
440
462
|
TelegramBot = class _TelegramBot {
|
|
441
463
|
config;
|
|
442
464
|
offset = 0;
|
|
@@ -457,9 +479,9 @@ var init_telegram = __esm({
|
|
|
457
479
|
choiceMessages = /* @__PURE__ */ new Map();
|
|
458
480
|
// Callback: get current sessions list
|
|
459
481
|
getSessions;
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
482
|
+
latestSelection;
|
|
483
|
+
// Keyed by prompt, not by chat: buttons from an older prompt must still send that prompt's message.
|
|
484
|
+
selections = /* @__PURE__ */ new Map();
|
|
463
485
|
constructor(config2) {
|
|
464
486
|
this.config = config2;
|
|
465
487
|
}
|
|
@@ -468,8 +490,7 @@ var init_telegram = __esm({
|
|
|
468
490
|
}
|
|
469
491
|
/** Send a notification message to Telegram */
|
|
470
492
|
async sendNotification(sessionId2, _sessionLabel, title, message) {
|
|
471
|
-
const text =
|
|
472
|
-
${this.mdToHtml(message)}`;
|
|
493
|
+
const text = this.fitNotification(title, message);
|
|
473
494
|
const result = await this.sendMessage(text);
|
|
474
495
|
if (result?.message_id) {
|
|
475
496
|
this.messageSessionMap.set(result.message_id, sessionId2);
|
|
@@ -481,6 +502,25 @@ ${this.mdToHtml(message)}`;
|
|
|
481
502
|
}
|
|
482
503
|
}
|
|
483
504
|
}
|
|
505
|
+
fitNotification(title, message) {
|
|
506
|
+
const render = (body) => `<b>${this.escHtml(title)}</b>
|
|
507
|
+
${this.mdToHtml(body)}`;
|
|
508
|
+
const full = render(message);
|
|
509
|
+
if (visibleLength(full) <= MAX_VISIBLE_CHARS) return full;
|
|
510
|
+
const cut = (n) => {
|
|
511
|
+
const end = n > 0 && /[\uD800-\uDBFF]/.test(message[n - 1]) ? n - 1 : n;
|
|
512
|
+
return render(`${message.slice(0, end)}
|
|
513
|
+
${TRUNCATED}`);
|
|
514
|
+
};
|
|
515
|
+
let lo = 0;
|
|
516
|
+
let hi = message.length;
|
|
517
|
+
while (lo < hi) {
|
|
518
|
+
const mid = Math.ceil((lo + hi) / 2);
|
|
519
|
+
if (visibleLength(cut(mid)) <= MAX_VISIBLE_CHARS) lo = mid;
|
|
520
|
+
else hi = mid - 1;
|
|
521
|
+
}
|
|
522
|
+
return cut(lo);
|
|
523
|
+
}
|
|
484
524
|
/** Send a text message to the configured chat */
|
|
485
525
|
async sendMessage(text, replyToMessageId, replyMarkup) {
|
|
486
526
|
try {
|
|
@@ -562,6 +602,13 @@ ${this.mdToHtml(message)}`;
|
|
|
562
602
|
const hasPhoto = msg.photo && msg.photo.length > 0;
|
|
563
603
|
const text = (msg.text || msg.caption || "").trim();
|
|
564
604
|
if (!text && !hasPhoto) return;
|
|
605
|
+
if (hasPhoto) {
|
|
606
|
+
const size = msg.photo[msg.photo.length - 1].file_size;
|
|
607
|
+
if (size !== void 0 && size > MAX_PHOTO_BYTES) {
|
|
608
|
+
this.photoNotDelivered("it is larger than 10 MB");
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
565
612
|
if (msg.reply_to_message) {
|
|
566
613
|
const sessionId2 = this.messageSessionMap.get(msg.reply_to_message.message_id);
|
|
567
614
|
if (sessionId2) {
|
|
@@ -575,18 +622,20 @@ ${this.mdToHtml(message)}`;
|
|
|
575
622
|
}
|
|
576
623
|
if (text) {
|
|
577
624
|
const selectMatch = text.match(/^\/s_(\d+)$/);
|
|
578
|
-
|
|
579
|
-
|
|
625
|
+
const latest = this.latestSelection;
|
|
626
|
+
if (selectMatch && latest !== void 0) {
|
|
627
|
+
const pending = this.selections.get(latest);
|
|
580
628
|
if (pending) {
|
|
581
|
-
this.
|
|
629
|
+
this.selections.delete(latest);
|
|
582
630
|
const session = this.pendingSession(pending, parseInt(selectMatch[1], 10) - 1);
|
|
583
631
|
if (session) {
|
|
632
|
+
let delivered = true;
|
|
584
633
|
if (pending.photoFileId) {
|
|
585
|
-
await this.deliverPhotoToSessionByFileId(session.id, pending.photoFileId, pending.caption);
|
|
634
|
+
delivered = await this.deliverPhotoToSessionByFileId(session.id, pending.photoFileId, pending.caption);
|
|
586
635
|
} else if (pending.text) {
|
|
587
636
|
this.deliverToSession(session.id, pending.text);
|
|
588
637
|
}
|
|
589
|
-
this.sendMessage(`Sent to [${this.getLabel(session)}]`);
|
|
638
|
+
if (delivered) this.sendMessage(`Sent to [${this.getLabel(session)}]`);
|
|
590
639
|
} else {
|
|
591
640
|
this.sendMessage("Invalid session number.");
|
|
592
641
|
}
|
|
@@ -607,16 +656,19 @@ ${this.mdToHtml(message)}`;
|
|
|
607
656
|
}
|
|
608
657
|
return;
|
|
609
658
|
}
|
|
659
|
+
const selectionId = randomUUID2().replace(/-/g, "").slice(0, 8);
|
|
660
|
+
this.latestSelection = selectionId;
|
|
610
661
|
const sessionIds = sessions.map((s) => s.id);
|
|
611
662
|
if (hasPhoto) {
|
|
612
663
|
const largest = msg.photo[msg.photo.length - 1];
|
|
613
|
-
this.
|
|
664
|
+
this.selections.set(selectionId, { photoFileId: largest.file_id, caption: text, sessionIds });
|
|
614
665
|
} else {
|
|
615
|
-
this.
|
|
666
|
+
this.selections.set(selectionId, { text, sessionIds });
|
|
616
667
|
}
|
|
668
|
+
while (this.selections.size > MAX_SELECTIONS) this.selections.delete(this.selections.keys().next().value);
|
|
617
669
|
const buttons = sessions.map((s, i) => ({
|
|
618
670
|
text: this.getLabel(s),
|
|
619
|
-
callback_data: `
|
|
671
|
+
callback_data: `sel:${selectionId}:${i}`
|
|
620
672
|
}));
|
|
621
673
|
const rows = [];
|
|
622
674
|
for (let i = 0; i < buttons.length; i += 2) {
|
|
@@ -631,44 +683,54 @@ ${this.mdToHtml(message)}`;
|
|
|
631
683
|
}
|
|
632
684
|
async deliverPhotoToSession(sessionId2, photos, caption) {
|
|
633
685
|
const largest = photos[photos.length - 1];
|
|
634
|
-
|
|
686
|
+
return this.deliverPhotoToSessionByFileId(sessionId2, largest.file_id, caption);
|
|
635
687
|
}
|
|
636
688
|
async deliverPhotoToSessionByFileId(sessionId2, fileId, caption) {
|
|
637
689
|
try {
|
|
638
690
|
const fileRes = await fetch(`${this.apiUrl}/getFile?file_id=${fileId}`);
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
return;
|
|
691
|
+
const fileData = fileRes.ok ? await fileRes.json() : void 0;
|
|
692
|
+
if (!fileData?.ok || !fileData.result) {
|
|
693
|
+
this.photoNotDelivered("Telegram did not return the file");
|
|
694
|
+
return false;
|
|
642
695
|
}
|
|
643
|
-
const fileData = await fileRes.json();
|
|
644
|
-
if (!fileData.ok) return;
|
|
645
696
|
const downloadUrl = `https://api.telegram.org/file/bot${this.config.botToken}/${fileData.result.file_path}`;
|
|
646
697
|
const imgRes = await fetch(downloadUrl);
|
|
647
698
|
if (!imgRes.ok) {
|
|
648
|
-
|
|
649
|
-
return;
|
|
699
|
+
this.photoNotDelivered("the download failed");
|
|
700
|
+
return false;
|
|
650
701
|
}
|
|
651
702
|
const buffer = Buffer.from(await imgRes.arrayBuffer());
|
|
703
|
+
if (buffer.length > MAX_PHOTO_BYTES) {
|
|
704
|
+
this.photoNotDelivered("it is larger than 10 MB");
|
|
705
|
+
return false;
|
|
706
|
+
}
|
|
652
707
|
const ext = fileData.result.file_path.split(".").pop() || "jpg";
|
|
653
708
|
const mimeType = ext === "png" ? "image/png" : ext === "gif" ? "image/gif" : ext === "webp" ? "image/webp" : "image/jpeg";
|
|
654
|
-
|
|
709
|
+
fs5.mkdirSync(UPLOADS_DIR, { recursive: true });
|
|
655
710
|
const filename = `${randomUUID2()}.${ext}`;
|
|
656
|
-
const filePath =
|
|
657
|
-
|
|
711
|
+
const filePath = path6.join(UPLOADS_DIR, filename);
|
|
712
|
+
fs5.writeFileSync(filePath, buffer);
|
|
658
713
|
logger.info(`Telegram photo saved: ${filename} (${buffer.length} bytes)`);
|
|
659
714
|
if (this.onImageToSession) {
|
|
660
715
|
this.onImageToSession(sessionId2, filePath, mimeType, caption);
|
|
661
716
|
}
|
|
662
717
|
setTimeout(() => {
|
|
663
718
|
try {
|
|
664
|
-
|
|
719
|
+
fs5.unlinkSync(filePath);
|
|
665
720
|
} catch {
|
|
666
721
|
}
|
|
667
|
-
}, 5 * 60 * 1e3);
|
|
722
|
+
}, 5 * 60 * 1e3).unref();
|
|
723
|
+
return this.onImageToSession !== void 0;
|
|
668
724
|
} catch (err) {
|
|
669
725
|
logger.warn(`Telegram photo download failed: ${err.message}`);
|
|
726
|
+
this.photoNotDelivered("the download failed");
|
|
727
|
+
return false;
|
|
670
728
|
}
|
|
671
729
|
}
|
|
730
|
+
photoNotDelivered(reason) {
|
|
731
|
+
logger.warn(`Telegram photo not delivered: ${reason}`);
|
|
732
|
+
void this.sendMessage(`Photo not delivered: ${reason}`);
|
|
733
|
+
}
|
|
672
734
|
// Buttons are numbered against the list shown when they were sent; Codex sessions come and go, so the live list may have shifted.
|
|
673
735
|
pendingSession(pending, idx) {
|
|
674
736
|
const id = pending.sessionIds[idx];
|
|
@@ -782,7 +844,7 @@ ${this.escHtml(description)}` : "") + (slice ? `
|
|
|
782
844
|
async handleChoiceCallback(query) {
|
|
783
845
|
const choice = this.choiceTokens.get(query.data.slice("pc:".length));
|
|
784
846
|
if (!choice) {
|
|
785
|
-
await this.
|
|
847
|
+
await this.expire(query);
|
|
786
848
|
return;
|
|
787
849
|
}
|
|
788
850
|
this.dropChoiceTokens(choice.sessionId, choice.requestId);
|
|
@@ -811,10 +873,14 @@ ${this.escHtml(description)}` : "") + (slice ? `
|
|
|
811
873
|
async handleCallbackQuery(query) {
|
|
812
874
|
if (!query.data) return;
|
|
813
875
|
if (String(query.message?.chat.id) !== String(this.config.chatId)) return;
|
|
814
|
-
if (query.data.startsWith("
|
|
876
|
+
if (query.data.startsWith("sel:")) {
|
|
815
877
|
await this.handleSessionSelectCallback(query);
|
|
816
878
|
return;
|
|
817
879
|
}
|
|
880
|
+
if (query.data.startsWith("sess:")) {
|
|
881
|
+
await this.expire(query);
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
818
884
|
if (query.data.startsWith("pc:")) {
|
|
819
885
|
await this.handleChoiceCallback(query);
|
|
820
886
|
return;
|
|
@@ -837,19 +903,25 @@ ${label}`);
|
|
|
837
903
|
}
|
|
838
904
|
}
|
|
839
905
|
async handleSessionSelectCallback(query) {
|
|
840
|
-
const
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
906
|
+
const [, selectionId, idxStr] = query.data.split(":");
|
|
907
|
+
const pending = this.selections.get(selectionId);
|
|
908
|
+
if (!pending) {
|
|
909
|
+
await this.expire(query);
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
const session = this.pendingSession(pending, parseInt(idxStr, 10));
|
|
913
|
+
if (!session) {
|
|
847
914
|
await this.answerCallbackQuery(query.id, "Session not found");
|
|
848
915
|
return;
|
|
849
916
|
}
|
|
850
|
-
this.
|
|
917
|
+
this.selections.delete(selectionId);
|
|
851
918
|
if (pending.photoFileId) {
|
|
852
|
-
await this.deliverPhotoToSessionByFileId(session.id, pending.photoFileId, pending.caption);
|
|
919
|
+
const delivered = await this.deliverPhotoToSessionByFileId(session.id, pending.photoFileId, pending.caption);
|
|
920
|
+
if (!delivered) {
|
|
921
|
+
await this.answerCallbackQuery(query.id, "Photo not delivered");
|
|
922
|
+
if (query.message) await this.removeButtons(query.message.chat.id, query.message.message_id);
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
853
925
|
} else if (pending.text) {
|
|
854
926
|
this.deliverToSession(session.id, pending.text);
|
|
855
927
|
}
|
|
@@ -869,6 +941,21 @@ ${label}`);
|
|
|
869
941
|
logger.warn(`Telegram answerCallbackQuery error: ${err.message}`);
|
|
870
942
|
}
|
|
871
943
|
}
|
|
944
|
+
async expire(query) {
|
|
945
|
+
await this.answerCallbackQuery(query.id, "Expired");
|
|
946
|
+
if (query.message) await this.removeButtons(query.message.chat.id, query.message.message_id);
|
|
947
|
+
}
|
|
948
|
+
async removeButtons(chatId, messageId) {
|
|
949
|
+
try {
|
|
950
|
+
await fetch(`${this.apiUrl}/editMessageReplyMarkup`, {
|
|
951
|
+
method: "POST",
|
|
952
|
+
headers: { "Content-Type": "application/json" },
|
|
953
|
+
body: JSON.stringify({ chat_id: chatId, message_id: messageId, reply_markup: { inline_keyboard: [] } })
|
|
954
|
+
});
|
|
955
|
+
} catch (err) {
|
|
956
|
+
logger.warn(`Telegram editMessageReplyMarkup error: ${err.message}`);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
872
959
|
async editMessageText(chatId, messageId, text) {
|
|
873
960
|
try {
|
|
874
961
|
await fetch(`${this.apiUrl}/editMessageText`, {
|
|
@@ -913,8 +1000,8 @@ ${label}`);
|
|
|
913
1000
|
css: "css",
|
|
914
1001
|
scss: "scss"
|
|
915
1002
|
};
|
|
916
|
-
langOf(
|
|
917
|
-
const m = /\.([a-z0-9]+)$/i.exec(
|
|
1003
|
+
langOf(path12) {
|
|
1004
|
+
const m = /\.([a-z0-9]+)$/i.exec(path12 || "");
|
|
918
1005
|
return m ? _TelegramBot.LANG_BY_EXT[m[1].toLowerCase()] ?? "" : "";
|
|
919
1006
|
}
|
|
920
1007
|
/** Convert markdown to Telegram HTML (escape first, then apply formatting) */
|
|
@@ -961,10 +1048,15 @@ ${bodyLines.join("\n")}`;
|
|
|
961
1048
|
|
|
962
1049
|
// src/hub/codex-supervisor.ts
|
|
963
1050
|
import { spawn as spawn2 } from "child_process";
|
|
964
|
-
import
|
|
965
|
-
import
|
|
1051
|
+
import fs6 from "fs";
|
|
1052
|
+
import path7 from "path";
|
|
966
1053
|
function resolveAdapterScript(baseDir) {
|
|
967
|
-
return [
|
|
1054
|
+
return [path7.join(baseDir, "..", "codex", "main.js"), path7.join(baseDir, "codex", "main.js")].find((p) => fs6.existsSync(p));
|
|
1055
|
+
}
|
|
1056
|
+
function adapterEnv(base, hub) {
|
|
1057
|
+
const env = { ...base, CLAUDE_ALARM_HUB_HOST: hub.host, CLAUDE_ALARM_HUB_PORT: String(hub.port) };
|
|
1058
|
+
delete env.CLAUDE_ALARM_HUB_TOKEN;
|
|
1059
|
+
return env;
|
|
968
1060
|
}
|
|
969
1061
|
var HEALTHY_RUN_MS, CodexSupervisor;
|
|
970
1062
|
var init_codex_supervisor = __esm({
|
|
@@ -973,18 +1065,24 @@ var init_codex_supervisor = __esm({
|
|
|
973
1065
|
init_logger();
|
|
974
1066
|
HEALTHY_RUN_MS = 6e4;
|
|
975
1067
|
CodexSupervisor = class {
|
|
976
|
-
constructor(script,
|
|
1068
|
+
constructor(script, opts = {}) {
|
|
977
1069
|
this.script = script;
|
|
978
|
-
this.spawnFn = spawnFn;
|
|
979
|
-
this.minDelayMs = minDelayMs;
|
|
980
|
-
this.maxDelayMs = maxDelayMs;
|
|
981
|
-
this.stopGraceMs = stopGraceMs;
|
|
982
|
-
this.
|
|
1070
|
+
this.spawnFn = opts.spawnFn ?? spawn2;
|
|
1071
|
+
this.minDelayMs = opts.minDelayMs ?? 2e3;
|
|
1072
|
+
this.maxDelayMs = opts.maxDelayMs ?? 6e4;
|
|
1073
|
+
this.stopGraceMs = opts.stopGraceMs ?? 3e3;
|
|
1074
|
+
this.env = opts.env;
|
|
1075
|
+
this.delay = this.minDelayMs;
|
|
983
1076
|
}
|
|
984
1077
|
child;
|
|
985
1078
|
timer;
|
|
986
1079
|
stopped = true;
|
|
987
1080
|
delay;
|
|
1081
|
+
spawnFn;
|
|
1082
|
+
minDelayMs;
|
|
1083
|
+
maxDelayMs;
|
|
1084
|
+
stopGraceMs;
|
|
1085
|
+
env;
|
|
988
1086
|
start() {
|
|
989
1087
|
this.stopped = false;
|
|
990
1088
|
this.launch();
|
|
@@ -1006,7 +1104,8 @@ var init_codex_supervisor = __esm({
|
|
|
1006
1104
|
const child = this.spawnFn(process.execPath, [this.script, "--watch-stdin"], {
|
|
1007
1105
|
detached: true,
|
|
1008
1106
|
stdio: ["pipe", "pipe", "pipe"],
|
|
1009
|
-
windowsHide: true
|
|
1107
|
+
windowsHide: true,
|
|
1108
|
+
...this.env ? { env: this.env } : {}
|
|
1010
1109
|
});
|
|
1011
1110
|
this.child = child;
|
|
1012
1111
|
child.stdout?.on("data", (d) => process.stdout.write(d));
|
|
@@ -1030,16 +1129,28 @@ var init_codex_supervisor = __esm({
|
|
|
1030
1129
|
}
|
|
1031
1130
|
});
|
|
1032
1131
|
|
|
1132
|
+
// src/shared/entry.ts
|
|
1133
|
+
function isEntryScript(argv1, suffixes) {
|
|
1134
|
+
if (!argv1) return false;
|
|
1135
|
+
const normalized = argv1.replace(/\\/g, "/");
|
|
1136
|
+
return suffixes.some((suffix) => normalized.endsWith(suffix));
|
|
1137
|
+
}
|
|
1138
|
+
var init_entry = __esm({
|
|
1139
|
+
"src/shared/entry.ts"() {
|
|
1140
|
+
"use strict";
|
|
1141
|
+
}
|
|
1142
|
+
});
|
|
1143
|
+
|
|
1033
1144
|
// src/hub/auth.ts
|
|
1034
|
-
import
|
|
1145
|
+
import crypto2 from "crypto";
|
|
1035
1146
|
function safeEqual(a, b) {
|
|
1036
1147
|
if (typeof a !== "string") return false;
|
|
1037
1148
|
const x = Buffer.from(a);
|
|
1038
1149
|
const y = Buffer.from(b);
|
|
1039
|
-
return x.length === y.length &&
|
|
1150
|
+
return x.length === y.length && crypto2.timingSafeEqual(x, y);
|
|
1040
1151
|
}
|
|
1041
1152
|
function dashboardCookieValue(token) {
|
|
1042
|
-
return
|
|
1153
|
+
return crypto2.createHmac("sha256", token).update("claude-alarm-dashboard").digest("hex");
|
|
1043
1154
|
}
|
|
1044
1155
|
function parseCookies(header) {
|
|
1045
1156
|
const out = {};
|
|
@@ -1099,8 +1210,8 @@ __export(server_exports, {
|
|
|
1099
1210
|
HubServer: () => HubServer
|
|
1100
1211
|
});
|
|
1101
1212
|
import http from "http";
|
|
1102
|
-
import
|
|
1103
|
-
import
|
|
1213
|
+
import fs7 from "fs";
|
|
1214
|
+
import path8 from "path";
|
|
1104
1215
|
import { fileURLToPath } from "url";
|
|
1105
1216
|
import { WebSocketServer, WebSocket as WebSocket2 } from "ws";
|
|
1106
1217
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
@@ -1109,7 +1220,7 @@ function validChoices(choices) {
|
|
|
1109
1220
|
const valid = choices.filter((c) => typeof c?.id === "string" && typeof c?.label === "string");
|
|
1110
1221
|
return valid.length ? valid : void 0;
|
|
1111
1222
|
}
|
|
1112
|
-
var __dirname, MAX_WS_PAYLOAD, HubServer;
|
|
1223
|
+
var __dirname, MAX_WS_PAYLOAD, nonEmpty, HubServer;
|
|
1113
1224
|
var init_server = __esm({
|
|
1114
1225
|
"src/hub/server.ts"() {
|
|
1115
1226
|
"use strict";
|
|
@@ -1123,9 +1234,12 @@ var init_server = __esm({
|
|
|
1123
1234
|
init_session_label();
|
|
1124
1235
|
init_crash_guard();
|
|
1125
1236
|
init_permission_key();
|
|
1237
|
+
init_entry();
|
|
1238
|
+
init_hub_url();
|
|
1126
1239
|
init_auth();
|
|
1127
|
-
__dirname =
|
|
1240
|
+
__dirname = path8.dirname(fileURLToPath(import.meta.url));
|
|
1128
1241
|
MAX_WS_PAYLOAD = 16 * 1024 * 1024;
|
|
1242
|
+
nonEmpty = (v) => typeof v === "string" && v.trim() !== "";
|
|
1129
1243
|
HubServer = class {
|
|
1130
1244
|
httpServer;
|
|
1131
1245
|
wssChannel;
|
|
@@ -1151,7 +1265,13 @@ var init_server = __esm({
|
|
|
1151
1265
|
token;
|
|
1152
1266
|
codexEnabled;
|
|
1153
1267
|
codexSupervisor;
|
|
1154
|
-
|
|
1268
|
+
wssCodex;
|
|
1269
|
+
codexAdapters = /* @__PURE__ */ new Map();
|
|
1270
|
+
codexAlive = /* @__PURE__ */ new WeakMap();
|
|
1271
|
+
codexCallTimeoutMs;
|
|
1272
|
+
codexCalls = /* @__PURE__ */ new Map();
|
|
1273
|
+
constructor(config2, options = {}) {
|
|
1274
|
+
this.codexCallTimeoutMs = options.codexCallTimeoutMs ?? 6e4;
|
|
1155
1275
|
this.host = config2?.hub?.host ?? DEFAULT_HUB_HOST;
|
|
1156
1276
|
this.port = config2?.hub?.port ?? DEFAULT_HUB_PORT;
|
|
1157
1277
|
this.token = config2?.hub?.token;
|
|
@@ -1164,7 +1284,7 @@ var init_server = __esm({
|
|
|
1164
1284
|
if (config2?.webhooks) {
|
|
1165
1285
|
this.notifier.configure({ webhooks: config2.webhooks });
|
|
1166
1286
|
}
|
|
1167
|
-
const displayHost = this.host
|
|
1287
|
+
const displayHost = hubUrlHost(this.host);
|
|
1168
1288
|
this.notifier.configure({ dashboardUrl: `http://${displayHost}:${this.port}` });
|
|
1169
1289
|
const fullConfig = loadConfig();
|
|
1170
1290
|
if (fullConfig.telegram?.enabled && fullConfig.telegram.botToken && fullConfig.telegram.chatId) {
|
|
@@ -1177,6 +1297,9 @@ var init_server = __esm({
|
|
|
1177
1297
|
this.wssDashboard = new WebSocketServer({ noServer: true, maxPayload: MAX_WS_PAYLOAD });
|
|
1178
1298
|
this.wssDashboard.on("connection", (ws) => this.handleDashboardConnection(ws));
|
|
1179
1299
|
this.wssDashboard.on("error", (err) => logger.warn(`Dashboard WebSocket server error: ${err.message}`));
|
|
1300
|
+
this.wssCodex = new WebSocketServer({ noServer: true, maxPayload: MAX_WS_PAYLOAD });
|
|
1301
|
+
this.wssCodex.on("connection", (ws, req) => this.handleCodexConnection(ws, req));
|
|
1302
|
+
this.wssCodex.on("error", (err) => logger.warn(`Codex WebSocket server error: ${err.message}`));
|
|
1180
1303
|
this.httpServer.on("upgrade", (req, socket, head) => {
|
|
1181
1304
|
socket.on("error", (err) => logger.debug(`Upgrade socket error: ${err.message}`));
|
|
1182
1305
|
const url = new URL(req.url ?? "/", "http://hub");
|
|
@@ -1197,6 +1320,10 @@ var init_server = __esm({
|
|
|
1197
1320
|
this.wssDashboard.handleUpgrade(req, socket, head, (ws) => {
|
|
1198
1321
|
this.wssDashboard.emit("connection", ws, req);
|
|
1199
1322
|
});
|
|
1323
|
+
} else if (pathname === WS_PATH_CODEX) {
|
|
1324
|
+
this.wssCodex.handleUpgrade(req, socket, head, (ws) => {
|
|
1325
|
+
this.wssCodex.emit("connection", ws, req);
|
|
1326
|
+
});
|
|
1200
1327
|
} else {
|
|
1201
1328
|
socket.destroy();
|
|
1202
1329
|
}
|
|
@@ -1211,7 +1338,7 @@ var init_server = __esm({
|
|
|
1211
1338
|
this.httpServer.listen(this.port, this.host, () => {
|
|
1212
1339
|
this.httpServer.removeListener("error", onStartupError);
|
|
1213
1340
|
this.httpServer.on("error", (err) => logger.error(`HTTP server error: ${err.message}`));
|
|
1214
|
-
const displayHost = this.host
|
|
1341
|
+
const displayHost = hubUrlHost(this.host);
|
|
1215
1342
|
logger.info(`Hub server listening on http://${displayHost}:${this.port}`);
|
|
1216
1343
|
this.startCodexAdapter();
|
|
1217
1344
|
resolve();
|
|
@@ -1226,10 +1353,12 @@ var init_server = __esm({
|
|
|
1226
1353
|
if (this.telegramBot) this.telegramBot.stopPolling();
|
|
1227
1354
|
for (const ws of this.channelSockets.values()) ws.terminate();
|
|
1228
1355
|
for (const ws of this.dashboardSockets) ws.terminate();
|
|
1356
|
+
for (const { ws } of this.codexAdapters.values()) ws.terminate();
|
|
1229
1357
|
this.channelSockets.clear();
|
|
1230
1358
|
this.dashboardSockets.clear();
|
|
1231
1359
|
this.wssChannel.close();
|
|
1232
1360
|
this.wssDashboard.close();
|
|
1361
|
+
this.wssCodex.close();
|
|
1233
1362
|
this.httpServer.close(() => {
|
|
1234
1363
|
logger.info("Hub server stopped");
|
|
1235
1364
|
resolve();
|
|
@@ -1294,6 +1423,12 @@ var init_server = __esm({
|
|
|
1294
1423
|
this.handleApiSend(req, res);
|
|
1295
1424
|
} else if (url.pathname === "/api/notify" && req.method === "POST") {
|
|
1296
1425
|
this.handleApiNotify(req, res);
|
|
1426
|
+
} else if (url.pathname === "/api/codex/folders" && req.method === "GET") {
|
|
1427
|
+
this.handleCodexFolders(url, res);
|
|
1428
|
+
} else if (url.pathname === "/api/codex/threads" && req.method === "POST") {
|
|
1429
|
+
this.handleCodexCreate(req, res);
|
|
1430
|
+
} else if (url.pathname === "/api/codex/threads/close" && req.method === "POST") {
|
|
1431
|
+
this.handleCodexClose(req, res);
|
|
1297
1432
|
} else if (url.pathname === "/api/webhooks" && req.method === "GET") {
|
|
1298
1433
|
const config2 = loadConfig();
|
|
1299
1434
|
this.jsonResponse(res, 200, { webhooks: config2.webhooks || [] });
|
|
@@ -1323,23 +1458,23 @@ var init_server = __esm({
|
|
|
1323
1458
|
return;
|
|
1324
1459
|
}
|
|
1325
1460
|
const candidates = [
|
|
1326
|
-
|
|
1461
|
+
path8.join(__dirname, "..", "dashboard", "index.html"),
|
|
1327
1462
|
// from dist/hub/
|
|
1328
|
-
|
|
1463
|
+
path8.join(__dirname, "dashboard", "index.html"),
|
|
1329
1464
|
// from dist/ (bundled index.js)
|
|
1330
|
-
|
|
1465
|
+
path8.join(__dirname, "..", "..", "src", "dashboard", "index.html"),
|
|
1331
1466
|
// from dist/hub/ -> src/
|
|
1332
|
-
|
|
1467
|
+
path8.join(__dirname, "..", "src", "dashboard", "index.html"),
|
|
1333
1468
|
// from dist/ -> src/
|
|
1334
|
-
|
|
1469
|
+
path8.join(process.cwd(), "dist", "dashboard", "index.html"),
|
|
1335
1470
|
// from cwd
|
|
1336
|
-
|
|
1471
|
+
path8.join(process.cwd(), "src", "dashboard", "index.html")
|
|
1337
1472
|
// from cwd/src
|
|
1338
1473
|
];
|
|
1339
1474
|
logger.debug(`Dashboard candidates: ${JSON.stringify(candidates)}`);
|
|
1340
1475
|
for (const candidate of candidates) {
|
|
1341
|
-
if (
|
|
1342
|
-
const html =
|
|
1476
|
+
if (fs7.existsSync(candidate)) {
|
|
1477
|
+
const html = fs7.readFileSync(candidate, "utf-8");
|
|
1343
1478
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
1344
1479
|
res.end(html);
|
|
1345
1480
|
return;
|
|
@@ -1557,6 +1692,114 @@ var init_server = __esm({
|
|
|
1557
1692
|
}
|
|
1558
1693
|
}
|
|
1559
1694
|
}
|
|
1695
|
+
handleCodexConnection(ws, req) {
|
|
1696
|
+
const isLocal = this.isLocalRequest(req);
|
|
1697
|
+
let adapterId;
|
|
1698
|
+
ws.on("error", (err) => {
|
|
1699
|
+
logger.warn(`Codex adapter WebSocket error: ${err.message}`);
|
|
1700
|
+
ws.terminate();
|
|
1701
|
+
});
|
|
1702
|
+
ws.on("pong", () => this.codexAlive.set(ws, true));
|
|
1703
|
+
ws.on("message", (data) => {
|
|
1704
|
+
let msg;
|
|
1705
|
+
try {
|
|
1706
|
+
msg = JSON.parse(data.toString());
|
|
1707
|
+
if (msg === null || typeof msg !== "object") {
|
|
1708
|
+
logger.warn("Invalid message from Codex adapter");
|
|
1709
|
+
return;
|
|
1710
|
+
}
|
|
1711
|
+
} catch {
|
|
1712
|
+
logger.warn("Invalid message from Codex adapter");
|
|
1713
|
+
return;
|
|
1714
|
+
}
|
|
1715
|
+
if (msg.type === "adapter_hello") {
|
|
1716
|
+
const a = msg.adapter;
|
|
1717
|
+
if (typeof a?.id !== "string" || !a.id || typeof a.host !== "string") return;
|
|
1718
|
+
if (adapterId && adapterId !== a.id) return;
|
|
1719
|
+
const holder = this.codexAdapters.get(a.id);
|
|
1720
|
+
if (holder && holder.ws !== ws) holder.ws.terminate();
|
|
1721
|
+
if (!adapterId) logger.info(`Codex adapter connected: ${a.host} (${a.id}, local: ${isLocal})`);
|
|
1722
|
+
adapterId = a.id;
|
|
1723
|
+
this.codexAdapters.set(a.id, { ws, info: { id: a.id, host: a.host, ready: a.ready === true, isLocal } });
|
|
1724
|
+
this.broadcastCodexAdapters();
|
|
1725
|
+
} else if (msg.type === "adapter_result" && adapterId) {
|
|
1726
|
+
const call = this.codexCalls.get(msg.requestId);
|
|
1727
|
+
if (!call || call.adapterId !== adapterId) return;
|
|
1728
|
+
call.settle(msg.ok ? { status: 200, body: msg.data } : { status: 422, body: { error: String(msg.error) } });
|
|
1729
|
+
}
|
|
1730
|
+
});
|
|
1731
|
+
ws.on("close", () => {
|
|
1732
|
+
if (!adapterId || this.codexAdapters.get(adapterId)?.ws !== ws) return;
|
|
1733
|
+
this.codexAdapters.delete(adapterId);
|
|
1734
|
+
for (const call of [...this.codexCalls.values()]) {
|
|
1735
|
+
if (call.adapterId === adapterId) call.settle({ status: 502, body: { error: "Codex adapter disconnected" } });
|
|
1736
|
+
}
|
|
1737
|
+
logger.info(`Codex adapter disconnected: ${adapterId}`);
|
|
1738
|
+
this.broadcastCodexAdapters();
|
|
1739
|
+
});
|
|
1740
|
+
}
|
|
1741
|
+
callCodexAdapter(adapterId, call) {
|
|
1742
|
+
const adapter = this.codexAdapters.get(adapterId);
|
|
1743
|
+
if (!adapter || adapter.ws.readyState !== WebSocket2.OPEN) {
|
|
1744
|
+
return Promise.resolve({ status: 404, body: { error: "Codex adapter is not connected" } });
|
|
1745
|
+
}
|
|
1746
|
+
const requestId = randomUUID3();
|
|
1747
|
+
return new Promise((resolve) => {
|
|
1748
|
+
const timer = setTimeout(
|
|
1749
|
+
() => settle({ status: 504, body: { error: "Codex did not respond in time. The conversation may still appear." } }),
|
|
1750
|
+
this.codexCallTimeoutMs
|
|
1751
|
+
);
|
|
1752
|
+
const settle = (outcome) => {
|
|
1753
|
+
clearTimeout(timer);
|
|
1754
|
+
this.codexCalls.delete(requestId);
|
|
1755
|
+
resolve(outcome);
|
|
1756
|
+
};
|
|
1757
|
+
this.codexCalls.set(requestId, { adapterId, settle });
|
|
1758
|
+
adapter.ws.send(JSON.stringify({ type: "adapter_call", requestId, call }));
|
|
1759
|
+
});
|
|
1760
|
+
}
|
|
1761
|
+
async handleCodexFolders(url, res) {
|
|
1762
|
+
const adapterId = url.searchParams.get("adapterId");
|
|
1763
|
+
if (!nonEmpty(adapterId)) {
|
|
1764
|
+
this.jsonResponse(res, 400, { error: "adapterId is required" });
|
|
1765
|
+
return;
|
|
1766
|
+
}
|
|
1767
|
+
const out = await this.callCodexAdapter(adapterId, { kind: "folders" });
|
|
1768
|
+
this.jsonResponse(res, out.status, out.body);
|
|
1769
|
+
}
|
|
1770
|
+
async handleCodexCreate(req, res) {
|
|
1771
|
+
const body = await this.readBody(req);
|
|
1772
|
+
const adapterId = body?.adapterId;
|
|
1773
|
+
const cwd = body?.cwd;
|
|
1774
|
+
if (!nonEmpty(adapterId) || !nonEmpty(cwd)) {
|
|
1775
|
+
this.jsonResponse(res, 400, { error: "adapterId and cwd are required" });
|
|
1776
|
+
return;
|
|
1777
|
+
}
|
|
1778
|
+
const out = await this.callCodexAdapter(adapterId, { kind: "create", cwd });
|
|
1779
|
+
this.jsonResponse(res, out.status, out.body);
|
|
1780
|
+
}
|
|
1781
|
+
async handleCodexClose(req, res) {
|
|
1782
|
+
const body = await this.readBody(req);
|
|
1783
|
+
const sessionId2 = body?.sessionId;
|
|
1784
|
+
if (!nonEmpty(sessionId2)) {
|
|
1785
|
+
this.jsonResponse(res, 400, { error: "sessionId is required" });
|
|
1786
|
+
return;
|
|
1787
|
+
}
|
|
1788
|
+
const session = this.sessions.get(sessionId2);
|
|
1789
|
+
const ws = this.channelSockets.get(sessionId2);
|
|
1790
|
+
if (session?.agentKind !== "codex" || !session.closable || ws?.readyState !== WebSocket2.OPEN) {
|
|
1791
|
+
this.jsonResponse(res, 404, { error: "No closable Codex conversation" });
|
|
1792
|
+
return;
|
|
1793
|
+
}
|
|
1794
|
+
ws.send(JSON.stringify({ type: "codex_close", sessionId: sessionId2 }));
|
|
1795
|
+
this.jsonResponse(res, 200, { ok: true });
|
|
1796
|
+
}
|
|
1797
|
+
codexAdapterList() {
|
|
1798
|
+
return [...this.codexAdapters.values()].map((a) => a.info);
|
|
1799
|
+
}
|
|
1800
|
+
broadcastCodexAdapters() {
|
|
1801
|
+
this.broadcastToDashboards({ type: "codex_adapters", adapters: this.codexAdapterList() });
|
|
1802
|
+
}
|
|
1560
1803
|
// --- Dashboard WebSocket ---
|
|
1561
1804
|
handleDashboardConnection(ws) {
|
|
1562
1805
|
this.dashboardSockets.add(ws);
|
|
@@ -1575,6 +1818,7 @@ var init_server = __esm({
|
|
|
1575
1818
|
requests: [...this.choiceRequests.values()].map((c) => c.request)
|
|
1576
1819
|
};
|
|
1577
1820
|
ws.send(JSON.stringify(pendingMsg));
|
|
1821
|
+
ws.send(JSON.stringify({ type: "codex_adapters", adapters: this.codexAdapterList() }));
|
|
1578
1822
|
ws.on("message", (data) => {
|
|
1579
1823
|
try {
|
|
1580
1824
|
const msg = JSON.parse(data.toString());
|
|
@@ -1584,7 +1828,7 @@ var init_server = __esm({
|
|
|
1584
1828
|
channelWs.send(JSON.stringify({ ...msg, source: "dashboard" }));
|
|
1585
1829
|
}
|
|
1586
1830
|
} else if (msg.type === "image_upload") {
|
|
1587
|
-
this.handleImageUpload(msg);
|
|
1831
|
+
this.handleImageUpload(ws, msg);
|
|
1588
1832
|
} else if (msg.type === "permission_response") {
|
|
1589
1833
|
if (this.forwardPermissionResponse(msg)) {
|
|
1590
1834
|
const verdict = msg.choiceId !== void 0 ? `choice ${msg.choiceId}` : msg.behavior;
|
|
@@ -1635,43 +1879,55 @@ var init_server = __esm({
|
|
|
1635
1879
|
}
|
|
1636
1880
|
}
|
|
1637
1881
|
}
|
|
1638
|
-
handleImageUpload(msg) {
|
|
1882
|
+
handleImageUpload(ws, msg) {
|
|
1639
1883
|
const { sessionId: sessionId2, imageData, mimeType, originalName, content } = msg;
|
|
1884
|
+
const reject = (reason) => {
|
|
1885
|
+
logger.warn(`Image upload rejected for ${sessionId2}: ${reason}`);
|
|
1886
|
+
const rejected = { type: "upload_rejected", sessionId: sessionId2, reason, withText: typeof content === "string" && content.trim().length > 0 };
|
|
1887
|
+
if (ws.readyState === WebSocket2.OPEN) ws.send(JSON.stringify(rejected));
|
|
1888
|
+
};
|
|
1889
|
+
const channelWs = this.channelSockets.get(sessionId2);
|
|
1890
|
+
if (!channelWs || channelWs.readyState !== WebSocket2.OPEN) {
|
|
1891
|
+
reject("the session is not connected");
|
|
1892
|
+
return;
|
|
1893
|
+
}
|
|
1640
1894
|
if (!this.localChannels.has(sessionId2)) {
|
|
1641
|
-
|
|
1895
|
+
reject("this session is on another PC; images can only go to sessions on the hub's PC");
|
|
1642
1896
|
return;
|
|
1643
1897
|
}
|
|
1644
|
-
const channelWs = this.channelSockets.get(sessionId2);
|
|
1645
|
-
if (!channelWs || channelWs.readyState !== WebSocket2.OPEN) return;
|
|
1646
1898
|
const allowedTypes = ["image/png", "image/jpeg", "image/gif", "image/webp"];
|
|
1647
|
-
if (!allowedTypes.includes(mimeType))
|
|
1899
|
+
if (!allowedTypes.includes(mimeType)) {
|
|
1900
|
+
reject("only PNG, JPEG, GIF and WebP images are supported");
|
|
1901
|
+
return;
|
|
1902
|
+
}
|
|
1648
1903
|
const base64Data = imageData.replace(/^data:image\/\w+;base64,/, "");
|
|
1649
1904
|
const buffer = Buffer.from(base64Data, "base64");
|
|
1650
1905
|
if (buffer.length > 10 * 1024 * 1024) {
|
|
1651
|
-
|
|
1906
|
+
reject("the image is larger than 10 MB");
|
|
1652
1907
|
return;
|
|
1653
1908
|
}
|
|
1654
|
-
|
|
1909
|
+
fs7.mkdirSync(UPLOADS_DIR, { recursive: true });
|
|
1655
1910
|
const ext = mimeType.split("/")[1] === "jpeg" ? "jpg" : mimeType.split("/")[1];
|
|
1656
1911
|
const filename = `${randomUUID3()}.${ext}`;
|
|
1657
|
-
const filePath =
|
|
1658
|
-
|
|
1912
|
+
const filePath = path8.join(UPLOADS_DIR, filename);
|
|
1913
|
+
fs7.writeFileSync(filePath, buffer);
|
|
1659
1914
|
const forwardMsg = {
|
|
1660
1915
|
type: "image_to_session",
|
|
1661
1916
|
sessionId: sessionId2,
|
|
1662
1917
|
imagePath: filePath,
|
|
1663
1918
|
mimeType,
|
|
1664
1919
|
originalName,
|
|
1665
|
-
content
|
|
1920
|
+
content,
|
|
1921
|
+
source: "dashboard"
|
|
1666
1922
|
};
|
|
1667
1923
|
channelWs.send(JSON.stringify(forwardMsg));
|
|
1668
1924
|
logger.info(`Image saved and forwarded: ${filename} (${buffer.length} bytes)`);
|
|
1669
1925
|
setTimeout(() => {
|
|
1670
1926
|
try {
|
|
1671
|
-
|
|
1927
|
+
fs7.unlinkSync(filePath);
|
|
1672
1928
|
} catch {
|
|
1673
1929
|
}
|
|
1674
|
-
}, 5 * 60 * 1e3);
|
|
1930
|
+
}, 5 * 60 * 1e3).unref();
|
|
1675
1931
|
}
|
|
1676
1932
|
async handleWebhookSave(req, res) {
|
|
1677
1933
|
const body = await this.readBody(req);
|
|
@@ -1704,7 +1960,7 @@ var init_server = __esm({
|
|
|
1704
1960
|
this.telegramBot.onImageToSession = (sessionId2, imagePath, mimeType, caption) => {
|
|
1705
1961
|
const channelWs = this.channelSockets.get(sessionId2);
|
|
1706
1962
|
if (channelWs?.readyState === WebSocket2.OPEN) {
|
|
1707
|
-
const msg = { type: "image_to_session", sessionId: sessionId2, imagePath, mimeType, content: caption };
|
|
1963
|
+
const msg = { type: "image_to_session", sessionId: sessionId2, imagePath, mimeType, content: caption, source: "telegram" };
|
|
1708
1964
|
channelWs.send(JSON.stringify(msg));
|
|
1709
1965
|
logger.info(`Telegram photo forwarded to session: ${sessionId2}`);
|
|
1710
1966
|
}
|
|
@@ -1832,7 +2088,7 @@ var init_server = __esm({
|
|
|
1832
2088
|
logger.warn("Codex adapter is enabled but codex/main.js was not found next to the hub");
|
|
1833
2089
|
return;
|
|
1834
2090
|
}
|
|
1835
|
-
this.codexSupervisor = new CodexSupervisor(script);
|
|
2091
|
+
this.codexSupervisor = new CodexSupervisor(script, { env: adapterEnv(process.env, { host: this.host, port: this.port }) });
|
|
1836
2092
|
this.codexSupervisor.start();
|
|
1837
2093
|
logger.info("Codex adapter started");
|
|
1838
2094
|
}
|
|
@@ -1847,15 +2103,23 @@ var init_server = __esm({
|
|
|
1847
2103
|
this.channelAlive.set(sessionId2, false);
|
|
1848
2104
|
ws.ping();
|
|
1849
2105
|
}
|
|
2106
|
+
for (const { ws } of this.codexAdapters.values()) {
|
|
2107
|
+
if (this.codexAlive.get(ws) === false) {
|
|
2108
|
+
ws.terminate();
|
|
2109
|
+
continue;
|
|
2110
|
+
}
|
|
2111
|
+
this.codexAlive.set(ws, false);
|
|
2112
|
+
ws.ping();
|
|
2113
|
+
}
|
|
1850
2114
|
}, 3e4);
|
|
1851
2115
|
}
|
|
1852
2116
|
cleanupUploads() {
|
|
1853
2117
|
try {
|
|
1854
|
-
if (!
|
|
1855
|
-
const files =
|
|
2118
|
+
if (!fs7.existsSync(UPLOADS_DIR)) return;
|
|
2119
|
+
const files = fs7.readdirSync(UPLOADS_DIR);
|
|
1856
2120
|
for (const file of files) {
|
|
1857
2121
|
try {
|
|
1858
|
-
|
|
2122
|
+
fs7.unlinkSync(path8.join(UPLOADS_DIR, file));
|
|
1859
2123
|
} catch {
|
|
1860
2124
|
}
|
|
1861
2125
|
}
|
|
@@ -1909,7 +2173,7 @@ Content-Length: 0\r
|
|
|
1909
2173
|
});
|
|
1910
2174
|
}
|
|
1911
2175
|
};
|
|
1912
|
-
if (process.argv[1]
|
|
2176
|
+
if (isEntryScript(process.argv[1], ["/hub/server.js", "/hub/server.ts"])) {
|
|
1913
2177
|
installCrashGuard("hub daemon");
|
|
1914
2178
|
logStartup("Hub daemon");
|
|
1915
2179
|
const config2 = loadConfig();
|
|
@@ -2054,9 +2318,9 @@ var init_hub_client = __esm({
|
|
|
2054
2318
|
});
|
|
2055
2319
|
|
|
2056
2320
|
// src/channel/peer-name.ts
|
|
2057
|
-
import
|
|
2321
|
+
import fs8 from "fs";
|
|
2058
2322
|
import os2 from "os";
|
|
2059
|
-
import
|
|
2323
|
+
import path9 from "path";
|
|
2060
2324
|
function findPeerName(records, env) {
|
|
2061
2325
|
const valid = records.filter((r) => typeof r === "object" && r !== null);
|
|
2062
2326
|
const nameOf = (match) => {
|
|
@@ -2076,18 +2340,18 @@ function readPeerName(env = process.env) {
|
|
|
2076
2340
|
sessionId: env.CLAUDE_CODE_SESSION_ID
|
|
2077
2341
|
};
|
|
2078
2342
|
if (!lookup.messagingSocket && !lookup.sessionId) return void 0;
|
|
2079
|
-
const configDir = env.CLAUDE_CONFIG_DIR ||
|
|
2080
|
-
const dir =
|
|
2343
|
+
const configDir = env.CLAUDE_CONFIG_DIR || path9.join(os2.homedir(), ".claude");
|
|
2344
|
+
const dir = path9.join(configDir, "sessions");
|
|
2081
2345
|
let files;
|
|
2082
2346
|
try {
|
|
2083
|
-
files =
|
|
2347
|
+
files = fs8.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
2084
2348
|
} catch {
|
|
2085
2349
|
return void 0;
|
|
2086
2350
|
}
|
|
2087
2351
|
const records = [];
|
|
2088
2352
|
for (const f of files) {
|
|
2089
2353
|
try {
|
|
2090
|
-
records.push(JSON.parse(
|
|
2354
|
+
records.push(JSON.parse(fs8.readFileSync(path9.join(dir, f), "utf8")));
|
|
2091
2355
|
} catch {
|
|
2092
2356
|
}
|
|
2093
2357
|
}
|
|
@@ -2110,7 +2374,7 @@ import {
|
|
|
2110
2374
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
2111
2375
|
import { z } from "zod";
|
|
2112
2376
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
2113
|
-
import
|
|
2377
|
+
import path10 from "path";
|
|
2114
2378
|
async function main() {
|
|
2115
2379
|
logger.info(`Starting MCP channel server (session: ${sessionId})`);
|
|
2116
2380
|
hubClient.connect();
|
|
@@ -2179,7 +2443,7 @@ var init_server2 = __esm({
|
|
|
2179
2443
|
init_hub_client();
|
|
2180
2444
|
init_peer_name2();
|
|
2181
2445
|
sessionId = randomUUID4();
|
|
2182
|
-
sessionName = process.env.CLAUDE_ALARM_SESSION_NAME ??
|
|
2446
|
+
sessionName = process.env.CLAUDE_ALARM_SESSION_NAME ?? path10.basename(process.cwd());
|
|
2183
2447
|
peerName = readPeerName();
|
|
2184
2448
|
server = new Server(
|
|
2185
2449
|
{
|
|
@@ -2356,8 +2620,8 @@ NOTIFICATIONS:
|
|
|
2356
2620
|
// src/cli.ts
|
|
2357
2621
|
init_config();
|
|
2358
2622
|
import { spawn as spawn3 } from "child_process";
|
|
2359
|
-
import
|
|
2360
|
-
import
|
|
2623
|
+
import fs9 from "fs";
|
|
2624
|
+
import path11 from "path";
|
|
2361
2625
|
import readline from "readline";
|
|
2362
2626
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2363
2627
|
|
|
@@ -2368,6 +2632,12 @@ import fs2 from "fs";
|
|
|
2368
2632
|
import path3 from "path";
|
|
2369
2633
|
import { Duplex } from "stream";
|
|
2370
2634
|
import WebSocket from "ws";
|
|
2635
|
+
function resolveCommand(command, platform = process.platform, env = process.env) {
|
|
2636
|
+
if (platform !== "win32") return { file: command, shell: false };
|
|
2637
|
+
if (/[\\/]/.test(command) || path3.extname(command)) return { file: command, shell: /\.(cmd|bat)$/i.test(command) };
|
|
2638
|
+
const file = findCodex(command, platform, env);
|
|
2639
|
+
return file ? { file, shell: file.endsWith(".cmd") } : { file: command, shell: false };
|
|
2640
|
+
}
|
|
2371
2641
|
function findOnPath(command, platform = process.platform, env = process.env) {
|
|
2372
2642
|
const dirs = (env.PATH ?? env.Path ?? "").split(path3.delimiter).filter(Boolean);
|
|
2373
2643
|
for (const ext of platform === "win32" ? [".exe", ".cmd"] : [""]) {
|
|
@@ -2378,12 +2648,772 @@ function findOnPath(command, platform = process.platform, env = process.env) {
|
|
|
2378
2648
|
}
|
|
2379
2649
|
return void 0;
|
|
2380
2650
|
}
|
|
2651
|
+
function findCodex(command, platform = process.platform, env = process.env) {
|
|
2652
|
+
const onPath = findOnPath(command, platform, env);
|
|
2653
|
+
if (onPath || platform !== "win32") return onPath;
|
|
2654
|
+
const candidates = [
|
|
2655
|
+
env.LOCALAPPDATA && path3.join(env.LOCALAPPDATA, "Programs", "OpenAI", "Codex", "bin", `${command}.exe`),
|
|
2656
|
+
env.APPDATA && path3.join(env.APPDATA, "npm", `${command}.cmd`)
|
|
2657
|
+
];
|
|
2658
|
+
return candidates.find((file) => !!file && fs2.existsSync(file));
|
|
2659
|
+
}
|
|
2660
|
+
var defaultSpawn = (command, args) => {
|
|
2661
|
+
const { file, shell } = resolveCommand(command);
|
|
2662
|
+
if (shell) return spawn(`"${file}" ${args.join(" ")}`, { stdio: "pipe", windowsHide: true, shell: true });
|
|
2663
|
+
return spawn(file, args, { stdio: "pipe", windowsHide: true });
|
|
2664
|
+
};
|
|
2665
|
+
var HANDSHAKE_TIMEOUT_MS = 1e4;
|
|
2666
|
+
function connectProxy(command, spawnFn = defaultSpawn, timeoutMs = HANDSHAKE_TIMEOUT_MS) {
|
|
2667
|
+
const child = spawnFn(command, ["app-server", "proxy"]);
|
|
2668
|
+
child.stdin?.on("error", () => {
|
|
2669
|
+
});
|
|
2670
|
+
child.stderr?.on("data", (d) => logger.debug(`codex proxy: ${String(d).trim()}`));
|
|
2671
|
+
const stream = new Duplex({
|
|
2672
|
+
read() {
|
|
2673
|
+
},
|
|
2674
|
+
write(chunk, _encoding, callback) {
|
|
2675
|
+
if (child.stdin?.writable) child.stdin.write(chunk, callback);
|
|
2676
|
+
else callback();
|
|
2677
|
+
},
|
|
2678
|
+
final(callback) {
|
|
2679
|
+
child.stdin?.end();
|
|
2680
|
+
callback();
|
|
2681
|
+
}
|
|
2682
|
+
});
|
|
2683
|
+
child.stdout?.on("data", (d) => stream.push(d));
|
|
2684
|
+
child.stdout?.on("end", () => stream.push(null));
|
|
2685
|
+
Object.assign(stream, { setNoDelay() {
|
|
2686
|
+
}, setTimeout() {
|
|
2687
|
+
}, setKeepAlive() {
|
|
2688
|
+
}, ref() {
|
|
2689
|
+
}, unref() {
|
|
2690
|
+
} });
|
|
2691
|
+
const ws = new WebSocket("ws://localhost/", {
|
|
2692
|
+
createConnection: (() => stream)
|
|
2693
|
+
});
|
|
2694
|
+
const close = () => {
|
|
2695
|
+
ws.terminate();
|
|
2696
|
+
child.kill();
|
|
2697
|
+
};
|
|
2698
|
+
return new Promise((resolve, reject) => {
|
|
2699
|
+
let settled = false;
|
|
2700
|
+
const timer = setTimeout(() => fail(new Error(`codex daemon did not answer within ${timeoutMs / 1e3}s`)), timeoutMs);
|
|
2701
|
+
const fail = (err) => {
|
|
2702
|
+
if (settled) return;
|
|
2703
|
+
settled = true;
|
|
2704
|
+
clearTimeout(timer);
|
|
2705
|
+
close();
|
|
2706
|
+
reject(err);
|
|
2707
|
+
};
|
|
2708
|
+
child.on("error", fail);
|
|
2709
|
+
child.once("exit", (code) => fail(new Error(`codex proxy exited (code ${code})`)));
|
|
2710
|
+
ws.on("error", fail);
|
|
2711
|
+
ws.once("open", () => {
|
|
2712
|
+
if (settled) return;
|
|
2713
|
+
settled = true;
|
|
2714
|
+
clearTimeout(timer);
|
|
2715
|
+
child.on("error", (err) => logger.warn(`codex proxy error: ${err.message}`));
|
|
2716
|
+
ws.on("error", (err) => logger.warn(`codex daemon socket error: ${err.message}`));
|
|
2717
|
+
resolve({ ws, close });
|
|
2718
|
+
});
|
|
2719
|
+
});
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
// src/codex/run.ts
|
|
2723
|
+
import path4 from "path";
|
|
2724
|
+
|
|
2725
|
+
// src/codex/rpc.ts
|
|
2726
|
+
init_logger();
|
|
2727
|
+
import { EventEmitter } from "events";
|
|
2728
|
+
var RpcError = class extends Error {
|
|
2729
|
+
constructor(code, message) {
|
|
2730
|
+
super(message);
|
|
2731
|
+
this.code = code;
|
|
2732
|
+
}
|
|
2733
|
+
};
|
|
2734
|
+
var RpcClient = class extends EventEmitter {
|
|
2735
|
+
constructor(ws, timeoutMs = 3e4) {
|
|
2736
|
+
super();
|
|
2737
|
+
this.ws = ws;
|
|
2738
|
+
this.timeoutMs = timeoutMs;
|
|
2739
|
+
ws.on("message", (data) => this.onMessage(String(data)));
|
|
2740
|
+
ws.on("close", () => {
|
|
2741
|
+
for (const p of this.pending.values()) {
|
|
2742
|
+
clearTimeout(p.timer);
|
|
2743
|
+
p.reject(new Error("connection closed"));
|
|
2744
|
+
}
|
|
2745
|
+
this.pending.clear();
|
|
2746
|
+
this.emit("close");
|
|
2747
|
+
});
|
|
2748
|
+
}
|
|
2749
|
+
nextId = 1;
|
|
2750
|
+
pending = /* @__PURE__ */ new Map();
|
|
2751
|
+
request(method, params, timeoutMs = this.timeoutMs) {
|
|
2752
|
+
if (this.ws.readyState !== this.ws.OPEN) return Promise.reject(new Error("connection closed"));
|
|
2753
|
+
const id = this.nextId++;
|
|
2754
|
+
return new Promise((resolve, reject) => {
|
|
2755
|
+
const timer = timeoutMs === null ? void 0 : setTimeout(() => {
|
|
2756
|
+
this.pending.delete(id);
|
|
2757
|
+
reject(new Error(`${method} timed out`));
|
|
2758
|
+
}, timeoutMs);
|
|
2759
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
2760
|
+
this.ws.send(JSON.stringify(params === void 0 ? { id, method } : { id, method, params }));
|
|
2761
|
+
});
|
|
2762
|
+
}
|
|
2763
|
+
notify(method, params) {
|
|
2764
|
+
if (this.ws.readyState !== this.ws.OPEN) return;
|
|
2765
|
+
this.ws.send(JSON.stringify(params === void 0 ? { method } : { method, params }));
|
|
2766
|
+
}
|
|
2767
|
+
respond(id, result) {
|
|
2768
|
+
if (this.ws.readyState !== this.ws.OPEN) return;
|
|
2769
|
+
this.ws.send(JSON.stringify({ id, result }));
|
|
2770
|
+
}
|
|
2771
|
+
// ws stops reading the socket for good if a 'message' listener throws.
|
|
2772
|
+
guarded(method, fn) {
|
|
2773
|
+
try {
|
|
2774
|
+
fn();
|
|
2775
|
+
} catch (err) {
|
|
2776
|
+
logger.warn(`Codex ${method} handling failed: ${err.message}`);
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
onMessage(text) {
|
|
2780
|
+
let msg;
|
|
2781
|
+
try {
|
|
2782
|
+
msg = JSON.parse(text);
|
|
2783
|
+
} catch {
|
|
2784
|
+
return;
|
|
2785
|
+
}
|
|
2786
|
+
if (!msg || typeof msg !== "object") return;
|
|
2787
|
+
if (msg.method === void 0 && msg.id !== void 0) {
|
|
2788
|
+
const p = this.pending.get(msg.id);
|
|
2789
|
+
if (!p) return;
|
|
2790
|
+
this.pending.delete(msg.id);
|
|
2791
|
+
clearTimeout(p.timer);
|
|
2792
|
+
if (msg.error) p.reject(new RpcError(msg.error.code, msg.error.message));
|
|
2793
|
+
else p.resolve(msg.result);
|
|
2794
|
+
} else if (msg.method !== void 0 && msg.id !== void 0) {
|
|
2795
|
+
this.guarded(msg.method, () => this.emit("request", msg.id, msg.method, msg.params));
|
|
2796
|
+
} else if (msg.method !== void 0) {
|
|
2797
|
+
this.guarded(msg.method, () => this.emit("notification", msg.method, msg.params));
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2800
|
+
};
|
|
2801
|
+
|
|
2802
|
+
// src/codex/mapping.ts
|
|
2803
|
+
var SOURCE_LABEL = { dashboard: "Dashboard", telegram: "Telegram", api: "API" };
|
|
2804
|
+
var SOURCE_PREFIX = new RegExp(`^\\[claude-alarm(?: \xB7 (?:${Object.values(SOURCE_LABEL).join("|")}))?\\] ?`);
|
|
2805
|
+
function finalAnswer(messages) {
|
|
2806
|
+
const finals = messages.filter((m) => m.phase === "final_answer").map((m) => m.text);
|
|
2807
|
+
if (finals.length) return finals.join("\n\n");
|
|
2808
|
+
return messages.at(-1)?.text ?? null;
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
// src/codex/run.ts
|
|
2812
|
+
init_constants();
|
|
2813
|
+
var MINUTE = 6e4;
|
|
2814
|
+
var SUMMARY_MAX = 200;
|
|
2815
|
+
var clip = (text) => text.length > SUMMARY_MAX ? `${text.slice(0, SUMMARY_MAX)}\u2026` : text;
|
|
2816
|
+
function relayed(method, params) {
|
|
2817
|
+
switch (method) {
|
|
2818
|
+
case "item/commandExecution/requestApproval": {
|
|
2819
|
+
const command = params.command ?? (params.commandActions ?? []).map((a) => a.command).join("\n");
|
|
2820
|
+
return { kind: "command", summary: clip(String(command || params.reason || "")), decline: { decision: "decline" } };
|
|
2821
|
+
}
|
|
2822
|
+
case "item/fileChange/requestApproval":
|
|
2823
|
+
return { kind: "file change", summary: clip(String(params.reason ?? "")), decline: { decision: "decline" } };
|
|
2824
|
+
case "mcpServer/elicitation/request":
|
|
2825
|
+
if (params._meta?.codex_approval_kind !== "mcp_tool_call") return null;
|
|
2826
|
+
return { kind: "MCP tool", summary: clip(`${params.serverName ?? ""}: ${params.message ?? ""}`), decline: { action: "decline" } };
|
|
2827
|
+
default:
|
|
2828
|
+
return null;
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
function unrelayed(method, params) {
|
|
2832
|
+
switch (method) {
|
|
2833
|
+
case "item/permissions/requestApproval":
|
|
2834
|
+
return { kind: "permissions", summary: clip(String(params.reason ?? "")), answer: { permissions: {} } };
|
|
2835
|
+
case "item/tool/requestUserInput":
|
|
2836
|
+
return {
|
|
2837
|
+
kind: "question",
|
|
2838
|
+
summary: clip((params.questions ?? []).map((q) => q.question ?? "").join(" / ")),
|
|
2839
|
+
answer: { answers: {} }
|
|
2840
|
+
};
|
|
2841
|
+
case "mcpServer/elicitation/request":
|
|
2842
|
+
return { kind: "MCP form", summary: clip(String(params.message ?? "")), answer: { action: "decline" } };
|
|
2843
|
+
default:
|
|
2844
|
+
return null;
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2847
|
+
async function runCodexTask(opts) {
|
|
2848
|
+
const progress = opts.progress ?? (() => {
|
|
2849
|
+
});
|
|
2850
|
+
const approvalTimeoutMs = opts.approvalTimeoutMs ?? 10 * MINUTE;
|
|
2851
|
+
const started = Date.now();
|
|
2852
|
+
const conn = await connectProxy(opts.command ?? "codex", opts.spawnFn ?? defaultSpawn);
|
|
2853
|
+
const rpc = new RpcClient(conn.ws);
|
|
2854
|
+
const timers = /* @__PURE__ */ new Set();
|
|
2855
|
+
try {
|
|
2856
|
+
const init2 = await rpc.request("initialize", {
|
|
2857
|
+
clientInfo: { name: "claude-alarm-run", version: CHANNEL_SERVER_VERSION }
|
|
2858
|
+
});
|
|
2859
|
+
rpc.notify("initialized");
|
|
2860
|
+
const yolo = { sandbox: "danger-full-access", approvalPolicy: "never" };
|
|
2861
|
+
let threadId;
|
|
2862
|
+
if (opts.threadId) {
|
|
2863
|
+
await rpc.request("thread/resume", opts.yolo ? { threadId: opts.threadId, ...yolo } : { threadId: opts.threadId });
|
|
2864
|
+
threadId = opts.threadId;
|
|
2865
|
+
} else {
|
|
2866
|
+
const res = await rpc.request("thread/start", {
|
|
2867
|
+
cwd: opts.cwd,
|
|
2868
|
+
...opts.yolo ? yolo : { sandbox: "workspace-write", approvalPolicy: "on-request" }
|
|
2869
|
+
});
|
|
2870
|
+
threadId = res.thread.id;
|
|
2871
|
+
if (opts.name) {
|
|
2872
|
+
await rpc.request("thread/name/set", { threadId, name: opts.name }).catch(() => {
|
|
2873
|
+
});
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
progress(`thread ${threadId}`);
|
|
2877
|
+
const messages = /* @__PURE__ */ new Map();
|
|
2878
|
+
const approvals = [];
|
|
2879
|
+
const waiting = /* @__PURE__ */ new Map();
|
|
2880
|
+
let finish;
|
|
2881
|
+
let lose;
|
|
2882
|
+
const completed = new Promise((resolve, reject) => {
|
|
2883
|
+
finish = resolve;
|
|
2884
|
+
lose = reject;
|
|
2885
|
+
});
|
|
2886
|
+
completed.catch(() => {
|
|
2887
|
+
});
|
|
2888
|
+
rpc.on("close", () => lose(new Error("connection to the Codex daemon closed")));
|
|
2889
|
+
rpc.on("notification", (method, params) => {
|
|
2890
|
+
if (params?.threadId !== threadId) return;
|
|
2891
|
+
if (method === "item/completed") {
|
|
2892
|
+
const item = params.item ?? {};
|
|
2893
|
+
if (item.type === "agentMessage") {
|
|
2894
|
+
const list = messages.get(params.turnId) ?? [];
|
|
2895
|
+
list.push({ text: item.text, phase: item.phase });
|
|
2896
|
+
messages.set(params.turnId, list);
|
|
2897
|
+
} else if (item.type === "commandExecution") {
|
|
2898
|
+
progress(`ran ${clip(String(item.command ?? ""))} (exit ${item.exitCode ?? "?"})`);
|
|
2899
|
+
}
|
|
2900
|
+
} else if (method === "serverRequest/resolved") {
|
|
2901
|
+
const record = waiting.get(params.requestId);
|
|
2902
|
+
if (record) {
|
|
2903
|
+
waiting.delete(params.requestId);
|
|
2904
|
+
record.outcome = "answered-elsewhere";
|
|
2905
|
+
progress(`approval answered elsewhere: ${record.kind}`);
|
|
2906
|
+
}
|
|
2907
|
+
} else if (method === "turn/completed") {
|
|
2908
|
+
finish(params.turn);
|
|
2909
|
+
}
|
|
2910
|
+
});
|
|
2911
|
+
rpc.on("request", (id, method, params) => {
|
|
2912
|
+
if (params?.threadId !== threadId) return;
|
|
2913
|
+
const wait = relayed(method, params);
|
|
2914
|
+
if (wait) {
|
|
2915
|
+
const record = { kind: wait.kind, summary: wait.summary, outcome: "unanswered" };
|
|
2916
|
+
approvals.push(record);
|
|
2917
|
+
waiting.set(id, record);
|
|
2918
|
+
progress(`approval pending (${Math.round(approvalTimeoutMs / 1e3)}s): ${wait.kind} ${wait.summary}`);
|
|
2919
|
+
const timer = setTimeout(() => {
|
|
2920
|
+
timers.delete(timer);
|
|
2921
|
+
if (!waiting.delete(id)) return;
|
|
2922
|
+
record.outcome = "timeout-declined";
|
|
2923
|
+
rpc.respond(id, wait.decline);
|
|
2924
|
+
progress(`approval declined after timeout: ${wait.kind}`);
|
|
2925
|
+
}, approvalTimeoutMs);
|
|
2926
|
+
timers.add(timer);
|
|
2927
|
+
return;
|
|
2928
|
+
}
|
|
2929
|
+
const answer = unrelayed(method, params);
|
|
2930
|
+
if (answer) {
|
|
2931
|
+
approvals.push({ kind: answer.kind, summary: answer.summary, outcome: "auto-declined" });
|
|
2932
|
+
rpc.respond(id, answer.answer);
|
|
2933
|
+
progress(`declined at once: ${answer.kind}`);
|
|
2934
|
+
}
|
|
2935
|
+
});
|
|
2936
|
+
const turnStart = { threadId, input: [{ type: "text", text: opts.brief }] };
|
|
2937
|
+
if (opts.outputSchema !== void 0) turnStart.outputSchema = opts.outputSchema;
|
|
2938
|
+
const { turn } = await rpc.request("turn/start", turnStart);
|
|
2939
|
+
const turnId = turn.id;
|
|
2940
|
+
progress(`turn ${turnId} started`);
|
|
2941
|
+
let stopReason = null;
|
|
2942
|
+
let interruptRequested;
|
|
2943
|
+
const stopped = new Promise((resolve) => interruptRequested = resolve);
|
|
2944
|
+
const stop = (reason) => {
|
|
2945
|
+
if (stopReason) return;
|
|
2946
|
+
stopReason = reason;
|
|
2947
|
+
progress(reason === "timedOut" ? "timeout reached, interrupting the turn" : "interrupting the turn");
|
|
2948
|
+
rpc.request("turn/interrupt", { threadId, turnId }).catch(() => {
|
|
2949
|
+
});
|
|
2950
|
+
interruptRequested();
|
|
2951
|
+
};
|
|
2952
|
+
const deadline = setTimeout(() => stop("timedOut"), opts.timeoutMs ?? 60 * MINUTE);
|
|
2953
|
+
timers.add(deadline);
|
|
2954
|
+
const onAbort = () => stop("interrupted");
|
|
2955
|
+
if (opts.signal?.aborted) onAbort();
|
|
2956
|
+
opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
2957
|
+
const grace = stopped.then(
|
|
2958
|
+
() => new Promise((resolve) => {
|
|
2959
|
+
const t = setTimeout(() => resolve(null), opts.interruptGraceMs ?? 15e3);
|
|
2960
|
+
timers.add(t);
|
|
2961
|
+
})
|
|
2962
|
+
);
|
|
2963
|
+
let ended;
|
|
2964
|
+
let lost = null;
|
|
2965
|
+
try {
|
|
2966
|
+
ended = await Promise.race([completed, grace]);
|
|
2967
|
+
} catch (err) {
|
|
2968
|
+
ended = null;
|
|
2969
|
+
lost = err.message;
|
|
2970
|
+
} finally {
|
|
2971
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
2972
|
+
}
|
|
2973
|
+
let status;
|
|
2974
|
+
let error = null;
|
|
2975
|
+
if (stopReason) status = stopReason;
|
|
2976
|
+
else if (lost) {
|
|
2977
|
+
status = "failed";
|
|
2978
|
+
error = lost;
|
|
2979
|
+
} else if (ended?.status === "failed" || ended?.error) {
|
|
2980
|
+
status = "failed";
|
|
2981
|
+
error = ended?.error?.message ?? "the turn failed";
|
|
2982
|
+
} else if (ended?.status === "interrupted") status = "interrupted";
|
|
2983
|
+
else status = "completed";
|
|
2984
|
+
const collected = messages.get(turnId) ?? [];
|
|
2985
|
+
const fromTurn = (ended?.items ?? []).filter((i) => i.type === "agentMessage");
|
|
2986
|
+
const finalText = finalAnswer(collected.length ? collected : fromTurn);
|
|
2987
|
+
let result = null;
|
|
2988
|
+
if (opts.outputSchema !== void 0 && finalText) {
|
|
2989
|
+
try {
|
|
2990
|
+
result = JSON.parse(finalText);
|
|
2991
|
+
} catch {
|
|
2992
|
+
result = null;
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2995
|
+
return {
|
|
2996
|
+
threadId,
|
|
2997
|
+
turnId,
|
|
2998
|
+
status,
|
|
2999
|
+
error,
|
|
3000
|
+
result,
|
|
3001
|
+
finalText,
|
|
3002
|
+
approvals,
|
|
3003
|
+
durationMs: Date.now() - started,
|
|
3004
|
+
codex: { userAgent: init2.userAgent ?? null }
|
|
3005
|
+
};
|
|
3006
|
+
} finally {
|
|
3007
|
+
for (const t of timers) clearTimeout(t);
|
|
3008
|
+
conn.close();
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
var VALUE_FLAGS = {
|
|
3012
|
+
"--brief": "brief",
|
|
3013
|
+
"--cwd": "cwd",
|
|
3014
|
+
"--thread": "threadId",
|
|
3015
|
+
"--name": "name",
|
|
3016
|
+
"--output-schema": "outputSchema",
|
|
3017
|
+
"--approval-timeout": "approvalTimeoutMs",
|
|
3018
|
+
"--timeout": "timeoutMs"
|
|
3019
|
+
};
|
|
3020
|
+
function parseRunArgs(argv) {
|
|
3021
|
+
const out = {};
|
|
3022
|
+
for (let i = 0; i < argv.length; i++) {
|
|
3023
|
+
const flag = argv[i];
|
|
3024
|
+
if (flag === "--yolo") {
|
|
3025
|
+
out.yolo = true;
|
|
3026
|
+
continue;
|
|
3027
|
+
}
|
|
3028
|
+
const key = VALUE_FLAGS[flag];
|
|
3029
|
+
if (!key) throw new Error(`Unknown option: ${flag}`);
|
|
3030
|
+
const value = argv[++i];
|
|
3031
|
+
if (value === void 0) throw new Error(`${flag} needs a value`);
|
|
3032
|
+
if (key === "approvalTimeoutMs" || key === "timeoutMs") {
|
|
3033
|
+
const minutes = Number(value);
|
|
3034
|
+
if (!Number.isFinite(minutes) || minutes <= 0) throw new Error(`${flag} must be a number of minutes`);
|
|
3035
|
+
out[key] = Math.round(minutes * MINUTE);
|
|
3036
|
+
} else {
|
|
3037
|
+
out[key] = value;
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
if (out.brief === void 0) throw new Error("--brief <file|-> is required");
|
|
3041
|
+
return out;
|
|
3042
|
+
}
|
|
3043
|
+
async function runFromCli(argv, io) {
|
|
3044
|
+
let task;
|
|
3045
|
+
try {
|
|
3046
|
+
const args = parseRunArgs(argv);
|
|
3047
|
+
const brief = args.brief === "-" ? await io.readStdin() : await io.readFile(args.brief);
|
|
3048
|
+
if (!brief.trim()) throw new Error("the brief is empty");
|
|
3049
|
+
task = {
|
|
3050
|
+
brief,
|
|
3051
|
+
cwd: path4.resolve(io.cwd, args.cwd ?? "."),
|
|
3052
|
+
threadId: args.threadId,
|
|
3053
|
+
name: args.name,
|
|
3054
|
+
yolo: args.yolo,
|
|
3055
|
+
outputSchema: args.outputSchema === void 0 ? void 0 : JSON.parse(await io.readFile(args.outputSchema)),
|
|
3056
|
+
approvalTimeoutMs: args.approvalTimeoutMs,
|
|
3057
|
+
timeoutMs: args.timeoutMs,
|
|
3058
|
+
signal: io.signal,
|
|
3059
|
+
spawnFn: io.spawnFn,
|
|
3060
|
+
progress: (line) => io.err(`[codex run] ${line}
|
|
3061
|
+
`)
|
|
3062
|
+
};
|
|
3063
|
+
} catch (err) {
|
|
3064
|
+
io.err(`codex run: ${err.message}
|
|
3065
|
+
`);
|
|
3066
|
+
return 2;
|
|
3067
|
+
}
|
|
3068
|
+
try {
|
|
3069
|
+
const result = await runCodexTask(task);
|
|
3070
|
+
io.out(`${JSON.stringify(result, null, 2)}
|
|
3071
|
+
`);
|
|
3072
|
+
return result.status === "completed" ? 0 : 1;
|
|
3073
|
+
} catch (err) {
|
|
3074
|
+
io.err(`codex run: could not start the task: ${err.message}
|
|
3075
|
+
`);
|
|
3076
|
+
return 2;
|
|
3077
|
+
}
|
|
3078
|
+
}
|
|
2381
3079
|
|
|
2382
3080
|
// src/cli.ts
|
|
2383
3081
|
init_constants();
|
|
2384
3082
|
init_logger();
|
|
2385
3083
|
init_crash_guard();
|
|
2386
|
-
|
|
3084
|
+
|
|
3085
|
+
// src/hub/readiness.ts
|
|
3086
|
+
var HUB_START_TIMEOUT_MS = 5e3;
|
|
3087
|
+
async function waitForHub(opts) {
|
|
3088
|
+
const timeoutMs = opts.timeoutMs ?? HUB_START_TIMEOUT_MS;
|
|
3089
|
+
const intervalMs = opts.intervalMs ?? 200;
|
|
3090
|
+
const deadline = Date.now() + timeoutMs;
|
|
3091
|
+
const headers = opts.token ? { Authorization: `Bearer ${opts.token}` } : {};
|
|
3092
|
+
for (; ; ) {
|
|
3093
|
+
if (!opts.isAlive()) return "exited";
|
|
3094
|
+
const remaining = deadline - Date.now();
|
|
3095
|
+
if (remaining <= 0) return "timeout";
|
|
3096
|
+
try {
|
|
3097
|
+
const res = await fetch(opts.url, { headers, signal: AbortSignal.timeout(Math.min(remaining, 1e3)) });
|
|
3098
|
+
if (res.ok) {
|
|
3099
|
+
const body = await res.json();
|
|
3100
|
+
if (body.pid === opts.pid) return "ready";
|
|
3101
|
+
}
|
|
3102
|
+
} catch {
|
|
3103
|
+
}
|
|
3104
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(intervalMs, Math.max(0, deadline - Date.now()))));
|
|
3105
|
+
}
|
|
3106
|
+
}
|
|
3107
|
+
|
|
3108
|
+
// src/cli.ts
|
|
3109
|
+
init_hub_url();
|
|
3110
|
+
|
|
3111
|
+
// src/codex/hub-target.ts
|
|
3112
|
+
init_hub_url();
|
|
3113
|
+
function resolveAdapterHub(config2, env = process.env) {
|
|
3114
|
+
const envHost = env.CLAUDE_ALARM_HUB_HOST;
|
|
3115
|
+
const envPort = env.CLAUDE_ALARM_HUB_PORT;
|
|
3116
|
+
const envToken = env.CLAUDE_ALARM_HUB_TOKEN;
|
|
3117
|
+
return {
|
|
3118
|
+
host: hubUrlHost(envHost ?? config2.hub.host),
|
|
3119
|
+
port: envPort ? parseInt(envPort, 10) : config2.hub.port,
|
|
3120
|
+
token: envToken ?? config2.hub.token,
|
|
3121
|
+
fromEnv: { host: envHost !== void 0, port: !!envPort, token: envToken !== void 0 }
|
|
3122
|
+
};
|
|
3123
|
+
}
|
|
3124
|
+
|
|
3125
|
+
// src/codex/start-check.ts
|
|
3126
|
+
var ADAPTER_REPORT_TIMEOUT_MS = 1e4;
|
|
3127
|
+
var HUB_CHECK_TIMEOUT_MS = 3e3;
|
|
3128
|
+
var RESTART_HINT = "After fixing this, restart the adapter: claude-alarm codex stop, then claude-alarm codex start (if the hub started the adapter, restart the hub instead)";
|
|
3129
|
+
var CONFIG_PATH = "~/.claude-alarm/config.json";
|
|
3130
|
+
var NOT_CHECKED = { text: "Codex daemon: not checked (the adapter was already running)", warning: false };
|
|
3131
|
+
function toOutcome(msg) {
|
|
3132
|
+
if (msg.connected === true) return typeof msg.userAgent === "string" ? { connected: true, userAgent: msg.userAgent } : { connected: true };
|
|
3133
|
+
return { connected: false, error: String(msg.error ?? ""), notFound: msg.notFound === true };
|
|
3134
|
+
}
|
|
3135
|
+
function waitForAdapterReport(child, timeoutMs = ADAPTER_REPORT_TIMEOUT_MS) {
|
|
3136
|
+
return new Promise((resolve) => {
|
|
3137
|
+
const finish = (result) => {
|
|
3138
|
+
clearTimeout(timer);
|
|
3139
|
+
child.off("message", onMessage);
|
|
3140
|
+
child.off("exit", onExit);
|
|
3141
|
+
child.off("error", onError);
|
|
3142
|
+
resolve(result);
|
|
3143
|
+
};
|
|
3144
|
+
const onMessage = (msg) => {
|
|
3145
|
+
if (!msg || typeof msg !== "object") return;
|
|
3146
|
+
const m = msg;
|
|
3147
|
+
if (m.type === "codex-first-connect") finish({ kind: "report", outcome: toOutcome(m) });
|
|
3148
|
+
else if (m.type === "codex-already-running" && Number.isInteger(m.pid)) finish({ kind: "already", pid: m.pid });
|
|
3149
|
+
else if (m.type === "codex-lock-failed") finish({ kind: "lockFailed", error: String(m.error ?? "") });
|
|
3150
|
+
};
|
|
3151
|
+
const onExit = (code, signal) => finish({ kind: "exited", code, signal });
|
|
3152
|
+
const onError = (error) => finish({ kind: "exited", code: null, signal: null, error });
|
|
3153
|
+
const timer = setTimeout(() => finish({ kind: "timeout" }), timeoutMs);
|
|
3154
|
+
child.on("message", onMessage);
|
|
3155
|
+
child.on("exit", onExit);
|
|
3156
|
+
child.on("error", onError);
|
|
3157
|
+
});
|
|
3158
|
+
}
|
|
3159
|
+
function hubUrl(hub) {
|
|
3160
|
+
return `http://${hub.host}:${hub.port}`;
|
|
3161
|
+
}
|
|
3162
|
+
async function checkHub(hub, timeoutMs = HUB_CHECK_TIMEOUT_MS) {
|
|
3163
|
+
let res;
|
|
3164
|
+
try {
|
|
3165
|
+
res = await fetch(`${hubUrl(hub)}/api/status`, {
|
|
3166
|
+
headers: hub.token ? { Authorization: `Bearer ${hub.token}` } : {},
|
|
3167
|
+
redirect: "manual",
|
|
3168
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
3169
|
+
});
|
|
3170
|
+
} catch (err) {
|
|
3171
|
+
const e = err;
|
|
3172
|
+
return { kind: "unreachable", reason: typeof e.cause?.code === "string" ? e.cause.code : e.name };
|
|
3173
|
+
}
|
|
3174
|
+
if (res.status === 200) {
|
|
3175
|
+
try {
|
|
3176
|
+
const body = await res.json();
|
|
3177
|
+
if (body?.running === true) return { kind: "ok" };
|
|
3178
|
+
} catch {
|
|
3179
|
+
}
|
|
3180
|
+
return { kind: "not-hub", status: 200 };
|
|
3181
|
+
}
|
|
3182
|
+
await res.body?.cancel().catch(() => {
|
|
3183
|
+
});
|
|
3184
|
+
return res.status === 401 ? { kind: "unauthorized" } : { kind: "not-hub", status: res.status };
|
|
3185
|
+
}
|
|
3186
|
+
function daemonLine(report, command, logFile) {
|
|
3187
|
+
if (report.kind === "timeout") {
|
|
3188
|
+
return { text: `Codex daemon: no answer within ${ADAPTER_REPORT_TIMEOUT_MS / 1e3}s. The adapter keeps trying. See ${logFile}`, warning: true };
|
|
3189
|
+
}
|
|
3190
|
+
const o = report.outcome;
|
|
3191
|
+
if (o.connected) return { text: `Codex daemon: connected (${o.userAgent ?? "unknown version"})`, warning: false };
|
|
3192
|
+
if (o.notFound) {
|
|
3193
|
+
return { text: `Codex daemon: "${command}" not found. Install Codex or set "codex.command" in ${CONFIG_PATH}, and run the restart from a new terminal.`, warning: true };
|
|
3194
|
+
}
|
|
3195
|
+
return { text: `Codex daemon: not connected (${o.error}). Is Codex running? The adapter keeps retrying.`, warning: true };
|
|
3196
|
+
}
|
|
3197
|
+
function addressSource(hub) {
|
|
3198
|
+
const host = hub.fromEnv.host ? "CLAUDE_ALARM_HUB_HOST" : "hub.host";
|
|
3199
|
+
const port = hub.fromEnv.port ? "CLAUDE_ALARM_HUB_PORT" : "hub.port";
|
|
3200
|
+
const fromConfig = !hub.fromEnv.host || !hub.fromEnv.port;
|
|
3201
|
+
return `${host} and ${port}${fromConfig ? ` in ${CONFIG_PATH}` : ""}`;
|
|
3202
|
+
}
|
|
3203
|
+
function tokenSource(hub) {
|
|
3204
|
+
return hub.fromEnv.token ? "CLAUDE_ALARM_HUB_TOKEN" : `hub.token in ${CONFIG_PATH}`;
|
|
3205
|
+
}
|
|
3206
|
+
function hubLine(check, hub) {
|
|
3207
|
+
const url = hubUrl(hub);
|
|
3208
|
+
switch (check.kind) {
|
|
3209
|
+
case "ok":
|
|
3210
|
+
return { text: `Hub: reachable at ${url}`, warning: false };
|
|
3211
|
+
case "unauthorized":
|
|
3212
|
+
return { text: `Hub: ${url} rejected the token (401). Check ${tokenSource(hub)}.`, warning: true };
|
|
3213
|
+
case "not-hub":
|
|
3214
|
+
return { text: `Hub: ${url} answered ${check.status} but is not a claude-alarm hub. Check ${addressSource(hub)}.`, warning: true };
|
|
3215
|
+
case "unreachable":
|
|
3216
|
+
return { text: `Hub: not reachable at ${url} (${check.reason}). Check ${addressSource(hub)} and that the hub is running.`, warning: true };
|
|
3217
|
+
}
|
|
3218
|
+
}
|
|
3219
|
+
function printChecks(d, daemon, hub) {
|
|
3220
|
+
d.out(` ${daemon.text}`);
|
|
3221
|
+
d.out(` ${hub.text}`);
|
|
3222
|
+
if (daemon.warning || hub.warning) d.out(` ${RESTART_HINT}`);
|
|
3223
|
+
}
|
|
3224
|
+
function printAlreadyRunning(d, pid, hub) {
|
|
3225
|
+
d.out(`Codex adapter is already running (PID: ${pid})`);
|
|
3226
|
+
printChecks(d, NOT_CHECKED, hubLine(hub, d.hub));
|
|
3227
|
+
}
|
|
3228
|
+
async function startAdapter(d) {
|
|
3229
|
+
const existing = await d.queryOwner();
|
|
3230
|
+
if (existing.state === "running") {
|
|
3231
|
+
printAlreadyRunning(d, existing.pid, await checkHub(d.hub, d.hubTimeoutMs));
|
|
3232
|
+
return 0;
|
|
3233
|
+
}
|
|
3234
|
+
const child = d.spawnAdapter();
|
|
3235
|
+
const seen = {};
|
|
3236
|
+
child.on("exit", (code, signal) => {
|
|
3237
|
+
seen.exit ??= { code, signal };
|
|
3238
|
+
});
|
|
3239
|
+
child.on("error", (error) => {
|
|
3240
|
+
seen.exit ??= { code: null, signal: null, error };
|
|
3241
|
+
});
|
|
3242
|
+
const [report, hub] = await Promise.all([
|
|
3243
|
+
waitForAdapterReport(child, d.reportTimeoutMs),
|
|
3244
|
+
checkHub(d.hub, d.hubTimeoutMs)
|
|
3245
|
+
]);
|
|
3246
|
+
if (child.connected) child.disconnect();
|
|
3247
|
+
child.unref();
|
|
3248
|
+
if (report.kind === "already") {
|
|
3249
|
+
printAlreadyRunning(d, report.pid, hub);
|
|
3250
|
+
return 0;
|
|
3251
|
+
}
|
|
3252
|
+
if (report.kind === "lockFailed") {
|
|
3253
|
+
d.err(`Codex adapter cannot start: ${report.error}`);
|
|
3254
|
+
return 1;
|
|
3255
|
+
}
|
|
3256
|
+
if (report.kind !== "exited" && !seen.exit && child.pid !== void 0) {
|
|
3257
|
+
d.out(`Codex adapter started (PID: ${child.pid}). Logs: ${d.logFile}`);
|
|
3258
|
+
printChecks(d, daemonLine(report, d.command, d.logFile), hubLine(hub, d.hub));
|
|
3259
|
+
return 0;
|
|
3260
|
+
}
|
|
3261
|
+
const exit = seen.exit ?? (report.kind === "exited" ? report : void 0);
|
|
3262
|
+
if (!exit || exit.error) {
|
|
3263
|
+
d.err(`Codex adapter failed to start: ${exit?.error?.message ?? "no process id"}`);
|
|
3264
|
+
return 1;
|
|
3265
|
+
}
|
|
3266
|
+
const owner = await d.queryOwner();
|
|
3267
|
+
if (owner.state === "running" && owner.pid !== child.pid) {
|
|
3268
|
+
printAlreadyRunning(d, owner.pid, hub);
|
|
3269
|
+
return 0;
|
|
3270
|
+
}
|
|
3271
|
+
const how = exit.code !== null ? `code ${exit.code}` : `signal ${exit.signal}`;
|
|
3272
|
+
d.err(`Codex adapter exited during startup (${how}). See ${d.logFile}`);
|
|
3273
|
+
return 1;
|
|
3274
|
+
}
|
|
3275
|
+
|
|
3276
|
+
// src/codex/instance-lock.ts
|
|
3277
|
+
import crypto from "crypto";
|
|
3278
|
+
import fs4 from "fs";
|
|
3279
|
+
import net2 from "net";
|
|
3280
|
+
import path5 from "path";
|
|
3281
|
+
var CONTROL_PROTOCOL = 1;
|
|
3282
|
+
var STOP_PROOF_LABEL = "claude-alarm codex stop";
|
|
3283
|
+
var MAX_LINE_BYTES = 4096;
|
|
3284
|
+
function stopProof(token) {
|
|
3285
|
+
return crypto.createHmac("sha256", token).update(STOP_PROOF_LABEL).digest("hex");
|
|
3286
|
+
}
|
|
3287
|
+
function controlEndpoint(configDir, platform = process.platform) {
|
|
3288
|
+
if (platform !== "win32") return path5.posix.join(configDir, "codex.sock");
|
|
3289
|
+
let dir;
|
|
3290
|
+
try {
|
|
3291
|
+
dir = fs4.realpathSync.native(configDir);
|
|
3292
|
+
} catch {
|
|
3293
|
+
dir = path5.win32.resolve(configDir);
|
|
3294
|
+
}
|
|
3295
|
+
const hash = crypto.createHash("sha256").update(dir.toLowerCase()).digest("hex").slice(0, 16);
|
|
3296
|
+
return `\\\\.\\pipe\\claude-alarm-codex-${hash}`;
|
|
3297
|
+
}
|
|
3298
|
+
function ask(endpoint, request, timeoutMs) {
|
|
3299
|
+
return new Promise((resolve) => {
|
|
3300
|
+
let buffer = "";
|
|
3301
|
+
let settled = false;
|
|
3302
|
+
const socket = net2.connect(endpoint);
|
|
3303
|
+
const finish = (answer) => {
|
|
3304
|
+
if (settled) return;
|
|
3305
|
+
settled = true;
|
|
3306
|
+
clearTimeout(timer);
|
|
3307
|
+
socket.destroy();
|
|
3308
|
+
resolve(answer);
|
|
3309
|
+
};
|
|
3310
|
+
const timer = setTimeout(() => finish({ state: "unknown", error: `no answer within ${timeoutMs}ms` }), timeoutMs);
|
|
3311
|
+
socket.setEncoding("utf8");
|
|
3312
|
+
socket.on("connect", () => socket.write(`${JSON.stringify(request)}
|
|
3313
|
+
`));
|
|
3314
|
+
socket.on("data", (chunk) => {
|
|
3315
|
+
buffer += chunk;
|
|
3316
|
+
const end = buffer.indexOf("\n");
|
|
3317
|
+
if (end < 0) {
|
|
3318
|
+
if (Buffer.byteLength(buffer) > MAX_LINE_BYTES) finish({ state: "unknown", error: "reply too long" });
|
|
3319
|
+
return;
|
|
3320
|
+
}
|
|
3321
|
+
try {
|
|
3322
|
+
finish({ reply: JSON.parse(buffer.slice(0, end)) });
|
|
3323
|
+
} catch {
|
|
3324
|
+
finish({ state: "unknown", error: "unreadable reply" });
|
|
3325
|
+
}
|
|
3326
|
+
});
|
|
3327
|
+
socket.on("end", () => finish({ state: "unknown", error: "closed without a reply" }));
|
|
3328
|
+
socket.on("error", (err) => {
|
|
3329
|
+
if (err.code === "ENOENT" || err.code === "ECONNREFUSED") finish({ state: "absent" });
|
|
3330
|
+
else finish({ state: "unknown", error: err.code ?? err.message });
|
|
3331
|
+
});
|
|
3332
|
+
});
|
|
3333
|
+
}
|
|
3334
|
+
async function queryOwner(endpoint, timeoutMs = 1e3) {
|
|
3335
|
+
const answer = await ask(endpoint, { type: "status" }, timeoutMs);
|
|
3336
|
+
if (!("reply" in answer)) return answer;
|
|
3337
|
+
const reply = answer.reply;
|
|
3338
|
+
if (reply?.type === "status" && reply.protocol === CONTROL_PROTOCOL && Number.isInteger(reply.pid)) {
|
|
3339
|
+
return { state: "running", pid: reply.pid };
|
|
3340
|
+
}
|
|
3341
|
+
return { state: "unknown", error: "unexpected reply" };
|
|
3342
|
+
}
|
|
3343
|
+
async function requestStop(endpoint, token, timeoutMs = 1e3) {
|
|
3344
|
+
const answer = await ask(endpoint, { type: "stop", proof: stopProof(token) }, timeoutMs);
|
|
3345
|
+
if (!("reply" in answer)) return answer;
|
|
3346
|
+
const reply = answer.reply;
|
|
3347
|
+
if (reply?.type === "stopping" && Number.isInteger(reply.pid)) return { state: "stopping", pid: reply.pid };
|
|
3348
|
+
if (reply?.type === "error" && reply.error === "unauthorized") return { state: "unauthorized" };
|
|
3349
|
+
return { state: "unknown", error: "unexpected reply" };
|
|
3350
|
+
}
|
|
3351
|
+
|
|
3352
|
+
// src/codex/control-cli.ts
|
|
3353
|
+
var STOP_WAIT_MS = 5e3;
|
|
3354
|
+
var STOP_POLL_MS = 100;
|
|
3355
|
+
function legacyNote(d, ownerPid) {
|
|
3356
|
+
const pid = d.legacyPid();
|
|
3357
|
+
if (pid === void 0 || pid === ownerPid) return;
|
|
3358
|
+
if (!d.isRunning(pid)) {
|
|
3359
|
+
d.removeLegacyPidFile();
|
|
3360
|
+
return;
|
|
3361
|
+
}
|
|
3362
|
+
d.out(`Note: ${d.legacyPidFile} names a running process (PID: ${pid}). claude-alarm 1.2.0 and earlier wrote this file; if that process is an old Codex adapter, restart the hub or end it yourself.`);
|
|
3363
|
+
}
|
|
3364
|
+
function unavailable(d, error) {
|
|
3365
|
+
d.err(`Codex adapter control endpoint is unavailable: ${error}`);
|
|
3366
|
+
return 1;
|
|
3367
|
+
}
|
|
3368
|
+
async function stopAdapter(d, startWithHub) {
|
|
3369
|
+
const owner = await d.queryOwner();
|
|
3370
|
+
if (owner.state === "unknown") return unavailable(d, owner.error);
|
|
3371
|
+
if (owner.state === "absent") {
|
|
3372
|
+
d.out("Codex adapter is not running");
|
|
3373
|
+
legacyNote(d);
|
|
3374
|
+
return 0;
|
|
3375
|
+
}
|
|
3376
|
+
const reply = await d.requestStop();
|
|
3377
|
+
if (reply.state === "unknown") return unavailable(d, reply.error);
|
|
3378
|
+
if (reply.state === "unauthorized") {
|
|
3379
|
+
d.err(`Codex adapter refused to stop: the token in ${d.configFile} does not match the one it started with`);
|
|
3380
|
+
return 1;
|
|
3381
|
+
}
|
|
3382
|
+
if (reply.state === "absent") {
|
|
3383
|
+
d.out("Codex adapter is not running");
|
|
3384
|
+
return 0;
|
|
3385
|
+
}
|
|
3386
|
+
const sleep = d.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
3387
|
+
for (let waited = 0; waited < (d.stopWaitMs ?? STOP_WAIT_MS); waited += STOP_POLL_MS) {
|
|
3388
|
+
await sleep(STOP_POLL_MS);
|
|
3389
|
+
const now = await d.queryOwner();
|
|
3390
|
+
if (now.state === "absent" || now.state === "running" && now.pid !== reply.pid) {
|
|
3391
|
+
d.out(`Codex adapter stopped (PID: ${reply.pid})`);
|
|
3392
|
+
if (now.state === "running") d.out(`Another Codex adapter took over (PID: ${now.pid})`);
|
|
3393
|
+
else if (startWithHub) d.out("If the hub started it, the hub will not start it again until you restart the hub.");
|
|
3394
|
+
return 0;
|
|
3395
|
+
}
|
|
3396
|
+
}
|
|
3397
|
+
d.err(`Stop requested, but the Codex adapter (PID: ${reply.pid}) is still running. Check with: claude-alarm codex status`);
|
|
3398
|
+
return 1;
|
|
3399
|
+
}
|
|
3400
|
+
async function adapterStatus(d, startWithHub) {
|
|
3401
|
+
const owner = await d.queryOwner();
|
|
3402
|
+
if (owner.state === "running") {
|
|
3403
|
+
d.out(`Codex adapter: running (PID: ${owner.pid})`);
|
|
3404
|
+
legacyNote(d, owner.pid);
|
|
3405
|
+
} else if (owner.state === "unknown") {
|
|
3406
|
+
d.out(`Codex adapter: unknown (${owner.error})`);
|
|
3407
|
+
} else {
|
|
3408
|
+
d.out("Codex adapter: not running");
|
|
3409
|
+
legacyNote(d);
|
|
3410
|
+
}
|
|
3411
|
+
d.out(`Start with hub: ${startWithHub ? "enabled" : "disabled"}`);
|
|
3412
|
+
return 0;
|
|
3413
|
+
}
|
|
3414
|
+
|
|
3415
|
+
// src/cli.ts
|
|
3416
|
+
var __dirname2 = path11.dirname(fileURLToPath2(import.meta.url));
|
|
2387
3417
|
function authHeaders(token) {
|
|
2388
3418
|
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
2389
3419
|
}
|
|
@@ -2407,6 +3437,10 @@ Usage:
|
|
|
2407
3437
|
claude-alarm codex start Run the Codex adapter on its own (e.g. Codex on another PC)
|
|
2408
3438
|
claude-alarm codex stop Stop a running Codex adapter
|
|
2409
3439
|
claude-alarm codex status Show Codex adapter status
|
|
3440
|
+
claude-alarm codex run --brief <file|-> [--cwd <dir>] [--thread <id>] [--name <title>]
|
|
3441
|
+
[--output-schema <file>] [--approval-timeout <min>] [--timeout <min>] [--yolo]
|
|
3442
|
+
Hand a task to Codex through the shared daemon and print the result as JSON
|
|
3443
|
+
(--yolo: no sandbox and no approvals, like codex --yolo)
|
|
2410
3444
|
claude-alarm help Show this help
|
|
2411
3445
|
|
|
2412
3446
|
Quick start:
|
|
@@ -2415,8 +3449,8 @@ Quick start:
|
|
|
2415
3449
|
}
|
|
2416
3450
|
async function checkForUpdates() {
|
|
2417
3451
|
try {
|
|
2418
|
-
const pkgPath =
|
|
2419
|
-
const pkg = JSON.parse(
|
|
3452
|
+
const pkgPath = path11.join(__dirname2, "..", "package.json");
|
|
3453
|
+
const pkg = JSON.parse(fs9.readFileSync(pkgPath, "utf-8"));
|
|
2420
3454
|
const currentVersion = pkg.version;
|
|
2421
3455
|
const res = await fetch("https://registry.npmjs.org/@delt/claude-alarm/latest", {
|
|
2422
3456
|
signal: AbortSignal.timeout(3e3)
|
|
@@ -2437,36 +3471,52 @@ async function hubStart(daemon) {
|
|
|
2437
3471
|
const config2 = loadConfig();
|
|
2438
3472
|
const host = config2.hub.host ?? DEFAULT_HUB_HOST;
|
|
2439
3473
|
const port = config2.hub.port ?? DEFAULT_HUB_PORT;
|
|
2440
|
-
const displayHost = host
|
|
3474
|
+
const displayHost = hubUrlHost(host);
|
|
2441
3475
|
checkForUpdates();
|
|
2442
|
-
if (
|
|
2443
|
-
const pid = parseInt(
|
|
3476
|
+
if (fs9.existsSync(PID_FILE)) {
|
|
3477
|
+
const pid = parseInt(fs9.readFileSync(PID_FILE, "utf-8").trim(), 10);
|
|
2444
3478
|
if (isProcessRunning(pid)) {
|
|
2445
3479
|
console.log(`Hub is already running (PID: ${pid}). Dashboard: ${loginLink(displayHost, port, config2.hub.token)}`);
|
|
2446
3480
|
return;
|
|
2447
3481
|
}
|
|
2448
|
-
|
|
3482
|
+
fs9.unlinkSync(PID_FILE);
|
|
2449
3483
|
}
|
|
2450
3484
|
if (daemon) {
|
|
2451
3485
|
ensureConfigDir();
|
|
2452
|
-
const logFd =
|
|
2453
|
-
const hubScript =
|
|
3486
|
+
const logFd = fs9.openSync(LOG_FILE, "a");
|
|
3487
|
+
const hubScript = path11.join(__dirname2, "hub", "server.js");
|
|
2454
3488
|
const child = spawn3(process.execPath, [hubScript], {
|
|
2455
3489
|
detached: true,
|
|
2456
3490
|
stdio: ["ignore", logFd, logFd],
|
|
2457
3491
|
env: { ...process.env }
|
|
2458
3492
|
});
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
child.unref();
|
|
2462
|
-
console.log(`Hub started as daemon (PID: ${child.pid})`);
|
|
2463
|
-
console.log(`Dashboard: ${loginLink(displayHost, port, config2.hub.token)}`);
|
|
2464
|
-
console.log(`Token: ${config2.hub.token}`);
|
|
2465
|
-
console.log(`Logs: ${LOG_FILE}`);
|
|
2466
|
-
} else {
|
|
3493
|
+
const pid = child.pid;
|
|
3494
|
+
if (!pid) {
|
|
2467
3495
|
console.error("Failed to start hub daemon");
|
|
2468
3496
|
process.exit(1);
|
|
2469
3497
|
}
|
|
3498
|
+
fs9.writeFileSync(PID_FILE, String(pid), "utf-8");
|
|
3499
|
+
let exited = false;
|
|
3500
|
+
child.once("exit", () => {
|
|
3501
|
+
exited = true;
|
|
3502
|
+
});
|
|
3503
|
+
child.unref();
|
|
3504
|
+
const startup = await waitForHub({
|
|
3505
|
+
url: `http://${displayHost}:${port}/api/status`,
|
|
3506
|
+
token: config2.hub.token,
|
|
3507
|
+
pid,
|
|
3508
|
+
isAlive: () => !exited
|
|
3509
|
+
});
|
|
3510
|
+
if (startup !== "ready") {
|
|
3511
|
+
if (!exited) child.kill();
|
|
3512
|
+
if (fs9.existsSync(PID_FILE)) fs9.unlinkSync(PID_FILE);
|
|
3513
|
+
console.error(startup === "exited" ? `Hub exited during startup. See ${LOG_FILE}` : `Hub did not answer at http://${displayHost}:${port} within ${HUB_START_TIMEOUT_MS / 1e3}s and was stopped. See ${LOG_FILE}`);
|
|
3514
|
+
process.exit(1);
|
|
3515
|
+
}
|
|
3516
|
+
console.log(`Hub started as daemon (PID: ${pid})`);
|
|
3517
|
+
console.log(`Dashboard: ${loginLink(displayHost, port, config2.hub.token)}`);
|
|
3518
|
+
console.log(`Token: ${config2.hub.token}`);
|
|
3519
|
+
console.log(`Logs: ${LOG_FILE}`);
|
|
2470
3520
|
} else {
|
|
2471
3521
|
console.log(`Starting hub on http://${displayHost}:${port} (press Ctrl+C to stop)`);
|
|
2472
3522
|
console.log(`Dashboard: ${loginLink(displayHost, port, config2.hub.token)}`);
|
|
@@ -2478,11 +3528,11 @@ async function hubStart(daemon) {
|
|
|
2478
3528
|
const hub = new HubServer2(config2);
|
|
2479
3529
|
await hub.start();
|
|
2480
3530
|
ensureConfigDir();
|
|
2481
|
-
|
|
3531
|
+
fs9.writeFileSync(PID_FILE, String(process.pid), "utf-8");
|
|
2482
3532
|
const shutdown = async () => {
|
|
2483
3533
|
console.log("\nShutting down...");
|
|
2484
3534
|
await hub.stop();
|
|
2485
|
-
if (
|
|
3535
|
+
if (fs9.existsSync(PID_FILE)) fs9.unlinkSync(PID_FILE);
|
|
2486
3536
|
process.exit(0);
|
|
2487
3537
|
};
|
|
2488
3538
|
process.on("SIGINT", shutdown);
|
|
@@ -2490,27 +3540,27 @@ async function hubStart(daemon) {
|
|
|
2490
3540
|
}
|
|
2491
3541
|
}
|
|
2492
3542
|
function hubStop() {
|
|
2493
|
-
if (!
|
|
3543
|
+
if (!fs9.existsSync(PID_FILE)) {
|
|
2494
3544
|
console.log("Hub is not running (no PID file found)");
|
|
2495
3545
|
return;
|
|
2496
3546
|
}
|
|
2497
|
-
const pid = parseInt(
|
|
3547
|
+
const pid = parseInt(fs9.readFileSync(PID_FILE, "utf-8").trim(), 10);
|
|
2498
3548
|
try {
|
|
2499
3549
|
process.kill(pid, "SIGTERM");
|
|
2500
3550
|
console.log(`Hub stopped (PID: ${pid})`);
|
|
2501
3551
|
} catch {
|
|
2502
3552
|
console.log("Hub process not found (may have already stopped)");
|
|
2503
3553
|
}
|
|
2504
|
-
|
|
3554
|
+
fs9.unlinkSync(PID_FILE);
|
|
2505
3555
|
}
|
|
2506
3556
|
async function hubStatus() {
|
|
2507
3557
|
const config2 = loadConfig();
|
|
2508
3558
|
const host = config2.hub.host ?? DEFAULT_HUB_HOST;
|
|
2509
3559
|
const port = config2.hub.port ?? DEFAULT_HUB_PORT;
|
|
2510
|
-
const displayHost = host
|
|
3560
|
+
const displayHost = hubUrlHost(host);
|
|
2511
3561
|
let pidInfo = "not running";
|
|
2512
|
-
if (
|
|
2513
|
-
const pid = parseInt(
|
|
3562
|
+
if (fs9.existsSync(PID_FILE)) {
|
|
3563
|
+
const pid = parseInt(fs9.readFileSync(PID_FILE, "utf-8").trim(), 10);
|
|
2514
3564
|
if (isProcessRunning(pid)) {
|
|
2515
3565
|
pidInfo = `running (PID: ${pid})`;
|
|
2516
3566
|
} else {
|
|
@@ -2518,7 +3568,7 @@ async function hubStatus() {
|
|
|
2518
3568
|
}
|
|
2519
3569
|
}
|
|
2520
3570
|
try {
|
|
2521
|
-
const res = await fetch(`http://${
|
|
3571
|
+
const res = await fetch(`http://${displayHost}:${port}/api/status`, { headers: authHeaders(config2.hub.token) });
|
|
2522
3572
|
if (res.ok) {
|
|
2523
3573
|
const data = await res.json();
|
|
2524
3574
|
console.log(`Hub: running (PID: ${data.pid})`);
|
|
@@ -2553,7 +3603,7 @@ async function test() {
|
|
|
2553
3603
|
if (config2.hub.token) {
|
|
2554
3604
|
headers["Authorization"] = `Bearer ${config2.hub.token}`;
|
|
2555
3605
|
}
|
|
2556
|
-
const res = await fetch(`http://${host}:${port}/api/notify`, {
|
|
3606
|
+
const res = await fetch(`http://${hubUrlHost(host)}:${port}/api/notify`, {
|
|
2557
3607
|
method: "POST",
|
|
2558
3608
|
headers,
|
|
2559
3609
|
body: JSON.stringify({
|
|
@@ -2571,7 +3621,7 @@ async function test() {
|
|
|
2571
3621
|
console.error("Could not reach hub. Start it first: claude-alarm hub start");
|
|
2572
3622
|
}
|
|
2573
3623
|
}
|
|
2574
|
-
function
|
|
3624
|
+
function ask2(question) {
|
|
2575
3625
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
2576
3626
|
return new Promise((resolve) => {
|
|
2577
3627
|
rl.question(question, (answer) => {
|
|
@@ -2582,18 +3632,18 @@ function ask(question) {
|
|
|
2582
3632
|
}
|
|
2583
3633
|
async function init() {
|
|
2584
3634
|
const dir = process.cwd();
|
|
2585
|
-
const projectName =
|
|
3635
|
+
const projectName = path11.basename(dir);
|
|
2586
3636
|
console.log(`
|
|
2587
3637
|
claude-alarm init for "${projectName}"
|
|
2588
3638
|
`);
|
|
2589
|
-
const remote = await
|
|
3639
|
+
const remote = await ask2("Connect to a remote hub? (y/N): ");
|
|
2590
3640
|
let env = {
|
|
2591
3641
|
CLAUDE_ALARM_SESSION_NAME: projectName
|
|
2592
3642
|
};
|
|
2593
3643
|
if (remote.toLowerCase() === "y") {
|
|
2594
|
-
const host = await
|
|
2595
|
-
const port = await
|
|
2596
|
-
const token = await
|
|
3644
|
+
const host = await ask2("Hub host (e.g. 192.168.1.100): ");
|
|
3645
|
+
const port = await ask2("Hub port (default: 7900): ");
|
|
3646
|
+
const token = await ask2("Hub token: ");
|
|
2597
3647
|
if (!host) {
|
|
2598
3648
|
console.error("Host is required.");
|
|
2599
3649
|
process.exit(1);
|
|
@@ -2602,11 +3652,11 @@ claude-alarm init for "${projectName}"
|
|
|
2602
3652
|
if (port) env.CLAUDE_ALARM_HUB_PORT = port;
|
|
2603
3653
|
if (token) env.CLAUDE_ALARM_HUB_TOKEN = token;
|
|
2604
3654
|
}
|
|
2605
|
-
const mcpPath =
|
|
3655
|
+
const mcpPath = path11.join(dir, ".mcp.json");
|
|
2606
3656
|
let mcpConfig = {};
|
|
2607
|
-
if (
|
|
3657
|
+
if (fs9.existsSync(mcpPath)) {
|
|
2608
3658
|
try {
|
|
2609
|
-
mcpConfig = JSON.parse(
|
|
3659
|
+
mcpConfig = JSON.parse(fs9.readFileSync(mcpPath, "utf-8"));
|
|
2610
3660
|
} catch {
|
|
2611
3661
|
mcpConfig = {};
|
|
2612
3662
|
}
|
|
@@ -2617,17 +3667,17 @@ claude-alarm init for "${projectName}"
|
|
|
2617
3667
|
args: ["-y", "@delt/claude-alarm", "serve"],
|
|
2618
3668
|
env
|
|
2619
3669
|
};
|
|
2620
|
-
|
|
3670
|
+
fs9.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2), "utf-8");
|
|
2621
3671
|
console.log(`
|
|
2622
3672
|
\u2713 Created ${mcpPath}`);
|
|
2623
3673
|
if (remote.toLowerCase() !== "y") {
|
|
2624
3674
|
const config2 = loadConfig();
|
|
2625
3675
|
const host = config2.hub.host ?? DEFAULT_HUB_HOST;
|
|
2626
3676
|
const port = config2.hub.port ?? DEFAULT_HUB_PORT;
|
|
2627
|
-
const displayHost = host
|
|
3677
|
+
const displayHost = hubUrlHost(host);
|
|
2628
3678
|
let hubRunning = false;
|
|
2629
3679
|
try {
|
|
2630
|
-
const res = await fetch(`http://${
|
|
3680
|
+
const res = await fetch(`http://${displayHost}:${port}/api/status`, { headers: authHeaders(config2.hub.token) });
|
|
2631
3681
|
hubRunning = res.ok;
|
|
2632
3682
|
} catch {
|
|
2633
3683
|
}
|
|
@@ -2638,8 +3688,8 @@ claude-alarm init for "${projectName}"
|
|
|
2638
3688
|
console.log(` claude-alarm hub start`);
|
|
2639
3689
|
}
|
|
2640
3690
|
console.log(` Dashboard: http://${displayHost}:${port}`);
|
|
2641
|
-
if (shouldOfferCodex(config2,
|
|
2642
|
-
const answer = await
|
|
3691
|
+
if (shouldOfferCodex(config2, findCodex("codex") !== void 0)) {
|
|
3692
|
+
const answer = await ask2("\nCodex is installed. Show Codex conversations on the dashboard too? (y/N): ");
|
|
2643
3693
|
if (answer.toLowerCase() === "y") codexEnable(true);
|
|
2644
3694
|
else setCodexEnabled(false);
|
|
2645
3695
|
}
|
|
@@ -2668,53 +3718,80 @@ function isProcessRunning(pid) {
|
|
|
2668
3718
|
}
|
|
2669
3719
|
}
|
|
2670
3720
|
function readCodexPid() {
|
|
2671
|
-
if (!
|
|
2672
|
-
const pid = parseInt(
|
|
3721
|
+
if (!fs9.existsSync(CODEX_PID_FILE)) return void 0;
|
|
3722
|
+
const pid = parseInt(fs9.readFileSync(CODEX_PID_FILE, "utf-8").trim(), 10);
|
|
2673
3723
|
return Number.isNaN(pid) ? void 0 : pid;
|
|
2674
3724
|
}
|
|
2675
3725
|
function codexEnable(enabled) {
|
|
2676
3726
|
setCodexEnabled(enabled);
|
|
2677
3727
|
console.log(enabled ? "Codex adapter enabled. Restart the hub to apply: claude-alarm hub stop, then claude-alarm hub start" : "Codex adapter disabled. Restart the hub to apply.");
|
|
2678
3728
|
}
|
|
2679
|
-
function codexStart() {
|
|
2680
|
-
const
|
|
2681
|
-
if (pid !== void 0 && isProcessRunning(pid)) {
|
|
2682
|
-
console.log(`Codex adapter is already running (PID: ${pid})`);
|
|
2683
|
-
return;
|
|
2684
|
-
}
|
|
3729
|
+
async function codexStart() {
|
|
3730
|
+
const config2 = loadConfig();
|
|
2685
3731
|
ensureConfigDir();
|
|
2686
|
-
|
|
2687
|
-
const
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
3732
|
+
legacyNote(controlDeps());
|
|
3733
|
+
const code = await startAdapter({
|
|
3734
|
+
hub: resolveAdapterHub(config2),
|
|
3735
|
+
command: config2.codex?.command ?? "codex",
|
|
3736
|
+
logFile: CODEX_LOG_FILE,
|
|
3737
|
+
queryOwner: () => queryOwner(controlEndpoint(CONFIG_DIR)),
|
|
3738
|
+
spawnAdapter: () => {
|
|
3739
|
+
const logFd = fs9.openSync(CODEX_LOG_FILE, "a");
|
|
3740
|
+
return spawn3(process.execPath, [path11.join(__dirname2, "codex", "main.js")], {
|
|
3741
|
+
detached: true,
|
|
3742
|
+
stdio: ["ignore", logFd, logFd, "ipc"],
|
|
3743
|
+
windowsHide: true,
|
|
3744
|
+
env: { ...process.env }
|
|
3745
|
+
});
|
|
3746
|
+
},
|
|
3747
|
+
out: (line) => console.log(line),
|
|
3748
|
+
err: (line) => console.error(line)
|
|
2691
3749
|
});
|
|
2692
|
-
|
|
2693
|
-
console.log(`Codex adapter started (PID: ${child.pid}). Logs: ${CODEX_LOG_FILE}`);
|
|
3750
|
+
if (code !== 0) process.exit(code);
|
|
2694
3751
|
}
|
|
2695
|
-
function
|
|
2696
|
-
const
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
3752
|
+
function controlDeps() {
|
|
3753
|
+
const endpoint = controlEndpoint(CONFIG_DIR);
|
|
3754
|
+
const token = loadConfig().hub.token ?? "";
|
|
3755
|
+
return {
|
|
3756
|
+
queryOwner: () => queryOwner(endpoint),
|
|
3757
|
+
requestStop: () => requestStop(endpoint, token),
|
|
3758
|
+
legacyPid: readCodexPid,
|
|
3759
|
+
isRunning: isProcessRunning,
|
|
3760
|
+
removeLegacyPidFile: () => {
|
|
3761
|
+
try {
|
|
3762
|
+
fs9.unlinkSync(CODEX_PID_FILE);
|
|
3763
|
+
} catch {
|
|
3764
|
+
}
|
|
3765
|
+
},
|
|
3766
|
+
legacyPidFile: CODEX_PID_FILE,
|
|
3767
|
+
configFile: CONFIG_FILE,
|
|
3768
|
+
out: (line) => console.log(line),
|
|
3769
|
+
err: (line) => console.error(line)
|
|
3770
|
+
};
|
|
3771
|
+
}
|
|
3772
|
+
async function codexRun(argv) {
|
|
3773
|
+
const abort = new AbortController();
|
|
3774
|
+
const stop = () => abort.abort();
|
|
3775
|
+
process.on("SIGINT", stop);
|
|
3776
|
+
process.on("SIGTERM", stop);
|
|
2707
3777
|
try {
|
|
2708
|
-
|
|
2709
|
-
|
|
3778
|
+
return await runFromCli(argv, {
|
|
3779
|
+
readFile: (file) => fs9.promises.readFile(file, "utf-8"),
|
|
3780
|
+
readStdin: async () => {
|
|
3781
|
+
let text = "";
|
|
3782
|
+
for await (const chunk of process.stdin) text += chunk;
|
|
3783
|
+
return text;
|
|
3784
|
+
},
|
|
3785
|
+
out: (text) => process.stdout.write(text),
|
|
3786
|
+
err: (text) => process.stderr.write(text),
|
|
3787
|
+
cwd: process.cwd(),
|
|
3788
|
+
signal: abort.signal
|
|
3789
|
+
});
|
|
3790
|
+
} finally {
|
|
3791
|
+
process.off("SIGINT", stop);
|
|
3792
|
+
process.off("SIGTERM", stop);
|
|
2710
3793
|
}
|
|
2711
3794
|
}
|
|
2712
|
-
function codexStatus() {
|
|
2713
|
-
const pid = readCodexPid();
|
|
2714
|
-
const state = pid === void 0 ? "not running" : isProcessRunning(pid) ? `running (PID: ${pid})` : "not running (stale PID file)";
|
|
2715
|
-
console.log(`Codex adapter: ${state}`);
|
|
2716
|
-
console.log(`Start with hub: ${loadConfig().codex?.enabled ? "enabled" : "disabled"}`);
|
|
2717
|
-
}
|
|
2718
3795
|
async function main2() {
|
|
2719
3796
|
const args = process.argv.slice(2);
|
|
2720
3797
|
const cmd = args[0];
|
|
@@ -2749,9 +3826,10 @@ async function main2() {
|
|
|
2749
3826
|
if (cmd === "codex") {
|
|
2750
3827
|
if (sub === "enable") codexEnable(true);
|
|
2751
3828
|
else if (sub === "disable") codexEnable(false);
|
|
2752
|
-
else if (sub === "start") codexStart();
|
|
2753
|
-
else if (sub === "stop")
|
|
2754
|
-
else if (sub === "status")
|
|
3829
|
+
else if (sub === "start") await codexStart();
|
|
3830
|
+
else if (sub === "stop") process.exitCode = await stopAdapter(controlDeps(), loadConfig().codex?.enabled === true);
|
|
3831
|
+
else if (sub === "status") process.exitCode = await adapterStatus(controlDeps(), loadConfig().codex?.enabled === true);
|
|
3832
|
+
else if (sub === "run") process.exitCode = await codexRun(args.slice(2));
|
|
2755
3833
|
else {
|
|
2756
3834
|
console.error(`Unknown codex command: ${sub}`);
|
|
2757
3835
|
printUsage();
|