@delt/claude-alarm 1.2.0 → 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 +2 -1
- package/dist/cli.js +408 -169
- package/dist/cli.js.map +1 -1
- package/dist/codex/main.js +302 -65
- package/dist/codex/main.js.map +1 -1
- package/dist/dashboard/index.html +14 -0
- package/dist/hub/server.js +143 -48
- package/dist/hub/server.js.map +1 -1
- package/dist/index.d.ts +11 -1
- package/dist/index.js +143 -48
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/dashboard/index.html +14 -0
package/dist/cli.js
CHANGED
|
@@ -439,10 +439,13 @@ var init_permission_key = __esm({
|
|
|
439
439
|
});
|
|
440
440
|
|
|
441
441
|
// src/hub/telegram.ts
|
|
442
|
-
import
|
|
443
|
-
import
|
|
442
|
+
import fs5 from "fs";
|
|
443
|
+
import path6 from "path";
|
|
444
444
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
445
|
-
|
|
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;
|
|
446
449
|
var init_telegram = __esm({
|
|
447
450
|
"src/hub/telegram.ts"() {
|
|
448
451
|
"use strict";
|
|
@@ -452,6 +455,10 @@ var init_telegram = __esm({
|
|
|
452
455
|
init_permission_key();
|
|
453
456
|
TELEGRAM_API = "https://api.telegram.org/bot";
|
|
454
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;
|
|
455
462
|
TelegramBot = class _TelegramBot {
|
|
456
463
|
config;
|
|
457
464
|
offset = 0;
|
|
@@ -472,9 +479,9 @@ var init_telegram = __esm({
|
|
|
472
479
|
choiceMessages = /* @__PURE__ */ new Map();
|
|
473
480
|
// Callback: get current sessions list
|
|
474
481
|
getSessions;
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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();
|
|
478
485
|
constructor(config2) {
|
|
479
486
|
this.config = config2;
|
|
480
487
|
}
|
|
@@ -483,8 +490,7 @@ var init_telegram = __esm({
|
|
|
483
490
|
}
|
|
484
491
|
/** Send a notification message to Telegram */
|
|
485
492
|
async sendNotification(sessionId2, _sessionLabel, title, message) {
|
|
486
|
-
const text =
|
|
487
|
-
${this.mdToHtml(message)}`;
|
|
493
|
+
const text = this.fitNotification(title, message);
|
|
488
494
|
const result = await this.sendMessage(text);
|
|
489
495
|
if (result?.message_id) {
|
|
490
496
|
this.messageSessionMap.set(result.message_id, sessionId2);
|
|
@@ -496,6 +502,25 @@ ${this.mdToHtml(message)}`;
|
|
|
496
502
|
}
|
|
497
503
|
}
|
|
498
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
|
+
}
|
|
499
524
|
/** Send a text message to the configured chat */
|
|
500
525
|
async sendMessage(text, replyToMessageId, replyMarkup) {
|
|
501
526
|
try {
|
|
@@ -577,6 +602,13 @@ ${this.mdToHtml(message)}`;
|
|
|
577
602
|
const hasPhoto = msg.photo && msg.photo.length > 0;
|
|
578
603
|
const text = (msg.text || msg.caption || "").trim();
|
|
579
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
|
+
}
|
|
580
612
|
if (msg.reply_to_message) {
|
|
581
613
|
const sessionId2 = this.messageSessionMap.get(msg.reply_to_message.message_id);
|
|
582
614
|
if (sessionId2) {
|
|
@@ -590,18 +622,20 @@ ${this.mdToHtml(message)}`;
|
|
|
590
622
|
}
|
|
591
623
|
if (text) {
|
|
592
624
|
const selectMatch = text.match(/^\/s_(\d+)$/);
|
|
593
|
-
|
|
594
|
-
|
|
625
|
+
const latest = this.latestSelection;
|
|
626
|
+
if (selectMatch && latest !== void 0) {
|
|
627
|
+
const pending = this.selections.get(latest);
|
|
595
628
|
if (pending) {
|
|
596
|
-
this.
|
|
629
|
+
this.selections.delete(latest);
|
|
597
630
|
const session = this.pendingSession(pending, parseInt(selectMatch[1], 10) - 1);
|
|
598
631
|
if (session) {
|
|
632
|
+
let delivered = true;
|
|
599
633
|
if (pending.photoFileId) {
|
|
600
|
-
await this.deliverPhotoToSessionByFileId(session.id, pending.photoFileId, pending.caption);
|
|
634
|
+
delivered = await this.deliverPhotoToSessionByFileId(session.id, pending.photoFileId, pending.caption);
|
|
601
635
|
} else if (pending.text) {
|
|
602
636
|
this.deliverToSession(session.id, pending.text);
|
|
603
637
|
}
|
|
604
|
-
this.sendMessage(`Sent to [${this.getLabel(session)}]`);
|
|
638
|
+
if (delivered) this.sendMessage(`Sent to [${this.getLabel(session)}]`);
|
|
605
639
|
} else {
|
|
606
640
|
this.sendMessage("Invalid session number.");
|
|
607
641
|
}
|
|
@@ -622,16 +656,19 @@ ${this.mdToHtml(message)}`;
|
|
|
622
656
|
}
|
|
623
657
|
return;
|
|
624
658
|
}
|
|
659
|
+
const selectionId = randomUUID2().replace(/-/g, "").slice(0, 8);
|
|
660
|
+
this.latestSelection = selectionId;
|
|
625
661
|
const sessionIds = sessions.map((s) => s.id);
|
|
626
662
|
if (hasPhoto) {
|
|
627
663
|
const largest = msg.photo[msg.photo.length - 1];
|
|
628
|
-
this.
|
|
664
|
+
this.selections.set(selectionId, { photoFileId: largest.file_id, caption: text, sessionIds });
|
|
629
665
|
} else {
|
|
630
|
-
this.
|
|
666
|
+
this.selections.set(selectionId, { text, sessionIds });
|
|
631
667
|
}
|
|
668
|
+
while (this.selections.size > MAX_SELECTIONS) this.selections.delete(this.selections.keys().next().value);
|
|
632
669
|
const buttons = sessions.map((s, i) => ({
|
|
633
670
|
text: this.getLabel(s),
|
|
634
|
-
callback_data: `
|
|
671
|
+
callback_data: `sel:${selectionId}:${i}`
|
|
635
672
|
}));
|
|
636
673
|
const rows = [];
|
|
637
674
|
for (let i = 0; i < buttons.length; i += 2) {
|
|
@@ -646,44 +683,54 @@ ${this.mdToHtml(message)}`;
|
|
|
646
683
|
}
|
|
647
684
|
async deliverPhotoToSession(sessionId2, photos, caption) {
|
|
648
685
|
const largest = photos[photos.length - 1];
|
|
649
|
-
|
|
686
|
+
return this.deliverPhotoToSessionByFileId(sessionId2, largest.file_id, caption);
|
|
650
687
|
}
|
|
651
688
|
async deliverPhotoToSessionByFileId(sessionId2, fileId, caption) {
|
|
652
689
|
try {
|
|
653
690
|
const fileRes = await fetch(`${this.apiUrl}/getFile?file_id=${fileId}`);
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
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;
|
|
657
695
|
}
|
|
658
|
-
const fileData = await fileRes.json();
|
|
659
|
-
if (!fileData.ok) return;
|
|
660
696
|
const downloadUrl = `https://api.telegram.org/file/bot${this.config.botToken}/${fileData.result.file_path}`;
|
|
661
697
|
const imgRes = await fetch(downloadUrl);
|
|
662
698
|
if (!imgRes.ok) {
|
|
663
|
-
|
|
664
|
-
return;
|
|
699
|
+
this.photoNotDelivered("the download failed");
|
|
700
|
+
return false;
|
|
665
701
|
}
|
|
666
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
|
+
}
|
|
667
707
|
const ext = fileData.result.file_path.split(".").pop() || "jpg";
|
|
668
708
|
const mimeType = ext === "png" ? "image/png" : ext === "gif" ? "image/gif" : ext === "webp" ? "image/webp" : "image/jpeg";
|
|
669
|
-
|
|
709
|
+
fs5.mkdirSync(UPLOADS_DIR, { recursive: true });
|
|
670
710
|
const filename = `${randomUUID2()}.${ext}`;
|
|
671
|
-
const filePath =
|
|
672
|
-
|
|
711
|
+
const filePath = path6.join(UPLOADS_DIR, filename);
|
|
712
|
+
fs5.writeFileSync(filePath, buffer);
|
|
673
713
|
logger.info(`Telegram photo saved: ${filename} (${buffer.length} bytes)`);
|
|
674
714
|
if (this.onImageToSession) {
|
|
675
715
|
this.onImageToSession(sessionId2, filePath, mimeType, caption);
|
|
676
716
|
}
|
|
677
717
|
setTimeout(() => {
|
|
678
718
|
try {
|
|
679
|
-
|
|
719
|
+
fs5.unlinkSync(filePath);
|
|
680
720
|
} catch {
|
|
681
721
|
}
|
|
682
|
-
}, 5 * 60 * 1e3);
|
|
722
|
+
}, 5 * 60 * 1e3).unref();
|
|
723
|
+
return this.onImageToSession !== void 0;
|
|
683
724
|
} catch (err) {
|
|
684
725
|
logger.warn(`Telegram photo download failed: ${err.message}`);
|
|
726
|
+
this.photoNotDelivered("the download failed");
|
|
727
|
+
return false;
|
|
685
728
|
}
|
|
686
729
|
}
|
|
730
|
+
photoNotDelivered(reason) {
|
|
731
|
+
logger.warn(`Telegram photo not delivered: ${reason}`);
|
|
732
|
+
void this.sendMessage(`Photo not delivered: ${reason}`);
|
|
733
|
+
}
|
|
687
734
|
// Buttons are numbered against the list shown when they were sent; Codex sessions come and go, so the live list may have shifted.
|
|
688
735
|
pendingSession(pending, idx) {
|
|
689
736
|
const id = pending.sessionIds[idx];
|
|
@@ -797,7 +844,7 @@ ${this.escHtml(description)}` : "") + (slice ? `
|
|
|
797
844
|
async handleChoiceCallback(query) {
|
|
798
845
|
const choice = this.choiceTokens.get(query.data.slice("pc:".length));
|
|
799
846
|
if (!choice) {
|
|
800
|
-
await this.
|
|
847
|
+
await this.expire(query);
|
|
801
848
|
return;
|
|
802
849
|
}
|
|
803
850
|
this.dropChoiceTokens(choice.sessionId, choice.requestId);
|
|
@@ -826,10 +873,14 @@ ${this.escHtml(description)}` : "") + (slice ? `
|
|
|
826
873
|
async handleCallbackQuery(query) {
|
|
827
874
|
if (!query.data) return;
|
|
828
875
|
if (String(query.message?.chat.id) !== String(this.config.chatId)) return;
|
|
829
|
-
if (query.data.startsWith("
|
|
876
|
+
if (query.data.startsWith("sel:")) {
|
|
830
877
|
await this.handleSessionSelectCallback(query);
|
|
831
878
|
return;
|
|
832
879
|
}
|
|
880
|
+
if (query.data.startsWith("sess:")) {
|
|
881
|
+
await this.expire(query);
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
833
884
|
if (query.data.startsWith("pc:")) {
|
|
834
885
|
await this.handleChoiceCallback(query);
|
|
835
886
|
return;
|
|
@@ -852,19 +903,25 @@ ${label}`);
|
|
|
852
903
|
}
|
|
853
904
|
}
|
|
854
905
|
async handleSessionSelectCallback(query) {
|
|
855
|
-
const
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
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) {
|
|
862
914
|
await this.answerCallbackQuery(query.id, "Session not found");
|
|
863
915
|
return;
|
|
864
916
|
}
|
|
865
|
-
this.
|
|
917
|
+
this.selections.delete(selectionId);
|
|
866
918
|
if (pending.photoFileId) {
|
|
867
|
-
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
|
+
}
|
|
868
925
|
} else if (pending.text) {
|
|
869
926
|
this.deliverToSession(session.id, pending.text);
|
|
870
927
|
}
|
|
@@ -884,6 +941,21 @@ ${label}`);
|
|
|
884
941
|
logger.warn(`Telegram answerCallbackQuery error: ${err.message}`);
|
|
885
942
|
}
|
|
886
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
|
+
}
|
|
887
959
|
async editMessageText(chatId, messageId, text) {
|
|
888
960
|
try {
|
|
889
961
|
await fetch(`${this.apiUrl}/editMessageText`, {
|
|
@@ -928,8 +1000,8 @@ ${label}`);
|
|
|
928
1000
|
css: "css",
|
|
929
1001
|
scss: "scss"
|
|
930
1002
|
};
|
|
931
|
-
langOf(
|
|
932
|
-
const m = /\.([a-z0-9]+)$/i.exec(
|
|
1003
|
+
langOf(path12) {
|
|
1004
|
+
const m = /\.([a-z0-9]+)$/i.exec(path12 || "");
|
|
933
1005
|
return m ? _TelegramBot.LANG_BY_EXT[m[1].toLowerCase()] ?? "" : "";
|
|
934
1006
|
}
|
|
935
1007
|
/** Convert markdown to Telegram HTML (escape first, then apply formatting) */
|
|
@@ -976,10 +1048,15 @@ ${bodyLines.join("\n")}`;
|
|
|
976
1048
|
|
|
977
1049
|
// src/hub/codex-supervisor.ts
|
|
978
1050
|
import { spawn as spawn2 } from "child_process";
|
|
979
|
-
import
|
|
980
|
-
import
|
|
1051
|
+
import fs6 from "fs";
|
|
1052
|
+
import path7 from "path";
|
|
981
1053
|
function resolveAdapterScript(baseDir) {
|
|
982
|
-
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;
|
|
983
1060
|
}
|
|
984
1061
|
var HEALTHY_RUN_MS, CodexSupervisor;
|
|
985
1062
|
var init_codex_supervisor = __esm({
|
|
@@ -988,18 +1065,24 @@ var init_codex_supervisor = __esm({
|
|
|
988
1065
|
init_logger();
|
|
989
1066
|
HEALTHY_RUN_MS = 6e4;
|
|
990
1067
|
CodexSupervisor = class {
|
|
991
|
-
constructor(script,
|
|
1068
|
+
constructor(script, opts = {}) {
|
|
992
1069
|
this.script = script;
|
|
993
|
-
this.spawnFn = spawnFn;
|
|
994
|
-
this.minDelayMs = minDelayMs;
|
|
995
|
-
this.maxDelayMs = maxDelayMs;
|
|
996
|
-
this.stopGraceMs = stopGraceMs;
|
|
997
|
-
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;
|
|
998
1076
|
}
|
|
999
1077
|
child;
|
|
1000
1078
|
timer;
|
|
1001
1079
|
stopped = true;
|
|
1002
1080
|
delay;
|
|
1081
|
+
spawnFn;
|
|
1082
|
+
minDelayMs;
|
|
1083
|
+
maxDelayMs;
|
|
1084
|
+
stopGraceMs;
|
|
1085
|
+
env;
|
|
1003
1086
|
start() {
|
|
1004
1087
|
this.stopped = false;
|
|
1005
1088
|
this.launch();
|
|
@@ -1021,7 +1104,8 @@ var init_codex_supervisor = __esm({
|
|
|
1021
1104
|
const child = this.spawnFn(process.execPath, [this.script, "--watch-stdin"], {
|
|
1022
1105
|
detached: true,
|
|
1023
1106
|
stdio: ["pipe", "pipe", "pipe"],
|
|
1024
|
-
windowsHide: true
|
|
1107
|
+
windowsHide: true,
|
|
1108
|
+
...this.env ? { env: this.env } : {}
|
|
1025
1109
|
});
|
|
1026
1110
|
this.child = child;
|
|
1027
1111
|
child.stdout?.on("data", (d) => process.stdout.write(d));
|
|
@@ -1058,15 +1142,15 @@ var init_entry = __esm({
|
|
|
1058
1142
|
});
|
|
1059
1143
|
|
|
1060
1144
|
// src/hub/auth.ts
|
|
1061
|
-
import
|
|
1145
|
+
import crypto2 from "crypto";
|
|
1062
1146
|
function safeEqual(a, b) {
|
|
1063
1147
|
if (typeof a !== "string") return false;
|
|
1064
1148
|
const x = Buffer.from(a);
|
|
1065
1149
|
const y = Buffer.from(b);
|
|
1066
|
-
return x.length === y.length &&
|
|
1150
|
+
return x.length === y.length && crypto2.timingSafeEqual(x, y);
|
|
1067
1151
|
}
|
|
1068
1152
|
function dashboardCookieValue(token) {
|
|
1069
|
-
return
|
|
1153
|
+
return crypto2.createHmac("sha256", token).update("claude-alarm-dashboard").digest("hex");
|
|
1070
1154
|
}
|
|
1071
1155
|
function parseCookies(header) {
|
|
1072
1156
|
const out = {};
|
|
@@ -1126,8 +1210,8 @@ __export(server_exports, {
|
|
|
1126
1210
|
HubServer: () => HubServer
|
|
1127
1211
|
});
|
|
1128
1212
|
import http from "http";
|
|
1129
|
-
import
|
|
1130
|
-
import
|
|
1213
|
+
import fs7 from "fs";
|
|
1214
|
+
import path8 from "path";
|
|
1131
1215
|
import { fileURLToPath } from "url";
|
|
1132
1216
|
import { WebSocketServer, WebSocket as WebSocket2 } from "ws";
|
|
1133
1217
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
@@ -1153,7 +1237,7 @@ var init_server = __esm({
|
|
|
1153
1237
|
init_entry();
|
|
1154
1238
|
init_hub_url();
|
|
1155
1239
|
init_auth();
|
|
1156
|
-
__dirname =
|
|
1240
|
+
__dirname = path8.dirname(fileURLToPath(import.meta.url));
|
|
1157
1241
|
MAX_WS_PAYLOAD = 16 * 1024 * 1024;
|
|
1158
1242
|
nonEmpty = (v) => typeof v === "string" && v.trim() !== "";
|
|
1159
1243
|
HubServer = class {
|
|
@@ -1374,23 +1458,23 @@ var init_server = __esm({
|
|
|
1374
1458
|
return;
|
|
1375
1459
|
}
|
|
1376
1460
|
const candidates = [
|
|
1377
|
-
|
|
1461
|
+
path8.join(__dirname, "..", "dashboard", "index.html"),
|
|
1378
1462
|
// from dist/hub/
|
|
1379
|
-
|
|
1463
|
+
path8.join(__dirname, "dashboard", "index.html"),
|
|
1380
1464
|
// from dist/ (bundled index.js)
|
|
1381
|
-
|
|
1465
|
+
path8.join(__dirname, "..", "..", "src", "dashboard", "index.html"),
|
|
1382
1466
|
// from dist/hub/ -> src/
|
|
1383
|
-
|
|
1467
|
+
path8.join(__dirname, "..", "src", "dashboard", "index.html"),
|
|
1384
1468
|
// from dist/ -> src/
|
|
1385
|
-
|
|
1469
|
+
path8.join(process.cwd(), "dist", "dashboard", "index.html"),
|
|
1386
1470
|
// from cwd
|
|
1387
|
-
|
|
1471
|
+
path8.join(process.cwd(), "src", "dashboard", "index.html")
|
|
1388
1472
|
// from cwd/src
|
|
1389
1473
|
];
|
|
1390
1474
|
logger.debug(`Dashboard candidates: ${JSON.stringify(candidates)}`);
|
|
1391
1475
|
for (const candidate of candidates) {
|
|
1392
|
-
if (
|
|
1393
|
-
const html =
|
|
1476
|
+
if (fs7.existsSync(candidate)) {
|
|
1477
|
+
const html = fs7.readFileSync(candidate, "utf-8");
|
|
1394
1478
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
1395
1479
|
res.end(html);
|
|
1396
1480
|
return;
|
|
@@ -1744,7 +1828,7 @@ var init_server = __esm({
|
|
|
1744
1828
|
channelWs.send(JSON.stringify({ ...msg, source: "dashboard" }));
|
|
1745
1829
|
}
|
|
1746
1830
|
} else if (msg.type === "image_upload") {
|
|
1747
|
-
this.handleImageUpload(msg);
|
|
1831
|
+
this.handleImageUpload(ws, msg);
|
|
1748
1832
|
} else if (msg.type === "permission_response") {
|
|
1749
1833
|
if (this.forwardPermissionResponse(msg)) {
|
|
1750
1834
|
const verdict = msg.choiceId !== void 0 ? `choice ${msg.choiceId}` : msg.behavior;
|
|
@@ -1795,27 +1879,38 @@ var init_server = __esm({
|
|
|
1795
1879
|
}
|
|
1796
1880
|
}
|
|
1797
1881
|
}
|
|
1798
|
-
handleImageUpload(msg) {
|
|
1882
|
+
handleImageUpload(ws, msg) {
|
|
1799
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
|
+
}
|
|
1800
1894
|
if (!this.localChannels.has(sessionId2)) {
|
|
1801
|
-
|
|
1895
|
+
reject("this session is on another PC; images can only go to sessions on the hub's PC");
|
|
1802
1896
|
return;
|
|
1803
1897
|
}
|
|
1804
|
-
const channelWs = this.channelSockets.get(sessionId2);
|
|
1805
|
-
if (!channelWs || channelWs.readyState !== WebSocket2.OPEN) return;
|
|
1806
1898
|
const allowedTypes = ["image/png", "image/jpeg", "image/gif", "image/webp"];
|
|
1807
|
-
if (!allowedTypes.includes(mimeType))
|
|
1899
|
+
if (!allowedTypes.includes(mimeType)) {
|
|
1900
|
+
reject("only PNG, JPEG, GIF and WebP images are supported");
|
|
1901
|
+
return;
|
|
1902
|
+
}
|
|
1808
1903
|
const base64Data = imageData.replace(/^data:image\/\w+;base64,/, "");
|
|
1809
1904
|
const buffer = Buffer.from(base64Data, "base64");
|
|
1810
1905
|
if (buffer.length > 10 * 1024 * 1024) {
|
|
1811
|
-
|
|
1906
|
+
reject("the image is larger than 10 MB");
|
|
1812
1907
|
return;
|
|
1813
1908
|
}
|
|
1814
|
-
|
|
1909
|
+
fs7.mkdirSync(UPLOADS_DIR, { recursive: true });
|
|
1815
1910
|
const ext = mimeType.split("/")[1] === "jpeg" ? "jpg" : mimeType.split("/")[1];
|
|
1816
1911
|
const filename = `${randomUUID3()}.${ext}`;
|
|
1817
|
-
const filePath =
|
|
1818
|
-
|
|
1912
|
+
const filePath = path8.join(UPLOADS_DIR, filename);
|
|
1913
|
+
fs7.writeFileSync(filePath, buffer);
|
|
1819
1914
|
const forwardMsg = {
|
|
1820
1915
|
type: "image_to_session",
|
|
1821
1916
|
sessionId: sessionId2,
|
|
@@ -1829,7 +1924,7 @@ var init_server = __esm({
|
|
|
1829
1924
|
logger.info(`Image saved and forwarded: ${filename} (${buffer.length} bytes)`);
|
|
1830
1925
|
setTimeout(() => {
|
|
1831
1926
|
try {
|
|
1832
|
-
|
|
1927
|
+
fs7.unlinkSync(filePath);
|
|
1833
1928
|
} catch {
|
|
1834
1929
|
}
|
|
1835
1930
|
}, 5 * 60 * 1e3).unref();
|
|
@@ -1993,7 +2088,7 @@ var init_server = __esm({
|
|
|
1993
2088
|
logger.warn("Codex adapter is enabled but codex/main.js was not found next to the hub");
|
|
1994
2089
|
return;
|
|
1995
2090
|
}
|
|
1996
|
-
this.codexSupervisor = new CodexSupervisor(script);
|
|
2091
|
+
this.codexSupervisor = new CodexSupervisor(script, { env: adapterEnv(process.env, { host: this.host, port: this.port }) });
|
|
1997
2092
|
this.codexSupervisor.start();
|
|
1998
2093
|
logger.info("Codex adapter started");
|
|
1999
2094
|
}
|
|
@@ -2020,11 +2115,11 @@ var init_server = __esm({
|
|
|
2020
2115
|
}
|
|
2021
2116
|
cleanupUploads() {
|
|
2022
2117
|
try {
|
|
2023
|
-
if (!
|
|
2024
|
-
const files =
|
|
2118
|
+
if (!fs7.existsSync(UPLOADS_DIR)) return;
|
|
2119
|
+
const files = fs7.readdirSync(UPLOADS_DIR);
|
|
2025
2120
|
for (const file of files) {
|
|
2026
2121
|
try {
|
|
2027
|
-
|
|
2122
|
+
fs7.unlinkSync(path8.join(UPLOADS_DIR, file));
|
|
2028
2123
|
} catch {
|
|
2029
2124
|
}
|
|
2030
2125
|
}
|
|
@@ -2223,9 +2318,9 @@ var init_hub_client = __esm({
|
|
|
2223
2318
|
});
|
|
2224
2319
|
|
|
2225
2320
|
// src/channel/peer-name.ts
|
|
2226
|
-
import
|
|
2321
|
+
import fs8 from "fs";
|
|
2227
2322
|
import os2 from "os";
|
|
2228
|
-
import
|
|
2323
|
+
import path9 from "path";
|
|
2229
2324
|
function findPeerName(records, env) {
|
|
2230
2325
|
const valid = records.filter((r) => typeof r === "object" && r !== null);
|
|
2231
2326
|
const nameOf = (match) => {
|
|
@@ -2245,18 +2340,18 @@ function readPeerName(env = process.env) {
|
|
|
2245
2340
|
sessionId: env.CLAUDE_CODE_SESSION_ID
|
|
2246
2341
|
};
|
|
2247
2342
|
if (!lookup.messagingSocket && !lookup.sessionId) return void 0;
|
|
2248
|
-
const configDir = env.CLAUDE_CONFIG_DIR ||
|
|
2249
|
-
const dir =
|
|
2343
|
+
const configDir = env.CLAUDE_CONFIG_DIR || path9.join(os2.homedir(), ".claude");
|
|
2344
|
+
const dir = path9.join(configDir, "sessions");
|
|
2250
2345
|
let files;
|
|
2251
2346
|
try {
|
|
2252
|
-
files =
|
|
2347
|
+
files = fs8.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
2253
2348
|
} catch {
|
|
2254
2349
|
return void 0;
|
|
2255
2350
|
}
|
|
2256
2351
|
const records = [];
|
|
2257
2352
|
for (const f of files) {
|
|
2258
2353
|
try {
|
|
2259
|
-
records.push(JSON.parse(
|
|
2354
|
+
records.push(JSON.parse(fs8.readFileSync(path9.join(dir, f), "utf8")));
|
|
2260
2355
|
} catch {
|
|
2261
2356
|
}
|
|
2262
2357
|
}
|
|
@@ -2279,7 +2374,7 @@ import {
|
|
|
2279
2374
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
2280
2375
|
import { z } from "zod";
|
|
2281
2376
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
2282
|
-
import
|
|
2377
|
+
import path10 from "path";
|
|
2283
2378
|
async function main() {
|
|
2284
2379
|
logger.info(`Starting MCP channel server (session: ${sessionId})`);
|
|
2285
2380
|
hubClient.connect();
|
|
@@ -2348,7 +2443,7 @@ var init_server2 = __esm({
|
|
|
2348
2443
|
init_hub_client();
|
|
2349
2444
|
init_peer_name2();
|
|
2350
2445
|
sessionId = randomUUID4();
|
|
2351
|
-
sessionName = process.env.CLAUDE_ALARM_SESSION_NAME ??
|
|
2446
|
+
sessionName = process.env.CLAUDE_ALARM_SESSION_NAME ?? path10.basename(process.cwd());
|
|
2352
2447
|
peerName = readPeerName();
|
|
2353
2448
|
server = new Server(
|
|
2354
2449
|
{
|
|
@@ -2525,8 +2620,8 @@ NOTIFICATIONS:
|
|
|
2525
2620
|
// src/cli.ts
|
|
2526
2621
|
init_config();
|
|
2527
2622
|
import { spawn as spawn3 } from "child_process";
|
|
2528
|
-
import
|
|
2529
|
-
import
|
|
2623
|
+
import fs9 from "fs";
|
|
2624
|
+
import path11 from "path";
|
|
2530
2625
|
import readline from "readline";
|
|
2531
2626
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2532
2627
|
|
|
@@ -2538,7 +2633,8 @@ import path3 from "path";
|
|
|
2538
2633
|
import { Duplex } from "stream";
|
|
2539
2634
|
import WebSocket from "ws";
|
|
2540
2635
|
function resolveCommand(command, platform = process.platform, env = process.env) {
|
|
2541
|
-
if (platform !== "win32"
|
|
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) };
|
|
2542
2638
|
const file = findCodex(command, platform, env);
|
|
2543
2639
|
return file ? { file, shell: file.endsWith(".cmd") } : { file: command, shell: false };
|
|
2544
2640
|
}
|
|
@@ -2566,7 +2662,8 @@ var defaultSpawn = (command, args) => {
|
|
|
2566
2662
|
if (shell) return spawn(`"${file}" ${args.join(" ")}`, { stdio: "pipe", windowsHide: true, shell: true });
|
|
2567
2663
|
return spawn(file, args, { stdio: "pipe", windowsHide: true });
|
|
2568
2664
|
};
|
|
2569
|
-
|
|
2665
|
+
var HANDSHAKE_TIMEOUT_MS = 1e4;
|
|
2666
|
+
function connectProxy(command, spawnFn = defaultSpawn, timeoutMs = HANDSHAKE_TIMEOUT_MS) {
|
|
2570
2667
|
const child = spawnFn(command, ["app-server", "proxy"]);
|
|
2571
2668
|
child.stdin?.on("error", () => {
|
|
2572
2669
|
});
|
|
@@ -2600,9 +2697,11 @@ function connectProxy(command, spawnFn = defaultSpawn) {
|
|
|
2600
2697
|
};
|
|
2601
2698
|
return new Promise((resolve, reject) => {
|
|
2602
2699
|
let settled = false;
|
|
2700
|
+
const timer = setTimeout(() => fail(new Error(`codex daemon did not answer within ${timeoutMs / 1e3}s`)), timeoutMs);
|
|
2603
2701
|
const fail = (err) => {
|
|
2604
2702
|
if (settled) return;
|
|
2605
2703
|
settled = true;
|
|
2704
|
+
clearTimeout(timer);
|
|
2606
2705
|
close();
|
|
2607
2706
|
reject(err);
|
|
2608
2707
|
};
|
|
@@ -2612,6 +2711,7 @@ function connectProxy(command, spawnFn = defaultSpawn) {
|
|
|
2612
2711
|
ws.once("open", () => {
|
|
2613
2712
|
if (settled) return;
|
|
2614
2713
|
settled = true;
|
|
2714
|
+
clearTimeout(timer);
|
|
2615
2715
|
child.on("error", (err) => logger.warn(`codex proxy error: ${err.message}`));
|
|
2616
2716
|
ws.on("error", (err) => logger.warn(`codex daemon socket error: ${err.message}`));
|
|
2617
2717
|
resolve({ ws, close });
|
|
@@ -3025,7 +3125,7 @@ function resolveAdapterHub(config2, env = process.env) {
|
|
|
3025
3125
|
// src/codex/start-check.ts
|
|
3026
3126
|
var ADAPTER_REPORT_TIMEOUT_MS = 1e4;
|
|
3027
3127
|
var HUB_CHECK_TIMEOUT_MS = 3e3;
|
|
3028
|
-
var RESTART_HINT = "After fixing this, restart the adapter: claude-alarm codex stop, then claude-alarm codex start";
|
|
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)";
|
|
3029
3129
|
var CONFIG_PATH = "~/.claude-alarm/config.json";
|
|
3030
3130
|
var NOT_CHECKED = { text: "Codex daemon: not checked (the adapter was already running)", warning: false };
|
|
3031
3131
|
function toOutcome(msg) {
|
|
@@ -3042,9 +3142,11 @@ function waitForAdapterReport(child, timeoutMs = ADAPTER_REPORT_TIMEOUT_MS) {
|
|
|
3042
3142
|
resolve(result);
|
|
3043
3143
|
};
|
|
3044
3144
|
const onMessage = (msg) => {
|
|
3045
|
-
if (msg
|
|
3046
|
-
|
|
3047
|
-
}
|
|
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 ?? "") });
|
|
3048
3150
|
};
|
|
3049
3151
|
const onExit = (code, signal) => finish({ kind: "exited", code, signal });
|
|
3050
3152
|
const onError = (error) => finish({ kind: "exited", code: null, signal: null, error });
|
|
@@ -3124,9 +3226,9 @@ function printAlreadyRunning(d, pid, hub) {
|
|
|
3124
3226
|
printChecks(d, NOT_CHECKED, hubLine(hub, d.hub));
|
|
3125
3227
|
}
|
|
3126
3228
|
async function startAdapter(d) {
|
|
3127
|
-
const existing = d.
|
|
3128
|
-
if (existing
|
|
3129
|
-
printAlreadyRunning(d, existing, await checkHub(d.hub, d.hubTimeoutMs));
|
|
3229
|
+
const existing = await d.queryOwner();
|
|
3230
|
+
if (existing.state === "running") {
|
|
3231
|
+
printAlreadyRunning(d, existing.pid, await checkHub(d.hub, d.hubTimeoutMs));
|
|
3130
3232
|
return 0;
|
|
3131
3233
|
}
|
|
3132
3234
|
const child = d.spawnAdapter();
|
|
@@ -3143,6 +3245,14 @@ async function startAdapter(d) {
|
|
|
3143
3245
|
]);
|
|
3144
3246
|
if (child.connected) child.disconnect();
|
|
3145
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
|
+
}
|
|
3146
3256
|
if (report.kind !== "exited" && !seen.exit && child.pid !== void 0) {
|
|
3147
3257
|
d.out(`Codex adapter started (PID: ${child.pid}). Logs: ${d.logFile}`);
|
|
3148
3258
|
printChecks(d, daemonLine(report, d.command, d.logFile), hubLine(hub, d.hub));
|
|
@@ -3153,10 +3263,9 @@ async function startAdapter(d) {
|
|
|
3153
3263
|
d.err(`Codex adapter failed to start: ${exit?.error?.message ?? "no process id"}`);
|
|
3154
3264
|
return 1;
|
|
3155
3265
|
}
|
|
3156
|
-
const owner = d.
|
|
3157
|
-
if (owner
|
|
3158
|
-
|
|
3159
|
-
printAlreadyRunning(d, owner, hub);
|
|
3266
|
+
const owner = await d.queryOwner();
|
|
3267
|
+
if (owner.state === "running" && owner.pid !== child.pid) {
|
|
3268
|
+
printAlreadyRunning(d, owner.pid, hub);
|
|
3160
3269
|
return 0;
|
|
3161
3270
|
}
|
|
3162
3271
|
const how = exit.code !== null ? `code ${exit.code}` : `signal ${exit.signal}`;
|
|
@@ -3164,8 +3273,147 @@ async function startAdapter(d) {
|
|
|
3164
3273
|
return 1;
|
|
3165
3274
|
}
|
|
3166
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
|
+
|
|
3167
3415
|
// src/cli.ts
|
|
3168
|
-
var __dirname2 =
|
|
3416
|
+
var __dirname2 = path11.dirname(fileURLToPath2(import.meta.url));
|
|
3169
3417
|
function authHeaders(token) {
|
|
3170
3418
|
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
3171
3419
|
}
|
|
@@ -3201,8 +3449,8 @@ Quick start:
|
|
|
3201
3449
|
}
|
|
3202
3450
|
async function checkForUpdates() {
|
|
3203
3451
|
try {
|
|
3204
|
-
const pkgPath =
|
|
3205
|
-
const pkg = JSON.parse(
|
|
3452
|
+
const pkgPath = path11.join(__dirname2, "..", "package.json");
|
|
3453
|
+
const pkg = JSON.parse(fs9.readFileSync(pkgPath, "utf-8"));
|
|
3206
3454
|
const currentVersion = pkg.version;
|
|
3207
3455
|
const res = await fetch("https://registry.npmjs.org/@delt/claude-alarm/latest", {
|
|
3208
3456
|
signal: AbortSignal.timeout(3e3)
|
|
@@ -3225,18 +3473,18 @@ async function hubStart(daemon) {
|
|
|
3225
3473
|
const port = config2.hub.port ?? DEFAULT_HUB_PORT;
|
|
3226
3474
|
const displayHost = hubUrlHost(host);
|
|
3227
3475
|
checkForUpdates();
|
|
3228
|
-
if (
|
|
3229
|
-
const pid = parseInt(
|
|
3476
|
+
if (fs9.existsSync(PID_FILE)) {
|
|
3477
|
+
const pid = parseInt(fs9.readFileSync(PID_FILE, "utf-8").trim(), 10);
|
|
3230
3478
|
if (isProcessRunning(pid)) {
|
|
3231
3479
|
console.log(`Hub is already running (PID: ${pid}). Dashboard: ${loginLink(displayHost, port, config2.hub.token)}`);
|
|
3232
3480
|
return;
|
|
3233
3481
|
}
|
|
3234
|
-
|
|
3482
|
+
fs9.unlinkSync(PID_FILE);
|
|
3235
3483
|
}
|
|
3236
3484
|
if (daemon) {
|
|
3237
3485
|
ensureConfigDir();
|
|
3238
|
-
const logFd =
|
|
3239
|
-
const hubScript =
|
|
3486
|
+
const logFd = fs9.openSync(LOG_FILE, "a");
|
|
3487
|
+
const hubScript = path11.join(__dirname2, "hub", "server.js");
|
|
3240
3488
|
const child = spawn3(process.execPath, [hubScript], {
|
|
3241
3489
|
detached: true,
|
|
3242
3490
|
stdio: ["ignore", logFd, logFd],
|
|
@@ -3247,7 +3495,7 @@ async function hubStart(daemon) {
|
|
|
3247
3495
|
console.error("Failed to start hub daemon");
|
|
3248
3496
|
process.exit(1);
|
|
3249
3497
|
}
|
|
3250
|
-
|
|
3498
|
+
fs9.writeFileSync(PID_FILE, String(pid), "utf-8");
|
|
3251
3499
|
let exited = false;
|
|
3252
3500
|
child.once("exit", () => {
|
|
3253
3501
|
exited = true;
|
|
@@ -3261,7 +3509,7 @@ async function hubStart(daemon) {
|
|
|
3261
3509
|
});
|
|
3262
3510
|
if (startup !== "ready") {
|
|
3263
3511
|
if (!exited) child.kill();
|
|
3264
|
-
if (
|
|
3512
|
+
if (fs9.existsSync(PID_FILE)) fs9.unlinkSync(PID_FILE);
|
|
3265
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}`);
|
|
3266
3514
|
process.exit(1);
|
|
3267
3515
|
}
|
|
@@ -3280,11 +3528,11 @@ async function hubStart(daemon) {
|
|
|
3280
3528
|
const hub = new HubServer2(config2);
|
|
3281
3529
|
await hub.start();
|
|
3282
3530
|
ensureConfigDir();
|
|
3283
|
-
|
|
3531
|
+
fs9.writeFileSync(PID_FILE, String(process.pid), "utf-8");
|
|
3284
3532
|
const shutdown = async () => {
|
|
3285
3533
|
console.log("\nShutting down...");
|
|
3286
3534
|
await hub.stop();
|
|
3287
|
-
if (
|
|
3535
|
+
if (fs9.existsSync(PID_FILE)) fs9.unlinkSync(PID_FILE);
|
|
3288
3536
|
process.exit(0);
|
|
3289
3537
|
};
|
|
3290
3538
|
process.on("SIGINT", shutdown);
|
|
@@ -3292,18 +3540,18 @@ async function hubStart(daemon) {
|
|
|
3292
3540
|
}
|
|
3293
3541
|
}
|
|
3294
3542
|
function hubStop() {
|
|
3295
|
-
if (!
|
|
3543
|
+
if (!fs9.existsSync(PID_FILE)) {
|
|
3296
3544
|
console.log("Hub is not running (no PID file found)");
|
|
3297
3545
|
return;
|
|
3298
3546
|
}
|
|
3299
|
-
const pid = parseInt(
|
|
3547
|
+
const pid = parseInt(fs9.readFileSync(PID_FILE, "utf-8").trim(), 10);
|
|
3300
3548
|
try {
|
|
3301
3549
|
process.kill(pid, "SIGTERM");
|
|
3302
3550
|
console.log(`Hub stopped (PID: ${pid})`);
|
|
3303
3551
|
} catch {
|
|
3304
3552
|
console.log("Hub process not found (may have already stopped)");
|
|
3305
3553
|
}
|
|
3306
|
-
|
|
3554
|
+
fs9.unlinkSync(PID_FILE);
|
|
3307
3555
|
}
|
|
3308
3556
|
async function hubStatus() {
|
|
3309
3557
|
const config2 = loadConfig();
|
|
@@ -3311,8 +3559,8 @@ async function hubStatus() {
|
|
|
3311
3559
|
const port = config2.hub.port ?? DEFAULT_HUB_PORT;
|
|
3312
3560
|
const displayHost = hubUrlHost(host);
|
|
3313
3561
|
let pidInfo = "not running";
|
|
3314
|
-
if (
|
|
3315
|
-
const pid = parseInt(
|
|
3562
|
+
if (fs9.existsSync(PID_FILE)) {
|
|
3563
|
+
const pid = parseInt(fs9.readFileSync(PID_FILE, "utf-8").trim(), 10);
|
|
3316
3564
|
if (isProcessRunning(pid)) {
|
|
3317
3565
|
pidInfo = `running (PID: ${pid})`;
|
|
3318
3566
|
} else {
|
|
@@ -3373,7 +3621,7 @@ async function test() {
|
|
|
3373
3621
|
console.error("Could not reach hub. Start it first: claude-alarm hub start");
|
|
3374
3622
|
}
|
|
3375
3623
|
}
|
|
3376
|
-
function
|
|
3624
|
+
function ask2(question) {
|
|
3377
3625
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
3378
3626
|
return new Promise((resolve) => {
|
|
3379
3627
|
rl.question(question, (answer) => {
|
|
@@ -3384,18 +3632,18 @@ function ask(question) {
|
|
|
3384
3632
|
}
|
|
3385
3633
|
async function init() {
|
|
3386
3634
|
const dir = process.cwd();
|
|
3387
|
-
const projectName =
|
|
3635
|
+
const projectName = path11.basename(dir);
|
|
3388
3636
|
console.log(`
|
|
3389
3637
|
claude-alarm init for "${projectName}"
|
|
3390
3638
|
`);
|
|
3391
|
-
const remote = await
|
|
3639
|
+
const remote = await ask2("Connect to a remote hub? (y/N): ");
|
|
3392
3640
|
let env = {
|
|
3393
3641
|
CLAUDE_ALARM_SESSION_NAME: projectName
|
|
3394
3642
|
};
|
|
3395
3643
|
if (remote.toLowerCase() === "y") {
|
|
3396
|
-
const host = await
|
|
3397
|
-
const port = await
|
|
3398
|
-
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: ");
|
|
3399
3647
|
if (!host) {
|
|
3400
3648
|
console.error("Host is required.");
|
|
3401
3649
|
process.exit(1);
|
|
@@ -3404,11 +3652,11 @@ claude-alarm init for "${projectName}"
|
|
|
3404
3652
|
if (port) env.CLAUDE_ALARM_HUB_PORT = port;
|
|
3405
3653
|
if (token) env.CLAUDE_ALARM_HUB_TOKEN = token;
|
|
3406
3654
|
}
|
|
3407
|
-
const mcpPath =
|
|
3655
|
+
const mcpPath = path11.join(dir, ".mcp.json");
|
|
3408
3656
|
let mcpConfig = {};
|
|
3409
|
-
if (
|
|
3657
|
+
if (fs9.existsSync(mcpPath)) {
|
|
3410
3658
|
try {
|
|
3411
|
-
mcpConfig = JSON.parse(
|
|
3659
|
+
mcpConfig = JSON.parse(fs9.readFileSync(mcpPath, "utf-8"));
|
|
3412
3660
|
} catch {
|
|
3413
3661
|
mcpConfig = {};
|
|
3414
3662
|
}
|
|
@@ -3419,7 +3667,7 @@ claude-alarm init for "${projectName}"
|
|
|
3419
3667
|
args: ["-y", "@delt/claude-alarm", "serve"],
|
|
3420
3668
|
env
|
|
3421
3669
|
};
|
|
3422
|
-
|
|
3670
|
+
fs9.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2), "utf-8");
|
|
3423
3671
|
console.log(`
|
|
3424
3672
|
\u2713 Created ${mcpPath}`);
|
|
3425
3673
|
if (remote.toLowerCase() !== "y") {
|
|
@@ -3441,7 +3689,7 @@ claude-alarm init for "${projectName}"
|
|
|
3441
3689
|
}
|
|
3442
3690
|
console.log(` Dashboard: http://${displayHost}:${port}`);
|
|
3443
3691
|
if (shouldOfferCodex(config2, findCodex("codex") !== void 0)) {
|
|
3444
|
-
const answer = await
|
|
3692
|
+
const answer = await ask2("\nCodex is installed. Show Codex conversations on the dashboard too? (y/N): ");
|
|
3445
3693
|
if (answer.toLowerCase() === "y") codexEnable(true);
|
|
3446
3694
|
else setCodexEnabled(false);
|
|
3447
3695
|
}
|
|
@@ -3470,8 +3718,8 @@ function isProcessRunning(pid) {
|
|
|
3470
3718
|
}
|
|
3471
3719
|
}
|
|
3472
3720
|
function readCodexPid() {
|
|
3473
|
-
if (!
|
|
3474
|
-
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);
|
|
3475
3723
|
return Number.isNaN(pid) ? void 0 : pid;
|
|
3476
3724
|
}
|
|
3477
3725
|
function codexEnable(enabled) {
|
|
@@ -3481,21 +3729,15 @@ function codexEnable(enabled) {
|
|
|
3481
3729
|
async function codexStart() {
|
|
3482
3730
|
const config2 = loadConfig();
|
|
3483
3731
|
ensureConfigDir();
|
|
3732
|
+
legacyNote(controlDeps());
|
|
3484
3733
|
const code = await startAdapter({
|
|
3485
3734
|
hub: resolveAdapterHub(config2),
|
|
3486
3735
|
command: config2.codex?.command ?? "codex",
|
|
3487
3736
|
logFile: CODEX_LOG_FILE,
|
|
3488
|
-
|
|
3489
|
-
isRunning: isProcessRunning,
|
|
3490
|
-
removePidFile: () => {
|
|
3491
|
-
try {
|
|
3492
|
-
fs8.unlinkSync(CODEX_PID_FILE);
|
|
3493
|
-
} catch {
|
|
3494
|
-
}
|
|
3495
|
-
},
|
|
3737
|
+
queryOwner: () => queryOwner(controlEndpoint(CONFIG_DIR)),
|
|
3496
3738
|
spawnAdapter: () => {
|
|
3497
|
-
const logFd =
|
|
3498
|
-
return spawn3(process.execPath, [
|
|
3739
|
+
const logFd = fs9.openSync(CODEX_LOG_FILE, "a");
|
|
3740
|
+
return spawn3(process.execPath, [path11.join(__dirname2, "codex", "main.js")], {
|
|
3499
3741
|
detached: true,
|
|
3500
3742
|
stdio: ["ignore", logFd, logFd, "ipc"],
|
|
3501
3743
|
windowsHide: true,
|
|
@@ -3507,28 +3749,25 @@ async function codexStart() {
|
|
|
3507
3749
|
});
|
|
3508
3750
|
if (code !== 0) process.exit(code);
|
|
3509
3751
|
}
|
|
3510
|
-
function
|
|
3511
|
-
const
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
const state = pid === void 0 ? "not running" : isProcessRunning(pid) ? `running (PID: ${pid})` : "not running (stale PID file)";
|
|
3530
|
-
console.log(`Codex adapter: ${state}`);
|
|
3531
|
-
console.log(`Start with hub: ${loadConfig().codex?.enabled ? "enabled" : "disabled"}`);
|
|
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
|
+
};
|
|
3532
3771
|
}
|
|
3533
3772
|
async function codexRun(argv) {
|
|
3534
3773
|
const abort = new AbortController();
|
|
@@ -3537,7 +3776,7 @@ async function codexRun(argv) {
|
|
|
3537
3776
|
process.on("SIGTERM", stop);
|
|
3538
3777
|
try {
|
|
3539
3778
|
return await runFromCli(argv, {
|
|
3540
|
-
readFile: (file) =>
|
|
3779
|
+
readFile: (file) => fs9.promises.readFile(file, "utf-8"),
|
|
3541
3780
|
readStdin: async () => {
|
|
3542
3781
|
let text = "";
|
|
3543
3782
|
for await (const chunk of process.stdin) text += chunk;
|
|
@@ -3588,8 +3827,8 @@ async function main2() {
|
|
|
3588
3827
|
if (sub === "enable") codexEnable(true);
|
|
3589
3828
|
else if (sub === "disable") codexEnable(false);
|
|
3590
3829
|
else if (sub === "start") await codexStart();
|
|
3591
|
-
else if (sub === "stop")
|
|
3592
|
-
else if (sub === "status")
|
|
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);
|
|
3593
3832
|
else if (sub === "run") process.exitCode = await codexRun(args.slice(2));
|
|
3594
3833
|
else {
|
|
3595
3834
|
console.error(`Unknown codex command: ${sub}`);
|