@delt/claude-alarm 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/cli.js +574 -206
- package/dist/cli.js.map +1 -1
- package/dist/codex/main.js +459 -93
- package/dist/codex/main.js.map +1 -1
- package/dist/dashboard/index.html +86 -41
- package/dist/hub/server.js +189 -69
- package/dist/hub/server.js.map +1 -1
- package/dist/index.d.ts +19 -3
- package/dist/index.js +189 -69
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/dashboard/index.html +86 -41
package/dist/cli.js
CHANGED
|
@@ -301,7 +301,7 @@ var init_session_manager = __esm({
|
|
|
301
301
|
|
|
302
302
|
// src/hub/notifier.ts
|
|
303
303
|
import notifier from "node-notifier";
|
|
304
|
-
import { execFile } from "child_process";
|
|
304
|
+
import { execFile as execFile2 } from "child_process";
|
|
305
305
|
var Notifier;
|
|
306
306
|
var init_notifier = __esm({
|
|
307
307
|
"src/hub/notifier.ts"() {
|
|
@@ -362,7 +362,7 @@ var init_notifier = __esm({
|
|
|
362
362
|
}
|
|
363
363
|
checkWindowsNotifications() {
|
|
364
364
|
return new Promise((resolve) => {
|
|
365
|
-
|
|
365
|
+
execFile2(
|
|
366
366
|
"powershell",
|
|
367
367
|
["-Command", '(Get-ItemProperty -Path "HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\PushNotifications" -Name ToastEnabled -ErrorAction SilentlyContinue).ToastEnabled'],
|
|
368
368
|
{ windowsHide: true },
|
|
@@ -383,7 +383,7 @@ var init_notifier = __esm({
|
|
|
383
383
|
logger.warn("Windows notifications are disabled. Opening notification settings...");
|
|
384
384
|
logger.warn("Please enable notifications for this app, then try again.");
|
|
385
385
|
if (process.platform === "win32") {
|
|
386
|
-
|
|
386
|
+
execFile2("powershell", ["-Command", "Start-Process ms-settings:notifications"], { windowsHide: true });
|
|
387
387
|
}
|
|
388
388
|
setTimeout(() => {
|
|
389
389
|
this.notificationSettingsOpened = false;
|
|
@@ -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, NO_LONGER_CONNECTED, TelegramBot;
|
|
446
449
|
var init_telegram = __esm({
|
|
447
450
|
"src/hub/telegram.ts"() {
|
|
448
451
|
"use strict";
|
|
@@ -452,6 +455,11 @@ 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;
|
|
462
|
+
NO_LONGER_CONNECTED = "the session is no longer connected";
|
|
455
463
|
TelegramBot = class _TelegramBot {
|
|
456
464
|
config;
|
|
457
465
|
offset = 0;
|
|
@@ -472,9 +480,9 @@ var init_telegram = __esm({
|
|
|
472
480
|
choiceMessages = /* @__PURE__ */ new Map();
|
|
473
481
|
// Callback: get current sessions list
|
|
474
482
|
getSessions;
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
483
|
+
latestSelection;
|
|
484
|
+
// Keyed by prompt, not by chat: buttons from an older prompt must still send that prompt's message.
|
|
485
|
+
selections = /* @__PURE__ */ new Map();
|
|
478
486
|
constructor(config2) {
|
|
479
487
|
this.config = config2;
|
|
480
488
|
}
|
|
@@ -483,8 +491,7 @@ var init_telegram = __esm({
|
|
|
483
491
|
}
|
|
484
492
|
/** Send a notification message to Telegram */
|
|
485
493
|
async sendNotification(sessionId2, _sessionLabel, title, message) {
|
|
486
|
-
const text =
|
|
487
|
-
${this.mdToHtml(message)}`;
|
|
494
|
+
const text = this.fitNotification(title, message);
|
|
488
495
|
const result = await this.sendMessage(text);
|
|
489
496
|
if (result?.message_id) {
|
|
490
497
|
this.messageSessionMap.set(result.message_id, sessionId2);
|
|
@@ -496,6 +503,25 @@ ${this.mdToHtml(message)}`;
|
|
|
496
503
|
}
|
|
497
504
|
}
|
|
498
505
|
}
|
|
506
|
+
fitNotification(title, message) {
|
|
507
|
+
const render = (body) => `<b>${this.escHtml(title)}</b>
|
|
508
|
+
${this.mdToHtml(body)}`;
|
|
509
|
+
const full = render(message);
|
|
510
|
+
if (visibleLength(full) <= MAX_VISIBLE_CHARS) return full;
|
|
511
|
+
const cut = (n) => {
|
|
512
|
+
const end = n > 0 && /[\uD800-\uDBFF]/.test(message[n - 1]) ? n - 1 : n;
|
|
513
|
+
return render(`${message.slice(0, end)}
|
|
514
|
+
${TRUNCATED}`);
|
|
515
|
+
};
|
|
516
|
+
let lo = 0;
|
|
517
|
+
let hi = message.length;
|
|
518
|
+
while (lo < hi) {
|
|
519
|
+
const mid = Math.ceil((lo + hi) / 2);
|
|
520
|
+
if (visibleLength(cut(mid)) <= MAX_VISIBLE_CHARS) lo = mid;
|
|
521
|
+
else hi = mid - 1;
|
|
522
|
+
}
|
|
523
|
+
return cut(lo);
|
|
524
|
+
}
|
|
499
525
|
/** Send a text message to the configured chat */
|
|
500
526
|
async sendMessage(text, replyToMessageId, replyMarkup) {
|
|
501
527
|
try {
|
|
@@ -577,6 +603,13 @@ ${this.mdToHtml(message)}`;
|
|
|
577
603
|
const hasPhoto = msg.photo && msg.photo.length > 0;
|
|
578
604
|
const text = (msg.text || msg.caption || "").trim();
|
|
579
605
|
if (!text && !hasPhoto) return;
|
|
606
|
+
if (hasPhoto) {
|
|
607
|
+
const size = msg.photo[msg.photo.length - 1].file_size;
|
|
608
|
+
if (size !== void 0 && size > MAX_PHOTO_BYTES) {
|
|
609
|
+
this.photoNotDelivered("it is larger than 10 MB");
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
580
613
|
if (msg.reply_to_message) {
|
|
581
614
|
const sessionId2 = this.messageSessionMap.get(msg.reply_to_message.message_id);
|
|
582
615
|
if (sessionId2) {
|
|
@@ -590,18 +623,14 @@ ${this.mdToHtml(message)}`;
|
|
|
590
623
|
}
|
|
591
624
|
if (text) {
|
|
592
625
|
const selectMatch = text.match(/^\/s_(\d+)$/);
|
|
593
|
-
|
|
594
|
-
|
|
626
|
+
const latest = this.latestSelection;
|
|
627
|
+
if (selectMatch && latest !== void 0) {
|
|
628
|
+
const pending = this.selections.get(latest);
|
|
595
629
|
if (pending) {
|
|
596
|
-
this.
|
|
630
|
+
this.selections.delete(latest);
|
|
597
631
|
const session = this.pendingSession(pending, parseInt(selectMatch[1], 10) - 1);
|
|
598
632
|
if (session) {
|
|
599
|
-
if (pending
|
|
600
|
-
await this.deliverPhotoToSessionByFileId(session.id, pending.photoFileId, pending.caption);
|
|
601
|
-
} else if (pending.text) {
|
|
602
|
-
this.deliverToSession(session.id, pending.text);
|
|
603
|
-
}
|
|
604
|
-
this.sendMessage(`Sent to [${this.getLabel(session)}]`);
|
|
633
|
+
if (await this.deliverPending(session.id, pending)) this.sendMessage(`Sent to [${this.getLabel(session)}]`);
|
|
605
634
|
} else {
|
|
606
635
|
this.sendMessage("Invalid session number.");
|
|
607
636
|
}
|
|
@@ -622,16 +651,19 @@ ${this.mdToHtml(message)}`;
|
|
|
622
651
|
}
|
|
623
652
|
return;
|
|
624
653
|
}
|
|
654
|
+
const selectionId = randomUUID2().replace(/-/g, "").slice(0, 8);
|
|
655
|
+
this.latestSelection = selectionId;
|
|
625
656
|
const sessionIds = sessions.map((s) => s.id);
|
|
626
657
|
if (hasPhoto) {
|
|
627
658
|
const largest = msg.photo[msg.photo.length - 1];
|
|
628
|
-
this.
|
|
659
|
+
this.selections.set(selectionId, { photoFileId: largest.file_id, caption: text, sessionIds });
|
|
629
660
|
} else {
|
|
630
|
-
this.
|
|
661
|
+
this.selections.set(selectionId, { text, sessionIds });
|
|
631
662
|
}
|
|
663
|
+
while (this.selections.size > MAX_SELECTIONS) this.selections.delete(this.selections.keys().next().value);
|
|
632
664
|
const buttons = sessions.map((s, i) => ({
|
|
633
665
|
text: this.getLabel(s),
|
|
634
|
-
callback_data: `
|
|
666
|
+
callback_data: `sel:${selectionId}:${i}`
|
|
635
667
|
}));
|
|
636
668
|
const rows = [];
|
|
637
669
|
for (let i = 0; i < buttons.length; i += 2) {
|
|
@@ -640,50 +672,75 @@ ${this.mdToHtml(message)}`;
|
|
|
640
672
|
this.sendMessage("Multiple sessions active. Select one:", void 0, { inline_keyboard: rows });
|
|
641
673
|
}
|
|
642
674
|
deliverToSession(sessionId2, content) {
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
675
|
+
const delivered = this.onMessageToSession?.(sessionId2, content) === true;
|
|
676
|
+
if (!delivered) void this.sendMessage(`Not delivered: ${NO_LONGER_CONNECTED}`);
|
|
677
|
+
return delivered;
|
|
678
|
+
}
|
|
679
|
+
async deliverPending(sessionId2, pending) {
|
|
680
|
+
if (pending.photoFileId) return this.deliverPhotoToSessionByFileId(sessionId2, pending.photoFileId, pending.caption);
|
|
681
|
+
return pending.text ? this.deliverToSession(sessionId2, pending.text) : false;
|
|
646
682
|
}
|
|
647
683
|
async deliverPhotoToSession(sessionId2, photos, caption) {
|
|
648
684
|
const largest = photos[photos.length - 1];
|
|
649
|
-
|
|
685
|
+
return this.deliverPhotoToSessionByFileId(sessionId2, largest.file_id, caption);
|
|
650
686
|
}
|
|
651
687
|
async deliverPhotoToSessionByFileId(sessionId2, fileId, caption) {
|
|
652
688
|
try {
|
|
653
689
|
const fileRes = await fetch(`${this.apiUrl}/getFile?file_id=${fileId}`);
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
return;
|
|
690
|
+
const fileData = fileRes.ok ? await fileRes.json() : void 0;
|
|
691
|
+
if (!fileData?.ok || !fileData.result) {
|
|
692
|
+
this.photoNotDelivered("Telegram did not return the file");
|
|
693
|
+
return false;
|
|
657
694
|
}
|
|
658
|
-
const fileData = await fileRes.json();
|
|
659
|
-
if (!fileData.ok) return;
|
|
660
695
|
const downloadUrl = `https://api.telegram.org/file/bot${this.config.botToken}/${fileData.result.file_path}`;
|
|
661
696
|
const imgRes = await fetch(downloadUrl);
|
|
662
697
|
if (!imgRes.ok) {
|
|
663
|
-
|
|
664
|
-
return;
|
|
698
|
+
this.photoNotDelivered("the download failed");
|
|
699
|
+
return false;
|
|
665
700
|
}
|
|
666
701
|
const buffer = Buffer.from(await imgRes.arrayBuffer());
|
|
702
|
+
if (buffer.length > MAX_PHOTO_BYTES) {
|
|
703
|
+
this.photoNotDelivered("it is larger than 10 MB");
|
|
704
|
+
return false;
|
|
705
|
+
}
|
|
667
706
|
const ext = fileData.result.file_path.split(".").pop() || "jpg";
|
|
668
707
|
const mimeType = ext === "png" ? "image/png" : ext === "gif" ? "image/gif" : ext === "webp" ? "image/webp" : "image/jpeg";
|
|
669
|
-
|
|
708
|
+
fs5.mkdirSync(UPLOADS_DIR, { recursive: true });
|
|
670
709
|
const filename = `${randomUUID2()}.${ext}`;
|
|
671
|
-
const filePath =
|
|
672
|
-
|
|
710
|
+
const filePath = path6.join(UPLOADS_DIR, filename);
|
|
711
|
+
fs5.writeFileSync(filePath, buffer);
|
|
673
712
|
logger.info(`Telegram photo saved: ${filename} (${buffer.length} bytes)`);
|
|
674
|
-
|
|
675
|
-
|
|
713
|
+
let accepted = false;
|
|
714
|
+
try {
|
|
715
|
+
accepted = this.onImageToSession?.(sessionId2, filePath, mimeType, caption) === true;
|
|
716
|
+
} catch (err) {
|
|
717
|
+
logger.warn(`Telegram photo hand-off failed: ${err.message}`);
|
|
718
|
+
}
|
|
719
|
+
if (!accepted) {
|
|
720
|
+
try {
|
|
721
|
+
fs5.unlinkSync(filePath);
|
|
722
|
+
} catch {
|
|
723
|
+
}
|
|
724
|
+
this.photoNotDelivered(NO_LONGER_CONNECTED);
|
|
725
|
+
return false;
|
|
676
726
|
}
|
|
677
727
|
setTimeout(() => {
|
|
678
728
|
try {
|
|
679
|
-
|
|
729
|
+
fs5.unlinkSync(filePath);
|
|
680
730
|
} catch {
|
|
681
731
|
}
|
|
682
|
-
}, 5 * 60 * 1e3);
|
|
732
|
+
}, 5 * 60 * 1e3).unref();
|
|
733
|
+
return true;
|
|
683
734
|
} catch (err) {
|
|
684
735
|
logger.warn(`Telegram photo download failed: ${err.message}`);
|
|
736
|
+
this.photoNotDelivered("the download failed");
|
|
737
|
+
return false;
|
|
685
738
|
}
|
|
686
739
|
}
|
|
740
|
+
photoNotDelivered(reason) {
|
|
741
|
+
logger.warn(`Telegram photo not delivered: ${reason}`);
|
|
742
|
+
void this.sendMessage(`Photo not delivered: ${reason}`);
|
|
743
|
+
}
|
|
687
744
|
// Buttons are numbered against the list shown when they were sent; Codex sessions come and go, so the live list may have shifted.
|
|
688
745
|
pendingSession(pending, idx) {
|
|
689
746
|
const id = pending.sessionIds[idx];
|
|
@@ -797,7 +854,7 @@ ${this.escHtml(description)}` : "") + (slice ? `
|
|
|
797
854
|
async handleChoiceCallback(query) {
|
|
798
855
|
const choice = this.choiceTokens.get(query.data.slice("pc:".length));
|
|
799
856
|
if (!choice) {
|
|
800
|
-
await this.
|
|
857
|
+
await this.expire(query);
|
|
801
858
|
return;
|
|
802
859
|
}
|
|
803
860
|
this.dropChoiceTokens(choice.sessionId, choice.requestId);
|
|
@@ -826,10 +883,14 @@ ${this.escHtml(description)}` : "") + (slice ? `
|
|
|
826
883
|
async handleCallbackQuery(query) {
|
|
827
884
|
if (!query.data) return;
|
|
828
885
|
if (String(query.message?.chat.id) !== String(this.config.chatId)) return;
|
|
829
|
-
if (query.data.startsWith("
|
|
886
|
+
if (query.data.startsWith("sel:")) {
|
|
830
887
|
await this.handleSessionSelectCallback(query);
|
|
831
888
|
return;
|
|
832
889
|
}
|
|
890
|
+
if (query.data.startsWith("sess:")) {
|
|
891
|
+
await this.expire(query);
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
833
894
|
if (query.data.startsWith("pc:")) {
|
|
834
895
|
await this.handleChoiceCallback(query);
|
|
835
896
|
return;
|
|
@@ -852,21 +913,22 @@ ${label}`);
|
|
|
852
913
|
}
|
|
853
914
|
}
|
|
854
915
|
async handleSessionSelectCallback(query) {
|
|
855
|
-
const
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
916
|
+
const [, selectionId, idxStr] = query.data.split(":");
|
|
917
|
+
const pending = this.selections.get(selectionId);
|
|
918
|
+
if (!pending) {
|
|
919
|
+
await this.expire(query);
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
const session = this.pendingSession(pending, parseInt(idxStr, 10));
|
|
923
|
+
if (!session) {
|
|
862
924
|
await this.answerCallbackQuery(query.id, "Session not found");
|
|
863
925
|
return;
|
|
864
926
|
}
|
|
865
|
-
this.
|
|
866
|
-
if (pending
|
|
867
|
-
await this.
|
|
868
|
-
|
|
869
|
-
|
|
927
|
+
this.selections.delete(selectionId);
|
|
928
|
+
if (!await this.deliverPending(session.id, pending)) {
|
|
929
|
+
await this.answerCallbackQuery(query.id, pending.photoFileId ? "Photo not delivered" : "Not delivered");
|
|
930
|
+
if (query.message) await this.removeButtons(query.message.chat.id, query.message.message_id);
|
|
931
|
+
return;
|
|
870
932
|
}
|
|
871
933
|
await this.answerCallbackQuery(query.id, `Sent to ${this.getLabel(session)}`);
|
|
872
934
|
if (query.message) {
|
|
@@ -884,6 +946,21 @@ ${label}`);
|
|
|
884
946
|
logger.warn(`Telegram answerCallbackQuery error: ${err.message}`);
|
|
885
947
|
}
|
|
886
948
|
}
|
|
949
|
+
async expire(query) {
|
|
950
|
+
await this.answerCallbackQuery(query.id, "Expired");
|
|
951
|
+
if (query.message) await this.removeButtons(query.message.chat.id, query.message.message_id);
|
|
952
|
+
}
|
|
953
|
+
async removeButtons(chatId, messageId) {
|
|
954
|
+
try {
|
|
955
|
+
await fetch(`${this.apiUrl}/editMessageReplyMarkup`, {
|
|
956
|
+
method: "POST",
|
|
957
|
+
headers: { "Content-Type": "application/json" },
|
|
958
|
+
body: JSON.stringify({ chat_id: chatId, message_id: messageId, reply_markup: { inline_keyboard: [] } })
|
|
959
|
+
});
|
|
960
|
+
} catch (err) {
|
|
961
|
+
logger.warn(`Telegram editMessageReplyMarkup error: ${err.message}`);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
887
964
|
async editMessageText(chatId, messageId, text) {
|
|
888
965
|
try {
|
|
889
966
|
await fetch(`${this.apiUrl}/editMessageText`, {
|
|
@@ -928,8 +1005,8 @@ ${label}`);
|
|
|
928
1005
|
css: "css",
|
|
929
1006
|
scss: "scss"
|
|
930
1007
|
};
|
|
931
|
-
langOf(
|
|
932
|
-
const m = /\.([a-z0-9]+)$/i.exec(
|
|
1008
|
+
langOf(path12) {
|
|
1009
|
+
const m = /\.([a-z0-9]+)$/i.exec(path12 || "");
|
|
933
1010
|
return m ? _TelegramBot.LANG_BY_EXT[m[1].toLowerCase()] ?? "" : "";
|
|
934
1011
|
}
|
|
935
1012
|
/** Convert markdown to Telegram HTML (escape first, then apply formatting) */
|
|
@@ -976,10 +1053,15 @@ ${bodyLines.join("\n")}`;
|
|
|
976
1053
|
|
|
977
1054
|
// src/hub/codex-supervisor.ts
|
|
978
1055
|
import { spawn as spawn2 } from "child_process";
|
|
979
|
-
import
|
|
980
|
-
import
|
|
1056
|
+
import fs6 from "fs";
|
|
1057
|
+
import path7 from "path";
|
|
981
1058
|
function resolveAdapterScript(baseDir) {
|
|
982
|
-
return [
|
|
1059
|
+
return [path7.join(baseDir, "..", "codex", "main.js"), path7.join(baseDir, "codex", "main.js")].find((p) => fs6.existsSync(p));
|
|
1060
|
+
}
|
|
1061
|
+
function adapterEnv(base, hub) {
|
|
1062
|
+
const env = { ...base, CLAUDE_ALARM_HUB_HOST: hub.host, CLAUDE_ALARM_HUB_PORT: String(hub.port) };
|
|
1063
|
+
delete env.CLAUDE_ALARM_HUB_TOKEN;
|
|
1064
|
+
return env;
|
|
983
1065
|
}
|
|
984
1066
|
var HEALTHY_RUN_MS, CodexSupervisor;
|
|
985
1067
|
var init_codex_supervisor = __esm({
|
|
@@ -988,18 +1070,24 @@ var init_codex_supervisor = __esm({
|
|
|
988
1070
|
init_logger();
|
|
989
1071
|
HEALTHY_RUN_MS = 6e4;
|
|
990
1072
|
CodexSupervisor = class {
|
|
991
|
-
constructor(script,
|
|
1073
|
+
constructor(script, opts = {}) {
|
|
992
1074
|
this.script = script;
|
|
993
|
-
this.spawnFn = spawnFn;
|
|
994
|
-
this.minDelayMs = minDelayMs;
|
|
995
|
-
this.maxDelayMs = maxDelayMs;
|
|
996
|
-
this.stopGraceMs = stopGraceMs;
|
|
997
|
-
this.
|
|
1075
|
+
this.spawnFn = opts.spawnFn ?? spawn2;
|
|
1076
|
+
this.minDelayMs = opts.minDelayMs ?? 2e3;
|
|
1077
|
+
this.maxDelayMs = opts.maxDelayMs ?? 6e4;
|
|
1078
|
+
this.stopGraceMs = opts.stopGraceMs ?? 3e3;
|
|
1079
|
+
this.env = opts.env;
|
|
1080
|
+
this.delay = this.minDelayMs;
|
|
998
1081
|
}
|
|
999
1082
|
child;
|
|
1000
1083
|
timer;
|
|
1001
1084
|
stopped = true;
|
|
1002
1085
|
delay;
|
|
1086
|
+
spawnFn;
|
|
1087
|
+
minDelayMs;
|
|
1088
|
+
maxDelayMs;
|
|
1089
|
+
stopGraceMs;
|
|
1090
|
+
env;
|
|
1003
1091
|
start() {
|
|
1004
1092
|
this.stopped = false;
|
|
1005
1093
|
this.launch();
|
|
@@ -1021,7 +1109,8 @@ var init_codex_supervisor = __esm({
|
|
|
1021
1109
|
const child = this.spawnFn(process.execPath, [this.script, "--watch-stdin"], {
|
|
1022
1110
|
detached: true,
|
|
1023
1111
|
stdio: ["pipe", "pipe", "pipe"],
|
|
1024
|
-
windowsHide: true
|
|
1112
|
+
windowsHide: true,
|
|
1113
|
+
...this.env ? { env: this.env } : {}
|
|
1025
1114
|
});
|
|
1026
1115
|
this.child = child;
|
|
1027
1116
|
child.stdout?.on("data", (d) => process.stdout.write(d));
|
|
@@ -1058,15 +1147,15 @@ var init_entry = __esm({
|
|
|
1058
1147
|
});
|
|
1059
1148
|
|
|
1060
1149
|
// src/hub/auth.ts
|
|
1061
|
-
import
|
|
1150
|
+
import crypto2 from "crypto";
|
|
1062
1151
|
function safeEqual(a, b) {
|
|
1063
1152
|
if (typeof a !== "string") return false;
|
|
1064
1153
|
const x = Buffer.from(a);
|
|
1065
1154
|
const y = Buffer.from(b);
|
|
1066
|
-
return x.length === y.length &&
|
|
1155
|
+
return x.length === y.length && crypto2.timingSafeEqual(x, y);
|
|
1067
1156
|
}
|
|
1068
1157
|
function dashboardCookieValue(token) {
|
|
1069
|
-
return
|
|
1158
|
+
return crypto2.createHmac("sha256", token).update("claude-alarm-dashboard").digest("hex");
|
|
1070
1159
|
}
|
|
1071
1160
|
function parseCookies(header) {
|
|
1072
1161
|
const out = {};
|
|
@@ -1126,8 +1215,8 @@ __export(server_exports, {
|
|
|
1126
1215
|
HubServer: () => HubServer
|
|
1127
1216
|
});
|
|
1128
1217
|
import http from "http";
|
|
1129
|
-
import
|
|
1130
|
-
import
|
|
1218
|
+
import fs7 from "fs";
|
|
1219
|
+
import path8 from "path";
|
|
1131
1220
|
import { fileURLToPath } from "url";
|
|
1132
1221
|
import { WebSocketServer, WebSocket as WebSocket2 } from "ws";
|
|
1133
1222
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
@@ -1153,7 +1242,7 @@ var init_server = __esm({
|
|
|
1153
1242
|
init_entry();
|
|
1154
1243
|
init_hub_url();
|
|
1155
1244
|
init_auth();
|
|
1156
|
-
__dirname =
|
|
1245
|
+
__dirname = path8.dirname(fileURLToPath(import.meta.url));
|
|
1157
1246
|
MAX_WS_PAYLOAD = 16 * 1024 * 1024;
|
|
1158
1247
|
nonEmpty = (v) => typeof v === "string" && v.trim() !== "";
|
|
1159
1248
|
HubServer = class {
|
|
@@ -1374,23 +1463,23 @@ var init_server = __esm({
|
|
|
1374
1463
|
return;
|
|
1375
1464
|
}
|
|
1376
1465
|
const candidates = [
|
|
1377
|
-
|
|
1466
|
+
path8.join(__dirname, "..", "dashboard", "index.html"),
|
|
1378
1467
|
// from dist/hub/
|
|
1379
|
-
|
|
1468
|
+
path8.join(__dirname, "dashboard", "index.html"),
|
|
1380
1469
|
// from dist/ (bundled index.js)
|
|
1381
|
-
|
|
1470
|
+
path8.join(__dirname, "..", "..", "src", "dashboard", "index.html"),
|
|
1382
1471
|
// from dist/hub/ -> src/
|
|
1383
|
-
|
|
1472
|
+
path8.join(__dirname, "..", "src", "dashboard", "index.html"),
|
|
1384
1473
|
// from dist/ -> src/
|
|
1385
|
-
|
|
1474
|
+
path8.join(process.cwd(), "dist", "dashboard", "index.html"),
|
|
1386
1475
|
// from cwd
|
|
1387
|
-
|
|
1476
|
+
path8.join(process.cwd(), "src", "dashboard", "index.html")
|
|
1388
1477
|
// from cwd/src
|
|
1389
1478
|
];
|
|
1390
1479
|
logger.debug(`Dashboard candidates: ${JSON.stringify(candidates)}`);
|
|
1391
1480
|
for (const candidate of candidates) {
|
|
1392
|
-
if (
|
|
1393
|
-
const html =
|
|
1481
|
+
if (fs7.existsSync(candidate)) {
|
|
1482
|
+
const html = fs7.readFileSync(candidate, "utf-8");
|
|
1394
1483
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
1395
1484
|
res.end(html);
|
|
1396
1485
|
return;
|
|
@@ -1540,7 +1629,16 @@ var init_server = __esm({
|
|
|
1540
1629
|
this.sessions.updateActivity(msg.sessionId);
|
|
1541
1630
|
const notifySession = this.sessions.get(msg.sessionId);
|
|
1542
1631
|
const notifyLabel = this.getSessionLabel(notifySession);
|
|
1543
|
-
|
|
1632
|
+
const notifyTitle = `[${notifyLabel}] ${msg.title}`;
|
|
1633
|
+
if (msg.to === "api") {
|
|
1634
|
+
logger.info(`${notifyTitle}: ${msg.message}`);
|
|
1635
|
+
break;
|
|
1636
|
+
}
|
|
1637
|
+
if (msg.to === "telegram") {
|
|
1638
|
+
void this.telegramBot?.sendNotification(msg.sessionId, notifyLabel, notifyTitle, msg.message);
|
|
1639
|
+
break;
|
|
1640
|
+
}
|
|
1641
|
+
if (msg.to !== "dashboard") this.notifier.notifyWithSession(msg.sessionId, notifyLabel, notifyTitle, msg.message, msg.level ?? "info");
|
|
1544
1642
|
this.broadcastToDashboards({
|
|
1545
1643
|
type: "notification",
|
|
1546
1644
|
sessionId: msg.sessionId,
|
|
@@ -1742,9 +1840,14 @@ var init_server = __esm({
|
|
|
1742
1840
|
const channelWs = this.channelSockets.get(msg.sessionId);
|
|
1743
1841
|
if (channelWs?.readyState === WebSocket2.OPEN) {
|
|
1744
1842
|
channelWs.send(JSON.stringify({ ...msg, source: "dashboard" }));
|
|
1843
|
+
} else {
|
|
1844
|
+
const reason = "the session is not connected";
|
|
1845
|
+
logger.warn(`Message rejected for ${msg.sessionId}: ${reason}`);
|
|
1846
|
+
const rejected = { type: "message_rejected", sessionId: msg.sessionId, reason };
|
|
1847
|
+
if (ws.readyState === WebSocket2.OPEN) ws.send(JSON.stringify(rejected));
|
|
1745
1848
|
}
|
|
1746
1849
|
} else if (msg.type === "image_upload") {
|
|
1747
|
-
this.handleImageUpload(msg);
|
|
1850
|
+
this.handleImageUpload(ws, msg);
|
|
1748
1851
|
} else if (msg.type === "permission_response") {
|
|
1749
1852
|
if (this.forwardPermissionResponse(msg)) {
|
|
1750
1853
|
const verdict = msg.choiceId !== void 0 ? `choice ${msg.choiceId}` : msg.behavior;
|
|
@@ -1795,27 +1898,38 @@ var init_server = __esm({
|
|
|
1795
1898
|
}
|
|
1796
1899
|
}
|
|
1797
1900
|
}
|
|
1798
|
-
handleImageUpload(msg) {
|
|
1901
|
+
handleImageUpload(ws, msg) {
|
|
1799
1902
|
const { sessionId: sessionId2, imageData, mimeType, originalName, content } = msg;
|
|
1903
|
+
const reject = (reason) => {
|
|
1904
|
+
logger.warn(`Image upload rejected for ${sessionId2}: ${reason}`);
|
|
1905
|
+
const rejected = { type: "upload_rejected", sessionId: sessionId2, reason, withText: typeof content === "string" && content.trim().length > 0 };
|
|
1906
|
+
if (ws.readyState === WebSocket2.OPEN) ws.send(JSON.stringify(rejected));
|
|
1907
|
+
};
|
|
1908
|
+
const channelWs = this.channelSockets.get(sessionId2);
|
|
1909
|
+
if (!channelWs || channelWs.readyState !== WebSocket2.OPEN) {
|
|
1910
|
+
reject("the session is not connected");
|
|
1911
|
+
return;
|
|
1912
|
+
}
|
|
1800
1913
|
if (!this.localChannels.has(sessionId2)) {
|
|
1801
|
-
|
|
1914
|
+
reject("this session is on another PC; images can only go to sessions on the hub's PC");
|
|
1802
1915
|
return;
|
|
1803
1916
|
}
|
|
1804
|
-
const channelWs = this.channelSockets.get(sessionId2);
|
|
1805
|
-
if (!channelWs || channelWs.readyState !== WebSocket2.OPEN) return;
|
|
1806
1917
|
const allowedTypes = ["image/png", "image/jpeg", "image/gif", "image/webp"];
|
|
1807
|
-
if (!allowedTypes.includes(mimeType))
|
|
1918
|
+
if (!allowedTypes.includes(mimeType)) {
|
|
1919
|
+
reject("only PNG, JPEG, GIF and WebP images are supported");
|
|
1920
|
+
return;
|
|
1921
|
+
}
|
|
1808
1922
|
const base64Data = imageData.replace(/^data:image\/\w+;base64,/, "");
|
|
1809
1923
|
const buffer = Buffer.from(base64Data, "base64");
|
|
1810
1924
|
if (buffer.length > 10 * 1024 * 1024) {
|
|
1811
|
-
|
|
1925
|
+
reject("the image is larger than 10 MB");
|
|
1812
1926
|
return;
|
|
1813
1927
|
}
|
|
1814
|
-
|
|
1928
|
+
fs7.mkdirSync(UPLOADS_DIR, { recursive: true });
|
|
1815
1929
|
const ext = mimeType.split("/")[1] === "jpeg" ? "jpg" : mimeType.split("/")[1];
|
|
1816
1930
|
const filename = `${randomUUID3()}.${ext}`;
|
|
1817
|
-
const filePath =
|
|
1818
|
-
|
|
1931
|
+
const filePath = path8.join(UPLOADS_DIR, filename);
|
|
1932
|
+
fs7.writeFileSync(filePath, buffer);
|
|
1819
1933
|
const forwardMsg = {
|
|
1820
1934
|
type: "image_to_session",
|
|
1821
1935
|
sessionId: sessionId2,
|
|
@@ -1829,7 +1943,7 @@ var init_server = __esm({
|
|
|
1829
1943
|
logger.info(`Image saved and forwarded: ${filename} (${buffer.length} bytes)`);
|
|
1830
1944
|
setTimeout(() => {
|
|
1831
1945
|
try {
|
|
1832
|
-
|
|
1946
|
+
fs7.unlinkSync(filePath);
|
|
1833
1947
|
} catch {
|
|
1834
1948
|
}
|
|
1835
1949
|
}, 5 * 60 * 1e3).unref();
|
|
@@ -1856,19 +1970,25 @@ var init_server = __esm({
|
|
|
1856
1970
|
this.telegramBot.getSessions = () => this.sessions.getAll();
|
|
1857
1971
|
this.telegramBot.onMessageToSession = (sessionId2, content) => {
|
|
1858
1972
|
const channelWs = this.channelSockets.get(sessionId2);
|
|
1859
|
-
if (channelWs?.readyState
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
logger.info(`Telegram message forwarded to session: ${sessionId2}`);
|
|
1973
|
+
if (channelWs?.readyState !== WebSocket2.OPEN) {
|
|
1974
|
+
logger.warn(`Telegram message not delivered to ${sessionId2}: the session is not connected`);
|
|
1975
|
+
return false;
|
|
1863
1976
|
}
|
|
1977
|
+
const msg = { type: "message_to_session", sessionId: sessionId2, content, source: "telegram" };
|
|
1978
|
+
channelWs.send(JSON.stringify(msg));
|
|
1979
|
+
logger.info(`Telegram message forwarded to session: ${sessionId2}`);
|
|
1980
|
+
return true;
|
|
1864
1981
|
};
|
|
1865
1982
|
this.telegramBot.onImageToSession = (sessionId2, imagePath, mimeType, caption) => {
|
|
1866
1983
|
const channelWs = this.channelSockets.get(sessionId2);
|
|
1867
|
-
if (channelWs?.readyState
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
logger.info(`Telegram photo forwarded to session: ${sessionId2}`);
|
|
1984
|
+
if (channelWs?.readyState !== WebSocket2.OPEN) {
|
|
1985
|
+
logger.warn(`Telegram photo not delivered to ${sessionId2}: the session is not connected`);
|
|
1986
|
+
return false;
|
|
1871
1987
|
}
|
|
1988
|
+
const msg = { type: "image_to_session", sessionId: sessionId2, imagePath, mimeType, content: caption, source: "telegram" };
|
|
1989
|
+
channelWs.send(JSON.stringify(msg));
|
|
1990
|
+
logger.info(`Telegram photo forwarded to session: ${sessionId2}`);
|
|
1991
|
+
return true;
|
|
1872
1992
|
};
|
|
1873
1993
|
this.telegramBot.onPermissionVerdict = (sessionId2, requestId, behavior) => {
|
|
1874
1994
|
if (this.forwardPermissionResponse({ sessionId: sessionId2, requestId, behavior })) {
|
|
@@ -1993,7 +2113,7 @@ var init_server = __esm({
|
|
|
1993
2113
|
logger.warn("Codex adapter is enabled but codex/main.js was not found next to the hub");
|
|
1994
2114
|
return;
|
|
1995
2115
|
}
|
|
1996
|
-
this.codexSupervisor = new CodexSupervisor(script);
|
|
2116
|
+
this.codexSupervisor = new CodexSupervisor(script, { env: adapterEnv(process.env, { host: this.host, port: this.port }) });
|
|
1997
2117
|
this.codexSupervisor.start();
|
|
1998
2118
|
logger.info("Codex adapter started");
|
|
1999
2119
|
}
|
|
@@ -2020,11 +2140,11 @@ var init_server = __esm({
|
|
|
2020
2140
|
}
|
|
2021
2141
|
cleanupUploads() {
|
|
2022
2142
|
try {
|
|
2023
|
-
if (!
|
|
2024
|
-
const files =
|
|
2143
|
+
if (!fs7.existsSync(UPLOADS_DIR)) return;
|
|
2144
|
+
const files = fs7.readdirSync(UPLOADS_DIR);
|
|
2025
2145
|
for (const file of files) {
|
|
2026
2146
|
try {
|
|
2027
|
-
|
|
2147
|
+
fs7.unlinkSync(path8.join(UPLOADS_DIR, file));
|
|
2028
2148
|
} catch {
|
|
2029
2149
|
}
|
|
2030
2150
|
}
|
|
@@ -2223,9 +2343,9 @@ var init_hub_client = __esm({
|
|
|
2223
2343
|
});
|
|
2224
2344
|
|
|
2225
2345
|
// src/channel/peer-name.ts
|
|
2226
|
-
import
|
|
2346
|
+
import fs8 from "fs";
|
|
2227
2347
|
import os2 from "os";
|
|
2228
|
-
import
|
|
2348
|
+
import path9 from "path";
|
|
2229
2349
|
function findPeerName(records, env) {
|
|
2230
2350
|
const valid = records.filter((r) => typeof r === "object" && r !== null);
|
|
2231
2351
|
const nameOf = (match) => {
|
|
@@ -2245,18 +2365,18 @@ function readPeerName(env = process.env) {
|
|
|
2245
2365
|
sessionId: env.CLAUDE_CODE_SESSION_ID
|
|
2246
2366
|
};
|
|
2247
2367
|
if (!lookup.messagingSocket && !lookup.sessionId) return void 0;
|
|
2248
|
-
const configDir = env.CLAUDE_CONFIG_DIR ||
|
|
2249
|
-
const dir =
|
|
2368
|
+
const configDir = env.CLAUDE_CONFIG_DIR || path9.join(os2.homedir(), ".claude");
|
|
2369
|
+
const dir = path9.join(configDir, "sessions");
|
|
2250
2370
|
let files;
|
|
2251
2371
|
try {
|
|
2252
|
-
files =
|
|
2372
|
+
files = fs8.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
2253
2373
|
} catch {
|
|
2254
2374
|
return void 0;
|
|
2255
2375
|
}
|
|
2256
2376
|
const records = [];
|
|
2257
2377
|
for (const f of files) {
|
|
2258
2378
|
try {
|
|
2259
|
-
records.push(JSON.parse(
|
|
2379
|
+
records.push(JSON.parse(fs8.readFileSync(path9.join(dir, f), "utf8")));
|
|
2260
2380
|
} catch {
|
|
2261
2381
|
}
|
|
2262
2382
|
}
|
|
@@ -2279,7 +2399,7 @@ import {
|
|
|
2279
2399
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
2280
2400
|
import { z } from "zod";
|
|
2281
2401
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
2282
|
-
import
|
|
2402
|
+
import path10 from "path";
|
|
2283
2403
|
async function main() {
|
|
2284
2404
|
logger.info(`Starting MCP channel server (session: ${sessionId})`);
|
|
2285
2405
|
hubClient.connect();
|
|
@@ -2348,7 +2468,7 @@ var init_server2 = __esm({
|
|
|
2348
2468
|
init_hub_client();
|
|
2349
2469
|
init_peer_name2();
|
|
2350
2470
|
sessionId = randomUUID4();
|
|
2351
|
-
sessionName = process.env.CLAUDE_ALARM_SESSION_NAME ??
|
|
2471
|
+
sessionName = process.env.CLAUDE_ALARM_SESSION_NAME ?? path10.basename(process.cwd());
|
|
2352
2472
|
peerName = readPeerName();
|
|
2353
2473
|
server = new Server(
|
|
2354
2474
|
{
|
|
@@ -2525,20 +2645,21 @@ NOTIFICATIONS:
|
|
|
2525
2645
|
// src/cli.ts
|
|
2526
2646
|
init_config();
|
|
2527
2647
|
import { spawn as spawn3 } from "child_process";
|
|
2528
|
-
import
|
|
2529
|
-
import
|
|
2648
|
+
import fs9 from "fs";
|
|
2649
|
+
import path11 from "path";
|
|
2530
2650
|
import readline from "readline";
|
|
2531
2651
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2532
2652
|
|
|
2533
2653
|
// src/codex/transport.ts
|
|
2534
2654
|
init_logger();
|
|
2535
|
-
import { spawn } from "child_process";
|
|
2655
|
+
import { execFile, spawn } from "child_process";
|
|
2536
2656
|
import fs2 from "fs";
|
|
2537
2657
|
import path3 from "path";
|
|
2538
2658
|
import { Duplex } from "stream";
|
|
2539
2659
|
import WebSocket from "ws";
|
|
2540
2660
|
function resolveCommand(command, platform = process.platform, env = process.env) {
|
|
2541
|
-
if (platform !== "win32"
|
|
2661
|
+
if (platform !== "win32") return { file: command, shell: false };
|
|
2662
|
+
if (/[\\/]/.test(command) || path3.extname(command)) return { file: command, shell: /\.(cmd|bat)$/i.test(command) };
|
|
2542
2663
|
const file = findCodex(command, platform, env);
|
|
2543
2664
|
return file ? { file, shell: file.endsWith(".cmd") } : { file: command, shell: false };
|
|
2544
2665
|
}
|
|
@@ -2561,13 +2682,116 @@ function findCodex(command, platform = process.platform, env = process.env) {
|
|
|
2561
2682
|
];
|
|
2562
2683
|
return candidates.find((file) => !!file && fs2.existsSync(file));
|
|
2563
2684
|
}
|
|
2685
|
+
var shellSpawns = /* @__PURE__ */ new WeakSet();
|
|
2686
|
+
var spawnedThroughShell = (child) => shellSpawns.has(child);
|
|
2564
2687
|
var defaultSpawn = (command, args) => {
|
|
2565
2688
|
const { file, shell } = resolveCommand(command);
|
|
2566
|
-
if (shell) return spawn(
|
|
2567
|
-
|
|
2689
|
+
if (!shell) return spawn(file, args, { stdio: "pipe", windowsHide: true });
|
|
2690
|
+
const child = spawn(`"${file}" ${args.join(" ")}`, { stdio: "pipe", windowsHide: true, shell: true });
|
|
2691
|
+
shellSpawns.add(child);
|
|
2692
|
+
return child;
|
|
2693
|
+
};
|
|
2694
|
+
var queryProcesses = () => new Promise((resolve, reject) => {
|
|
2695
|
+
const script = "@(Get-CimInstance Win32_Process | ForEach-Object { [pscustomobject]@{ pid = [int]$_.ProcessId; parentPid = [int]$_.ParentProcessId; creationTime = $_.CreationDate.ToUniversalTime().ToString('o') } }) | ConvertTo-Json -Compress";
|
|
2696
|
+
execFile("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], { windowsHide: true, timeout: 5e3, maxBuffer: 4 * 1024 * 1024 }, (err, stdout) => {
|
|
2697
|
+
if (err) {
|
|
2698
|
+
reject(err);
|
|
2699
|
+
return;
|
|
2700
|
+
}
|
|
2701
|
+
try {
|
|
2702
|
+
const rows = JSON.parse(stdout);
|
|
2703
|
+
if (!Array.isArray(rows) || rows.some((row) => !Number.isInteger(row.pid) || !Number.isInteger(row.parentPid) || typeof row.creationTime !== "string")) {
|
|
2704
|
+
throw new Error("invalid process snapshot");
|
|
2705
|
+
}
|
|
2706
|
+
resolve(rows);
|
|
2707
|
+
} catch (error) {
|
|
2708
|
+
reject(error);
|
|
2709
|
+
}
|
|
2710
|
+
});
|
|
2711
|
+
});
|
|
2712
|
+
var runFile = (file, args, options, callback) => {
|
|
2713
|
+
execFile(file, args, options, (err) => callback(err));
|
|
2568
2714
|
};
|
|
2569
|
-
function
|
|
2715
|
+
function descendantsOf(rootPid, rows) {
|
|
2716
|
+
const root = rows.find((row) => row.pid === rootPid);
|
|
2717
|
+
if (!root) return void 0;
|
|
2718
|
+
const found = [];
|
|
2719
|
+
const seen = /* @__PURE__ */ new Set([root.pid]);
|
|
2720
|
+
let level = [root];
|
|
2721
|
+
for (let depth = 1; level.length; depth++) {
|
|
2722
|
+
const next = [];
|
|
2723
|
+
for (const parent of level) {
|
|
2724
|
+
for (const row of rows) {
|
|
2725
|
+
if (row.parentPid !== parent.pid || seen.has(row.pid) || row.creationTime < parent.creationTime) continue;
|
|
2726
|
+
seen.add(row.pid);
|
|
2727
|
+
next.push(row);
|
|
2728
|
+
found.push({ ...row, depth });
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
level = next;
|
|
2732
|
+
}
|
|
2733
|
+
return found;
|
|
2734
|
+
}
|
|
2735
|
+
var exited = (child) => child.exitCode !== null || child.signalCode !== null;
|
|
2736
|
+
function treeKiller(platform = process.platform, run = runFile, query = queryProcesses, throughShell = spawnedThroughShell) {
|
|
2737
|
+
const recorded = /* @__PURE__ */ new WeakMap();
|
|
2738
|
+
const stop = (pid) => new Promise((resolve) => {
|
|
2739
|
+
run("taskkill", ["/PID", String(pid), "/F"], { windowsHide: true }, (err) => {
|
|
2740
|
+
if (err) logger.debug(`taskkill ${pid} failed: ${err.message}`);
|
|
2741
|
+
resolve();
|
|
2742
|
+
});
|
|
2743
|
+
});
|
|
2744
|
+
const stopDeepestFirst = async (descendants) => {
|
|
2745
|
+
for (const descendant of [...descendants].sort((a, b) => b.depth - a.depth)) await stop(descendant.pid);
|
|
2746
|
+
};
|
|
2747
|
+
const snapshot = async () => {
|
|
2748
|
+
try {
|
|
2749
|
+
return await query();
|
|
2750
|
+
} catch (err) {
|
|
2751
|
+
logger.debug(`codex process query failed: ${String(err)}`);
|
|
2752
|
+
return void 0;
|
|
2753
|
+
}
|
|
2754
|
+
};
|
|
2755
|
+
const isShellTree = (child) => platform === "win32" && child.pid !== void 0 && throughShell(child);
|
|
2756
|
+
const killer = async (child) => {
|
|
2757
|
+
if (!isShellTree(child)) {
|
|
2758
|
+
if (!exited(child)) child.kill();
|
|
2759
|
+
return;
|
|
2760
|
+
}
|
|
2761
|
+
if (!exited(child)) {
|
|
2762
|
+
const rows = await snapshot();
|
|
2763
|
+
if (!exited(child)) {
|
|
2764
|
+
const descendants2 = rows && descendantsOf(child.pid, rows);
|
|
2765
|
+
if (descendants2) await stopDeepestFirst(descendants2);
|
|
2766
|
+
child.kill();
|
|
2767
|
+
return;
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2770
|
+
const descendants = await recorded.get(child) ?? [];
|
|
2771
|
+
if (!descendants.length) return;
|
|
2772
|
+
const current = await snapshot();
|
|
2773
|
+
if (!current) return;
|
|
2774
|
+
await stopDeepestFirst(descendants.filter((d) => current.some((row) => row.pid === d.pid && row.creationTime === d.creationTime)));
|
|
2775
|
+
};
|
|
2776
|
+
killer.track = async (child) => {
|
|
2777
|
+
if (!isShellTree(child) || exited(child)) return;
|
|
2778
|
+
const earlier = recorded.get(child);
|
|
2779
|
+
const latest = (async () => {
|
|
2780
|
+
const rows = await snapshot();
|
|
2781
|
+
const found = rows && !exited(child) ? descendantsOf(child.pid, rows) ?? [] : [];
|
|
2782
|
+
const before = await earlier ?? [];
|
|
2783
|
+
return [...before, ...found.filter((d) => !before.some((b) => b.pid === d.pid && b.creationTime === d.creationTime))];
|
|
2784
|
+
})();
|
|
2785
|
+
recorded.set(child, latest);
|
|
2786
|
+
await latest;
|
|
2787
|
+
};
|
|
2788
|
+
return killer;
|
|
2789
|
+
}
|
|
2790
|
+
var killTree = treeKiller();
|
|
2791
|
+
var HANDSHAKE_TIMEOUT_MS = 1e4;
|
|
2792
|
+
function connectProxy(command, spawnFn = defaultSpawn, timeoutMs = HANDSHAKE_TIMEOUT_MS, killTreeFn = killTree) {
|
|
2570
2793
|
const child = spawnFn(command, ["app-server", "proxy"]);
|
|
2794
|
+
void killTreeFn.track?.(child);
|
|
2571
2795
|
child.stdin?.on("error", () => {
|
|
2572
2796
|
});
|
|
2573
2797
|
child.stderr?.on("data", (d) => logger.debug(`codex proxy: ${String(d).trim()}`));
|
|
@@ -2594,17 +2818,20 @@ function connectProxy(command, spawnFn = defaultSpawn) {
|
|
|
2594
2818
|
const ws = new WebSocket("ws://localhost/", {
|
|
2595
2819
|
createConnection: (() => stream)
|
|
2596
2820
|
});
|
|
2597
|
-
|
|
2821
|
+
let closing;
|
|
2822
|
+
const close = () => closing ??= (async () => {
|
|
2598
2823
|
ws.terminate();
|
|
2599
|
-
child.
|
|
2600
|
-
|
|
2824
|
+
child.stdin?.end();
|
|
2825
|
+
await killTreeFn(child);
|
|
2826
|
+
})();
|
|
2601
2827
|
return new Promise((resolve, reject) => {
|
|
2602
2828
|
let settled = false;
|
|
2829
|
+
const timer = setTimeout(() => fail(new Error(`codex daemon did not answer within ${timeoutMs / 1e3}s`)), timeoutMs);
|
|
2603
2830
|
const fail = (err) => {
|
|
2604
2831
|
if (settled) return;
|
|
2605
2832
|
settled = true;
|
|
2606
|
-
|
|
2607
|
-
reject(err);
|
|
2833
|
+
clearTimeout(timer);
|
|
2834
|
+
void close().then(() => reject(err), () => reject(err));
|
|
2608
2835
|
};
|
|
2609
2836
|
child.on("error", fail);
|
|
2610
2837
|
child.once("exit", (code) => fail(new Error(`codex proxy exited (code ${code})`)));
|
|
@@ -2612,8 +2839,10 @@ function connectProxy(command, spawnFn = defaultSpawn) {
|
|
|
2612
2839
|
ws.once("open", () => {
|
|
2613
2840
|
if (settled) return;
|
|
2614
2841
|
settled = true;
|
|
2842
|
+
clearTimeout(timer);
|
|
2615
2843
|
child.on("error", (err) => logger.warn(`codex proxy error: ${err.message}`));
|
|
2616
2844
|
ws.on("error", (err) => logger.warn(`codex daemon socket error: ${err.message}`));
|
|
2845
|
+
void killTreeFn.track?.(child);
|
|
2617
2846
|
resolve({ ws, close });
|
|
2618
2847
|
});
|
|
2619
2848
|
});
|
|
@@ -2905,7 +3134,7 @@ async function runCodexTask(opts) {
|
|
|
2905
3134
|
};
|
|
2906
3135
|
} finally {
|
|
2907
3136
|
for (const t of timers) clearTimeout(t);
|
|
2908
|
-
conn.close();
|
|
3137
|
+
await conn.close();
|
|
2909
3138
|
}
|
|
2910
3139
|
}
|
|
2911
3140
|
var VALUE_FLAGS = {
|
|
@@ -3025,7 +3254,7 @@ function resolveAdapterHub(config2, env = process.env) {
|
|
|
3025
3254
|
// src/codex/start-check.ts
|
|
3026
3255
|
var ADAPTER_REPORT_TIMEOUT_MS = 1e4;
|
|
3027
3256
|
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";
|
|
3257
|
+
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
3258
|
var CONFIG_PATH = "~/.claude-alarm/config.json";
|
|
3030
3259
|
var NOT_CHECKED = { text: "Codex daemon: not checked (the adapter was already running)", warning: false };
|
|
3031
3260
|
function toOutcome(msg) {
|
|
@@ -3042,9 +3271,11 @@ function waitForAdapterReport(child, timeoutMs = ADAPTER_REPORT_TIMEOUT_MS) {
|
|
|
3042
3271
|
resolve(result);
|
|
3043
3272
|
};
|
|
3044
3273
|
const onMessage = (msg) => {
|
|
3045
|
-
if (msg
|
|
3046
|
-
|
|
3047
|
-
}
|
|
3274
|
+
if (!msg || typeof msg !== "object") return;
|
|
3275
|
+
const m = msg;
|
|
3276
|
+
if (m.type === "codex-first-connect") finish({ kind: "report", outcome: toOutcome(m) });
|
|
3277
|
+
else if (m.type === "codex-already-running" && Number.isInteger(m.pid)) finish({ kind: "already", pid: m.pid });
|
|
3278
|
+
else if (m.type === "codex-lock-failed") finish({ kind: "lockFailed", error: String(m.error ?? "") });
|
|
3048
3279
|
};
|
|
3049
3280
|
const onExit = (code, signal) => finish({ kind: "exited", code, signal });
|
|
3050
3281
|
const onError = (error) => finish({ kind: "exited", code: null, signal: null, error });
|
|
@@ -3124,9 +3355,9 @@ function printAlreadyRunning(d, pid, hub) {
|
|
|
3124
3355
|
printChecks(d, NOT_CHECKED, hubLine(hub, d.hub));
|
|
3125
3356
|
}
|
|
3126
3357
|
async function startAdapter(d) {
|
|
3127
|
-
const existing = d.
|
|
3128
|
-
if (existing
|
|
3129
|
-
printAlreadyRunning(d, existing, await checkHub(d.hub, d.hubTimeoutMs));
|
|
3358
|
+
const existing = await d.queryOwner();
|
|
3359
|
+
if (existing.state === "running") {
|
|
3360
|
+
printAlreadyRunning(d, existing.pid, await checkHub(d.hub, d.hubTimeoutMs));
|
|
3130
3361
|
return 0;
|
|
3131
3362
|
}
|
|
3132
3363
|
const child = d.spawnAdapter();
|
|
@@ -3143,6 +3374,14 @@ async function startAdapter(d) {
|
|
|
3143
3374
|
]);
|
|
3144
3375
|
if (child.connected) child.disconnect();
|
|
3145
3376
|
child.unref();
|
|
3377
|
+
if (report.kind === "already") {
|
|
3378
|
+
printAlreadyRunning(d, report.pid, hub);
|
|
3379
|
+
return 0;
|
|
3380
|
+
}
|
|
3381
|
+
if (report.kind === "lockFailed") {
|
|
3382
|
+
d.err(`Codex adapter cannot start: ${report.error}`);
|
|
3383
|
+
return 1;
|
|
3384
|
+
}
|
|
3146
3385
|
if (report.kind !== "exited" && !seen.exit && child.pid !== void 0) {
|
|
3147
3386
|
d.out(`Codex adapter started (PID: ${child.pid}). Logs: ${d.logFile}`);
|
|
3148
3387
|
printChecks(d, daemonLine(report, d.command, d.logFile), hubLine(hub, d.hub));
|
|
@@ -3153,10 +3392,9 @@ async function startAdapter(d) {
|
|
|
3153
3392
|
d.err(`Codex adapter failed to start: ${exit?.error?.message ?? "no process id"}`);
|
|
3154
3393
|
return 1;
|
|
3155
3394
|
}
|
|
3156
|
-
const owner = d.
|
|
3157
|
-
if (owner
|
|
3158
|
-
|
|
3159
|
-
printAlreadyRunning(d, owner, hub);
|
|
3395
|
+
const owner = await d.queryOwner();
|
|
3396
|
+
if (owner.state === "running" && owner.pid !== child.pid) {
|
|
3397
|
+
printAlreadyRunning(d, owner.pid, hub);
|
|
3160
3398
|
return 0;
|
|
3161
3399
|
}
|
|
3162
3400
|
const how = exit.code !== null ? `code ${exit.code}` : `signal ${exit.signal}`;
|
|
@@ -3164,8 +3402,147 @@ async function startAdapter(d) {
|
|
|
3164
3402
|
return 1;
|
|
3165
3403
|
}
|
|
3166
3404
|
|
|
3405
|
+
// src/codex/instance-lock.ts
|
|
3406
|
+
import crypto from "crypto";
|
|
3407
|
+
import fs4 from "fs";
|
|
3408
|
+
import net2 from "net";
|
|
3409
|
+
import path5 from "path";
|
|
3410
|
+
var CONTROL_PROTOCOL = 1;
|
|
3411
|
+
var STOP_PROOF_LABEL = "claude-alarm codex stop";
|
|
3412
|
+
var MAX_LINE_BYTES = 4096;
|
|
3413
|
+
function stopProof(token) {
|
|
3414
|
+
return crypto.createHmac("sha256", token).update(STOP_PROOF_LABEL).digest("hex");
|
|
3415
|
+
}
|
|
3416
|
+
function controlEndpoint(configDir, platform = process.platform) {
|
|
3417
|
+
if (platform !== "win32") return path5.posix.join(configDir, "codex.sock");
|
|
3418
|
+
let dir;
|
|
3419
|
+
try {
|
|
3420
|
+
dir = fs4.realpathSync.native(configDir);
|
|
3421
|
+
} catch {
|
|
3422
|
+
dir = path5.win32.resolve(configDir);
|
|
3423
|
+
}
|
|
3424
|
+
const hash = crypto.createHash("sha256").update(dir.toLowerCase()).digest("hex").slice(0, 16);
|
|
3425
|
+
return `\\\\.\\pipe\\claude-alarm-codex-${hash}`;
|
|
3426
|
+
}
|
|
3427
|
+
function ask(endpoint, request, timeoutMs) {
|
|
3428
|
+
return new Promise((resolve) => {
|
|
3429
|
+
let buffer = "";
|
|
3430
|
+
let settled = false;
|
|
3431
|
+
const socket = net2.connect(endpoint);
|
|
3432
|
+
const finish = (answer) => {
|
|
3433
|
+
if (settled) return;
|
|
3434
|
+
settled = true;
|
|
3435
|
+
clearTimeout(timer);
|
|
3436
|
+
socket.destroy();
|
|
3437
|
+
resolve(answer);
|
|
3438
|
+
};
|
|
3439
|
+
const timer = setTimeout(() => finish({ state: "unknown", error: `no answer within ${timeoutMs}ms` }), timeoutMs);
|
|
3440
|
+
socket.setEncoding("utf8");
|
|
3441
|
+
socket.on("connect", () => socket.write(`${JSON.stringify(request)}
|
|
3442
|
+
`));
|
|
3443
|
+
socket.on("data", (chunk) => {
|
|
3444
|
+
buffer += chunk;
|
|
3445
|
+
const end = buffer.indexOf("\n");
|
|
3446
|
+
if (end < 0) {
|
|
3447
|
+
if (Buffer.byteLength(buffer) > MAX_LINE_BYTES) finish({ state: "unknown", error: "reply too long" });
|
|
3448
|
+
return;
|
|
3449
|
+
}
|
|
3450
|
+
try {
|
|
3451
|
+
finish({ reply: JSON.parse(buffer.slice(0, end)) });
|
|
3452
|
+
} catch {
|
|
3453
|
+
finish({ state: "unknown", error: "unreadable reply" });
|
|
3454
|
+
}
|
|
3455
|
+
});
|
|
3456
|
+
socket.on("end", () => finish({ state: "unknown", error: "closed without a reply" }));
|
|
3457
|
+
socket.on("error", (err) => {
|
|
3458
|
+
if (err.code === "ENOENT" || err.code === "ECONNREFUSED") finish({ state: "absent" });
|
|
3459
|
+
else finish({ state: "unknown", error: err.code ?? err.message });
|
|
3460
|
+
});
|
|
3461
|
+
});
|
|
3462
|
+
}
|
|
3463
|
+
async function queryOwner(endpoint, timeoutMs = 1e3) {
|
|
3464
|
+
const answer = await ask(endpoint, { type: "status" }, timeoutMs);
|
|
3465
|
+
if (!("reply" in answer)) return answer;
|
|
3466
|
+
const reply = answer.reply;
|
|
3467
|
+
if (reply?.type === "status" && reply.protocol === CONTROL_PROTOCOL && Number.isInteger(reply.pid)) {
|
|
3468
|
+
return { state: "running", pid: reply.pid };
|
|
3469
|
+
}
|
|
3470
|
+
return { state: "unknown", error: "unexpected reply" };
|
|
3471
|
+
}
|
|
3472
|
+
async function requestStop(endpoint, token, timeoutMs = 1e3) {
|
|
3473
|
+
const answer = await ask(endpoint, { type: "stop", proof: stopProof(token) }, timeoutMs);
|
|
3474
|
+
if (!("reply" in answer)) return answer;
|
|
3475
|
+
const reply = answer.reply;
|
|
3476
|
+
if (reply?.type === "stopping" && Number.isInteger(reply.pid)) return { state: "stopping", pid: reply.pid };
|
|
3477
|
+
if (reply?.type === "error" && reply.error === "unauthorized") return { state: "unauthorized" };
|
|
3478
|
+
return { state: "unknown", error: "unexpected reply" };
|
|
3479
|
+
}
|
|
3480
|
+
|
|
3481
|
+
// src/codex/control-cli.ts
|
|
3482
|
+
var STOP_WAIT_MS = 5e3;
|
|
3483
|
+
var STOP_POLL_MS = 100;
|
|
3484
|
+
function legacyNote(d, ownerPid) {
|
|
3485
|
+
const pid = d.legacyPid();
|
|
3486
|
+
if (pid === void 0 || pid === ownerPid) return;
|
|
3487
|
+
if (!d.isRunning(pid)) {
|
|
3488
|
+
d.removeLegacyPidFile();
|
|
3489
|
+
return;
|
|
3490
|
+
}
|
|
3491
|
+
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.`);
|
|
3492
|
+
}
|
|
3493
|
+
function unavailable(d, error) {
|
|
3494
|
+
d.err(`Codex adapter control endpoint is unavailable: ${error}`);
|
|
3495
|
+
return 1;
|
|
3496
|
+
}
|
|
3497
|
+
async function stopAdapter(d, startWithHub) {
|
|
3498
|
+
const owner = await d.queryOwner();
|
|
3499
|
+
if (owner.state === "unknown") return unavailable(d, owner.error);
|
|
3500
|
+
if (owner.state === "absent") {
|
|
3501
|
+
d.out("Codex adapter is not running");
|
|
3502
|
+
legacyNote(d);
|
|
3503
|
+
return 0;
|
|
3504
|
+
}
|
|
3505
|
+
const reply = await d.requestStop();
|
|
3506
|
+
if (reply.state === "unknown") return unavailable(d, reply.error);
|
|
3507
|
+
if (reply.state === "unauthorized") {
|
|
3508
|
+
d.err(`Codex adapter refused to stop: the token in ${d.configFile} does not match the one it started with`);
|
|
3509
|
+
return 1;
|
|
3510
|
+
}
|
|
3511
|
+
if (reply.state === "absent") {
|
|
3512
|
+
d.out("Codex adapter is not running");
|
|
3513
|
+
return 0;
|
|
3514
|
+
}
|
|
3515
|
+
const sleep = d.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
3516
|
+
for (let waited = 0; waited < (d.stopWaitMs ?? STOP_WAIT_MS); waited += STOP_POLL_MS) {
|
|
3517
|
+
await sleep(STOP_POLL_MS);
|
|
3518
|
+
const now = await d.queryOwner();
|
|
3519
|
+
if (now.state === "absent" || now.state === "running" && now.pid !== reply.pid) {
|
|
3520
|
+
d.out(`Codex adapter stopped (PID: ${reply.pid})`);
|
|
3521
|
+
if (now.state === "running") d.out(`Another Codex adapter took over (PID: ${now.pid})`);
|
|
3522
|
+
else if (startWithHub) d.out("If the hub started it, the hub will not start it again until you restart the hub.");
|
|
3523
|
+
return 0;
|
|
3524
|
+
}
|
|
3525
|
+
}
|
|
3526
|
+
d.err(`Stop requested, but the Codex adapter (PID: ${reply.pid}) is still running. Check with: claude-alarm codex status`);
|
|
3527
|
+
return 1;
|
|
3528
|
+
}
|
|
3529
|
+
async function adapterStatus(d, startWithHub) {
|
|
3530
|
+
const owner = await d.queryOwner();
|
|
3531
|
+
if (owner.state === "running") {
|
|
3532
|
+
d.out(`Codex adapter: running (PID: ${owner.pid})`);
|
|
3533
|
+
legacyNote(d, owner.pid);
|
|
3534
|
+
} else if (owner.state === "unknown") {
|
|
3535
|
+
d.out(`Codex adapter: unknown (${owner.error})`);
|
|
3536
|
+
} else {
|
|
3537
|
+
d.out("Codex adapter: not running");
|
|
3538
|
+
legacyNote(d);
|
|
3539
|
+
}
|
|
3540
|
+
d.out(`Start with hub: ${startWithHub ? "enabled" : "disabled"}`);
|
|
3541
|
+
return 0;
|
|
3542
|
+
}
|
|
3543
|
+
|
|
3167
3544
|
// src/cli.ts
|
|
3168
|
-
var __dirname2 =
|
|
3545
|
+
var __dirname2 = path11.dirname(fileURLToPath2(import.meta.url));
|
|
3169
3546
|
function authHeaders(token) {
|
|
3170
3547
|
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
3171
3548
|
}
|
|
@@ -3201,8 +3578,8 @@ Quick start:
|
|
|
3201
3578
|
}
|
|
3202
3579
|
async function checkForUpdates() {
|
|
3203
3580
|
try {
|
|
3204
|
-
const pkgPath =
|
|
3205
|
-
const pkg = JSON.parse(
|
|
3581
|
+
const pkgPath = path11.join(__dirname2, "..", "package.json");
|
|
3582
|
+
const pkg = JSON.parse(fs9.readFileSync(pkgPath, "utf-8"));
|
|
3206
3583
|
const currentVersion = pkg.version;
|
|
3207
3584
|
const res = await fetch("https://registry.npmjs.org/@delt/claude-alarm/latest", {
|
|
3208
3585
|
signal: AbortSignal.timeout(3e3)
|
|
@@ -3225,18 +3602,18 @@ async function hubStart(daemon) {
|
|
|
3225
3602
|
const port = config2.hub.port ?? DEFAULT_HUB_PORT;
|
|
3226
3603
|
const displayHost = hubUrlHost(host);
|
|
3227
3604
|
checkForUpdates();
|
|
3228
|
-
if (
|
|
3229
|
-
const pid = parseInt(
|
|
3605
|
+
if (fs9.existsSync(PID_FILE)) {
|
|
3606
|
+
const pid = parseInt(fs9.readFileSync(PID_FILE, "utf-8").trim(), 10);
|
|
3230
3607
|
if (isProcessRunning(pid)) {
|
|
3231
3608
|
console.log(`Hub is already running (PID: ${pid}). Dashboard: ${loginLink(displayHost, port, config2.hub.token)}`);
|
|
3232
3609
|
return;
|
|
3233
3610
|
}
|
|
3234
|
-
|
|
3611
|
+
fs9.unlinkSync(PID_FILE);
|
|
3235
3612
|
}
|
|
3236
3613
|
if (daemon) {
|
|
3237
3614
|
ensureConfigDir();
|
|
3238
|
-
const logFd =
|
|
3239
|
-
const hubScript =
|
|
3615
|
+
const logFd = fs9.openSync(LOG_FILE, "a");
|
|
3616
|
+
const hubScript = path11.join(__dirname2, "hub", "server.js");
|
|
3240
3617
|
const child = spawn3(process.execPath, [hubScript], {
|
|
3241
3618
|
detached: true,
|
|
3242
3619
|
stdio: ["ignore", logFd, logFd],
|
|
@@ -3247,21 +3624,21 @@ async function hubStart(daemon) {
|
|
|
3247
3624
|
console.error("Failed to start hub daemon");
|
|
3248
3625
|
process.exit(1);
|
|
3249
3626
|
}
|
|
3250
|
-
|
|
3251
|
-
let
|
|
3627
|
+
fs9.writeFileSync(PID_FILE, String(pid), "utf-8");
|
|
3628
|
+
let exited2 = false;
|
|
3252
3629
|
child.once("exit", () => {
|
|
3253
|
-
|
|
3630
|
+
exited2 = true;
|
|
3254
3631
|
});
|
|
3255
3632
|
child.unref();
|
|
3256
3633
|
const startup = await waitForHub({
|
|
3257
3634
|
url: `http://${displayHost}:${port}/api/status`,
|
|
3258
3635
|
token: config2.hub.token,
|
|
3259
3636
|
pid,
|
|
3260
|
-
isAlive: () => !
|
|
3637
|
+
isAlive: () => !exited2
|
|
3261
3638
|
});
|
|
3262
3639
|
if (startup !== "ready") {
|
|
3263
|
-
if (!
|
|
3264
|
-
if (
|
|
3640
|
+
if (!exited2) child.kill();
|
|
3641
|
+
if (fs9.existsSync(PID_FILE)) fs9.unlinkSync(PID_FILE);
|
|
3265
3642
|
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
3643
|
process.exit(1);
|
|
3267
3644
|
}
|
|
@@ -3280,11 +3657,11 @@ async function hubStart(daemon) {
|
|
|
3280
3657
|
const hub = new HubServer2(config2);
|
|
3281
3658
|
await hub.start();
|
|
3282
3659
|
ensureConfigDir();
|
|
3283
|
-
|
|
3660
|
+
fs9.writeFileSync(PID_FILE, String(process.pid), "utf-8");
|
|
3284
3661
|
const shutdown = async () => {
|
|
3285
3662
|
console.log("\nShutting down...");
|
|
3286
3663
|
await hub.stop();
|
|
3287
|
-
if (
|
|
3664
|
+
if (fs9.existsSync(PID_FILE)) fs9.unlinkSync(PID_FILE);
|
|
3288
3665
|
process.exit(0);
|
|
3289
3666
|
};
|
|
3290
3667
|
process.on("SIGINT", shutdown);
|
|
@@ -3292,18 +3669,18 @@ async function hubStart(daemon) {
|
|
|
3292
3669
|
}
|
|
3293
3670
|
}
|
|
3294
3671
|
function hubStop() {
|
|
3295
|
-
if (!
|
|
3672
|
+
if (!fs9.existsSync(PID_FILE)) {
|
|
3296
3673
|
console.log("Hub is not running (no PID file found)");
|
|
3297
3674
|
return;
|
|
3298
3675
|
}
|
|
3299
|
-
const pid = parseInt(
|
|
3676
|
+
const pid = parseInt(fs9.readFileSync(PID_FILE, "utf-8").trim(), 10);
|
|
3300
3677
|
try {
|
|
3301
3678
|
process.kill(pid, "SIGTERM");
|
|
3302
3679
|
console.log(`Hub stopped (PID: ${pid})`);
|
|
3303
3680
|
} catch {
|
|
3304
3681
|
console.log("Hub process not found (may have already stopped)");
|
|
3305
3682
|
}
|
|
3306
|
-
|
|
3683
|
+
fs9.unlinkSync(PID_FILE);
|
|
3307
3684
|
}
|
|
3308
3685
|
async function hubStatus() {
|
|
3309
3686
|
const config2 = loadConfig();
|
|
@@ -3311,8 +3688,8 @@ async function hubStatus() {
|
|
|
3311
3688
|
const port = config2.hub.port ?? DEFAULT_HUB_PORT;
|
|
3312
3689
|
const displayHost = hubUrlHost(host);
|
|
3313
3690
|
let pidInfo = "not running";
|
|
3314
|
-
if (
|
|
3315
|
-
const pid = parseInt(
|
|
3691
|
+
if (fs9.existsSync(PID_FILE)) {
|
|
3692
|
+
const pid = parseInt(fs9.readFileSync(PID_FILE, "utf-8").trim(), 10);
|
|
3316
3693
|
if (isProcessRunning(pid)) {
|
|
3317
3694
|
pidInfo = `running (PID: ${pid})`;
|
|
3318
3695
|
} else {
|
|
@@ -3373,7 +3750,7 @@ async function test() {
|
|
|
3373
3750
|
console.error("Could not reach hub. Start it first: claude-alarm hub start");
|
|
3374
3751
|
}
|
|
3375
3752
|
}
|
|
3376
|
-
function
|
|
3753
|
+
function ask2(question) {
|
|
3377
3754
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
3378
3755
|
return new Promise((resolve) => {
|
|
3379
3756
|
rl.question(question, (answer) => {
|
|
@@ -3384,18 +3761,18 @@ function ask(question) {
|
|
|
3384
3761
|
}
|
|
3385
3762
|
async function init() {
|
|
3386
3763
|
const dir = process.cwd();
|
|
3387
|
-
const projectName =
|
|
3764
|
+
const projectName = path11.basename(dir);
|
|
3388
3765
|
console.log(`
|
|
3389
3766
|
claude-alarm init for "${projectName}"
|
|
3390
3767
|
`);
|
|
3391
|
-
const remote = await
|
|
3768
|
+
const remote = await ask2("Connect to a remote hub? (y/N): ");
|
|
3392
3769
|
let env = {
|
|
3393
3770
|
CLAUDE_ALARM_SESSION_NAME: projectName
|
|
3394
3771
|
};
|
|
3395
3772
|
if (remote.toLowerCase() === "y") {
|
|
3396
|
-
const host = await
|
|
3397
|
-
const port = await
|
|
3398
|
-
const token = await
|
|
3773
|
+
const host = await ask2("Hub host (e.g. 192.168.1.100): ");
|
|
3774
|
+
const port = await ask2("Hub port (default: 7900): ");
|
|
3775
|
+
const token = await ask2("Hub token: ");
|
|
3399
3776
|
if (!host) {
|
|
3400
3777
|
console.error("Host is required.");
|
|
3401
3778
|
process.exit(1);
|
|
@@ -3404,11 +3781,11 @@ claude-alarm init for "${projectName}"
|
|
|
3404
3781
|
if (port) env.CLAUDE_ALARM_HUB_PORT = port;
|
|
3405
3782
|
if (token) env.CLAUDE_ALARM_HUB_TOKEN = token;
|
|
3406
3783
|
}
|
|
3407
|
-
const mcpPath =
|
|
3784
|
+
const mcpPath = path11.join(dir, ".mcp.json");
|
|
3408
3785
|
let mcpConfig = {};
|
|
3409
|
-
if (
|
|
3786
|
+
if (fs9.existsSync(mcpPath)) {
|
|
3410
3787
|
try {
|
|
3411
|
-
mcpConfig = JSON.parse(
|
|
3788
|
+
mcpConfig = JSON.parse(fs9.readFileSync(mcpPath, "utf-8"));
|
|
3412
3789
|
} catch {
|
|
3413
3790
|
mcpConfig = {};
|
|
3414
3791
|
}
|
|
@@ -3419,7 +3796,7 @@ claude-alarm init for "${projectName}"
|
|
|
3419
3796
|
args: ["-y", "@delt/claude-alarm", "serve"],
|
|
3420
3797
|
env
|
|
3421
3798
|
};
|
|
3422
|
-
|
|
3799
|
+
fs9.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2), "utf-8");
|
|
3423
3800
|
console.log(`
|
|
3424
3801
|
\u2713 Created ${mcpPath}`);
|
|
3425
3802
|
if (remote.toLowerCase() !== "y") {
|
|
@@ -3441,7 +3818,7 @@ claude-alarm init for "${projectName}"
|
|
|
3441
3818
|
}
|
|
3442
3819
|
console.log(` Dashboard: http://${displayHost}:${port}`);
|
|
3443
3820
|
if (shouldOfferCodex(config2, findCodex("codex") !== void 0)) {
|
|
3444
|
-
const answer = await
|
|
3821
|
+
const answer = await ask2("\nCodex is installed. Show Codex conversations on the dashboard too? (y/N): ");
|
|
3445
3822
|
if (answer.toLowerCase() === "y") codexEnable(true);
|
|
3446
3823
|
else setCodexEnabled(false);
|
|
3447
3824
|
}
|
|
@@ -3470,8 +3847,8 @@ function isProcessRunning(pid) {
|
|
|
3470
3847
|
}
|
|
3471
3848
|
}
|
|
3472
3849
|
function readCodexPid() {
|
|
3473
|
-
if (!
|
|
3474
|
-
const pid = parseInt(
|
|
3850
|
+
if (!fs9.existsSync(CODEX_PID_FILE)) return void 0;
|
|
3851
|
+
const pid = parseInt(fs9.readFileSync(CODEX_PID_FILE, "utf-8").trim(), 10);
|
|
3475
3852
|
return Number.isNaN(pid) ? void 0 : pid;
|
|
3476
3853
|
}
|
|
3477
3854
|
function codexEnable(enabled) {
|
|
@@ -3481,21 +3858,15 @@ function codexEnable(enabled) {
|
|
|
3481
3858
|
async function codexStart() {
|
|
3482
3859
|
const config2 = loadConfig();
|
|
3483
3860
|
ensureConfigDir();
|
|
3861
|
+
legacyNote(controlDeps());
|
|
3484
3862
|
const code = await startAdapter({
|
|
3485
3863
|
hub: resolveAdapterHub(config2),
|
|
3486
3864
|
command: config2.codex?.command ?? "codex",
|
|
3487
3865
|
logFile: CODEX_LOG_FILE,
|
|
3488
|
-
|
|
3489
|
-
isRunning: isProcessRunning,
|
|
3490
|
-
removePidFile: () => {
|
|
3491
|
-
try {
|
|
3492
|
-
fs8.unlinkSync(CODEX_PID_FILE);
|
|
3493
|
-
} catch {
|
|
3494
|
-
}
|
|
3495
|
-
},
|
|
3866
|
+
queryOwner: () => queryOwner(controlEndpoint(CONFIG_DIR)),
|
|
3496
3867
|
spawnAdapter: () => {
|
|
3497
|
-
const logFd =
|
|
3498
|
-
return spawn3(process.execPath, [
|
|
3868
|
+
const logFd = fs9.openSync(CODEX_LOG_FILE, "a");
|
|
3869
|
+
return spawn3(process.execPath, [path11.join(__dirname2, "codex", "main.js")], {
|
|
3499
3870
|
detached: true,
|
|
3500
3871
|
stdio: ["ignore", logFd, logFd, "ipc"],
|
|
3501
3872
|
windowsHide: true,
|
|
@@ -3507,28 +3878,25 @@ async function codexStart() {
|
|
|
3507
3878
|
});
|
|
3508
3879
|
if (code !== 0) process.exit(code);
|
|
3509
3880
|
}
|
|
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"}`);
|
|
3881
|
+
function controlDeps() {
|
|
3882
|
+
const endpoint = controlEndpoint(CONFIG_DIR);
|
|
3883
|
+
const token = loadConfig().hub.token ?? "";
|
|
3884
|
+
return {
|
|
3885
|
+
queryOwner: () => queryOwner(endpoint),
|
|
3886
|
+
requestStop: () => requestStop(endpoint, token),
|
|
3887
|
+
legacyPid: readCodexPid,
|
|
3888
|
+
isRunning: isProcessRunning,
|
|
3889
|
+
removeLegacyPidFile: () => {
|
|
3890
|
+
try {
|
|
3891
|
+
fs9.unlinkSync(CODEX_PID_FILE);
|
|
3892
|
+
} catch {
|
|
3893
|
+
}
|
|
3894
|
+
},
|
|
3895
|
+
legacyPidFile: CODEX_PID_FILE,
|
|
3896
|
+
configFile: CONFIG_FILE,
|
|
3897
|
+
out: (line) => console.log(line),
|
|
3898
|
+
err: (line) => console.error(line)
|
|
3899
|
+
};
|
|
3532
3900
|
}
|
|
3533
3901
|
async function codexRun(argv) {
|
|
3534
3902
|
const abort = new AbortController();
|
|
@@ -3537,7 +3905,7 @@ async function codexRun(argv) {
|
|
|
3537
3905
|
process.on("SIGTERM", stop);
|
|
3538
3906
|
try {
|
|
3539
3907
|
return await runFromCli(argv, {
|
|
3540
|
-
readFile: (file) =>
|
|
3908
|
+
readFile: (file) => fs9.promises.readFile(file, "utf-8"),
|
|
3541
3909
|
readStdin: async () => {
|
|
3542
3910
|
let text = "";
|
|
3543
3911
|
for await (const chunk of process.stdin) text += chunk;
|
|
@@ -3588,8 +3956,8 @@ async function main2() {
|
|
|
3588
3956
|
if (sub === "enable") codexEnable(true);
|
|
3589
3957
|
else if (sub === "disable") codexEnable(false);
|
|
3590
3958
|
else if (sub === "start") await codexStart();
|
|
3591
|
-
else if (sub === "stop")
|
|
3592
|
-
else if (sub === "status")
|
|
3959
|
+
else if (sub === "stop") process.exitCode = await stopAdapter(controlDeps(), loadConfig().codex?.enabled === true);
|
|
3960
|
+
else if (sub === "status") process.exitCode = await adapterStatus(controlDeps(), loadConfig().codex?.enabled === true);
|
|
3593
3961
|
else if (sub === "run") process.exitCode = await codexRun(args.slice(2));
|
|
3594
3962
|
else {
|
|
3595
3963
|
console.error(`Unknown codex command: ${sub}`);
|