@delt/claude-alarm 0.9.1 → 1.0.1
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 +24 -0
- package/dist/channel/server.js +41 -22
- package/dist/channel/server.js.map +1 -1
- package/dist/cli.js +354 -130
- package/dist/cli.js.map +1 -1
- package/dist/codex/main.js +778 -0
- package/dist/codex/main.js.map +1 -0
- package/dist/dashboard/index.html +18 -4
- package/dist/hub/server.js +155 -61
- package/dist/hub/server.js.map +1 -1
- package/dist/index.d.ts +24 -2
- package/dist/index.js +201 -88
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/dashboard/index.html +18 -4
package/dist/cli.js
CHANGED
|
@@ -12,7 +12,7 @@ var __export = (target, all) => {
|
|
|
12
12
|
// src/shared/constants.ts
|
|
13
13
|
import path from "path";
|
|
14
14
|
import os from "os";
|
|
15
|
-
var DEFAULT_HUB_HOST, DEFAULT_HUB_PORT, CONFIG_DIR, CONFIG_FILE, PID_FILE, LOG_FILE, UPLOADS_DIR, WS_PATH_CHANNEL, WS_PATH_DASHBOARD, CHANNEL_SERVER_NAME, CHANNEL_SERVER_VERSION;
|
|
15
|
+
var DEFAULT_HUB_HOST, DEFAULT_HUB_PORT, CONFIG_DIR, CONFIG_FILE, PID_FILE, LOG_FILE, UPLOADS_DIR, CODEX_PID_FILE, CODEX_LOG_FILE, WS_PATH_CHANNEL, WS_PATH_DASHBOARD, CHANNEL_SERVER_NAME, CHANNEL_SERVER_VERSION;
|
|
16
16
|
var init_constants = __esm({
|
|
17
17
|
"src/shared/constants.ts"() {
|
|
18
18
|
"use strict";
|
|
@@ -23,6 +23,8 @@ var init_constants = __esm({
|
|
|
23
23
|
PID_FILE = path.join(CONFIG_DIR, "hub.pid");
|
|
24
24
|
LOG_FILE = path.join(CONFIG_DIR, "hub.log");
|
|
25
25
|
UPLOADS_DIR = path.join(CONFIG_DIR, "uploads");
|
|
26
|
+
CODEX_PID_FILE = path.join(CONFIG_DIR, "codex.pid");
|
|
27
|
+
CODEX_LOG_FILE = path.join(CONFIG_DIR, "codex.log");
|
|
26
28
|
WS_PATH_CHANNEL = "/ws/channel";
|
|
27
29
|
WS_PATH_DASHBOARD = "/ws/dashboard";
|
|
28
30
|
CHANNEL_SERVER_NAME = "claude-alarm";
|
|
@@ -67,6 +69,15 @@ function saveConfig(config2) {
|
|
|
67
69
|
ensureConfigDir();
|
|
68
70
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config2, null, 2), { encoding: "utf-8", mode: 384 });
|
|
69
71
|
}
|
|
72
|
+
function setCodexEnabled(enabled) {
|
|
73
|
+
const config2 = loadConfig();
|
|
74
|
+
config2.codex = { ...config2.codex, enabled };
|
|
75
|
+
saveConfig(config2);
|
|
76
|
+
return config2;
|
|
77
|
+
}
|
|
78
|
+
function shouldOfferCodex(config2, installed) {
|
|
79
|
+
return installed && config2.codex === void 0;
|
|
80
|
+
}
|
|
70
81
|
function setupMcpConfig(targetDir) {
|
|
71
82
|
const dir = targetDir ?? process.cwd();
|
|
72
83
|
const mcpPath = path2.join(dir, ".mcp.json");
|
|
@@ -135,11 +146,11 @@ var init_logger = __esm({
|
|
|
135
146
|
});
|
|
136
147
|
|
|
137
148
|
// src/shared/crash-guard.ts
|
|
138
|
-
import
|
|
149
|
+
import fs3 from "fs";
|
|
139
150
|
function appendToLog(text) {
|
|
140
151
|
try {
|
|
141
|
-
|
|
142
|
-
|
|
152
|
+
fs3.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
153
|
+
fs3.appendFileSync(LOG_FILE, text, "utf-8");
|
|
143
154
|
} catch {
|
|
144
155
|
}
|
|
145
156
|
}
|
|
@@ -211,9 +222,9 @@ var init_session_manager = __esm({
|
|
|
211
222
|
SessionManager = class {
|
|
212
223
|
sessions = /* @__PURE__ */ new Map();
|
|
213
224
|
register(session) {
|
|
214
|
-
const baseName =
|
|
225
|
+
const baseName = this.baseName(session);
|
|
215
226
|
const existing = Array.from(this.sessions.values()).filter(
|
|
216
|
-
(s) => s.id !== session.id &&
|
|
227
|
+
(s) => s.id !== session.id && this.baseName(s) === baseName
|
|
217
228
|
);
|
|
218
229
|
if (existing.length > 0) {
|
|
219
230
|
const usedNums = existing.map((s) => {
|
|
@@ -267,6 +278,9 @@ var init_session_manager = __esm({
|
|
|
267
278
|
count() {
|
|
268
279
|
return this.sessions.size;
|
|
269
280
|
}
|
|
281
|
+
baseName(session) {
|
|
282
|
+
return session.title || session.cwd?.replace(/^.*[/\\]/, "") || session.name;
|
|
283
|
+
}
|
|
270
284
|
};
|
|
271
285
|
}
|
|
272
286
|
});
|
|
@@ -294,7 +308,7 @@ var init_notifier = __esm({
|
|
|
294
308
|
async notify(title, message, level = "info") {
|
|
295
309
|
await this.notifyWithSession(void 0, void 0, title, message, level);
|
|
296
310
|
}
|
|
297
|
-
async notifyWithSession(sessionId2,
|
|
311
|
+
async notifyWithSession(sessionId2, sessionLabel2, title, message, level = "info") {
|
|
298
312
|
const promises = [];
|
|
299
313
|
if (this.desktopEnabled) {
|
|
300
314
|
promises.push(this.sendDesktop(title, message, level));
|
|
@@ -302,8 +316,8 @@ var init_notifier = __esm({
|
|
|
302
316
|
for (const webhook of this.webhooks) {
|
|
303
317
|
promises.push(this.sendWebhook(webhook, title, message, level));
|
|
304
318
|
}
|
|
305
|
-
if (this.telegramBot && sessionId2 &&
|
|
306
|
-
promises.push(this.telegramBot.sendNotification(sessionId2,
|
|
319
|
+
if (this.telegramBot && sessionId2 && sessionLabel2) {
|
|
320
|
+
promises.push(this.telegramBot.sendNotification(sessionId2, sessionLabel2, title, message));
|
|
307
321
|
}
|
|
308
322
|
await Promise.allSettled(promises);
|
|
309
323
|
}
|
|
@@ -387,9 +401,20 @@ var init_notifier = __esm({
|
|
|
387
401
|
}
|
|
388
402
|
});
|
|
389
403
|
|
|
404
|
+
// src/shared/session-label.ts
|
|
405
|
+
function sessionLabel(session) {
|
|
406
|
+
const base = session.displayName || session.cwd?.replace(/^.*[/\\]/, "") || session.name;
|
|
407
|
+
return session.agentKind === "codex" ? `Codex \xB7 ${base}` : base;
|
|
408
|
+
}
|
|
409
|
+
var init_session_label = __esm({
|
|
410
|
+
"src/shared/session-label.ts"() {
|
|
411
|
+
"use strict";
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
|
|
390
415
|
// src/hub/telegram.ts
|
|
391
|
-
import
|
|
392
|
-
import
|
|
416
|
+
import fs4 from "fs";
|
|
417
|
+
import path4 from "path";
|
|
393
418
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
394
419
|
var TELEGRAM_API, TelegramBot;
|
|
395
420
|
var init_telegram = __esm({
|
|
@@ -397,6 +422,7 @@ var init_telegram = __esm({
|
|
|
397
422
|
"use strict";
|
|
398
423
|
init_logger();
|
|
399
424
|
init_constants();
|
|
425
|
+
init_session_label();
|
|
400
426
|
TELEGRAM_API = "https://api.telegram.org/bot";
|
|
401
427
|
TelegramBot = class _TelegramBot {
|
|
402
428
|
config;
|
|
@@ -535,15 +561,14 @@ ${this.mdToHtml(message)}`;
|
|
|
535
561
|
const pending = this.pendingMessages.get(msg.chat.id);
|
|
536
562
|
if (pending) {
|
|
537
563
|
this.pendingMessages.delete(msg.chat.id);
|
|
538
|
-
const
|
|
539
|
-
|
|
540
|
-
if (idx >= 0 && idx < sessions2.length) {
|
|
564
|
+
const session = this.pendingSession(pending, parseInt(selectMatch[1], 10) - 1);
|
|
565
|
+
if (session) {
|
|
541
566
|
if (pending.photoFileId) {
|
|
542
|
-
await this.deliverPhotoToSessionByFileId(
|
|
567
|
+
await this.deliverPhotoToSessionByFileId(session.id, pending.photoFileId, pending.caption);
|
|
543
568
|
} else if (pending.text) {
|
|
544
|
-
this.deliverToSession(
|
|
569
|
+
this.deliverToSession(session.id, pending.text);
|
|
545
570
|
}
|
|
546
|
-
this.sendMessage(`Sent to [${this.getLabel(
|
|
571
|
+
this.sendMessage(`Sent to [${this.getLabel(session)}]`);
|
|
547
572
|
} else {
|
|
548
573
|
this.sendMessage("Invalid session number.");
|
|
549
574
|
}
|
|
@@ -564,11 +589,12 @@ ${this.mdToHtml(message)}`;
|
|
|
564
589
|
}
|
|
565
590
|
return;
|
|
566
591
|
}
|
|
592
|
+
const sessionIds = sessions.map((s) => s.id);
|
|
567
593
|
if (hasPhoto) {
|
|
568
594
|
const largest = msg.photo[msg.photo.length - 1];
|
|
569
|
-
this.pendingMessages.set(msg.chat.id, { photoFileId: largest.file_id, caption: text });
|
|
595
|
+
this.pendingMessages.set(msg.chat.id, { photoFileId: largest.file_id, caption: text, sessionIds });
|
|
570
596
|
} else {
|
|
571
|
-
this.pendingMessages.set(msg.chat.id, { text });
|
|
597
|
+
this.pendingMessages.set(msg.chat.id, { text, sessionIds });
|
|
572
598
|
}
|
|
573
599
|
const buttons = sessions.map((s, i) => ({
|
|
574
600
|
text: this.getLabel(s),
|
|
@@ -607,17 +633,17 @@ ${this.mdToHtml(message)}`;
|
|
|
607
633
|
const buffer = Buffer.from(await imgRes.arrayBuffer());
|
|
608
634
|
const ext = fileData.result.file_path.split(".").pop() || "jpg";
|
|
609
635
|
const mimeType = ext === "png" ? "image/png" : ext === "gif" ? "image/gif" : ext === "webp" ? "image/webp" : "image/jpeg";
|
|
610
|
-
|
|
636
|
+
fs4.mkdirSync(UPLOADS_DIR, { recursive: true });
|
|
611
637
|
const filename = `${randomUUID2()}.${ext}`;
|
|
612
|
-
const filePath =
|
|
613
|
-
|
|
638
|
+
const filePath = path4.join(UPLOADS_DIR, filename);
|
|
639
|
+
fs4.writeFileSync(filePath, buffer);
|
|
614
640
|
logger.info(`Telegram photo saved: ${filename} (${buffer.length} bytes)`);
|
|
615
641
|
if (this.onImageToSession) {
|
|
616
642
|
this.onImageToSession(sessionId2, filePath, mimeType, caption);
|
|
617
643
|
}
|
|
618
644
|
setTimeout(() => {
|
|
619
645
|
try {
|
|
620
|
-
|
|
646
|
+
fs4.unlinkSync(filePath);
|
|
621
647
|
} catch {
|
|
622
648
|
}
|
|
623
649
|
}, 5 * 60 * 1e3);
|
|
@@ -625,11 +651,16 @@ ${this.mdToHtml(message)}`;
|
|
|
625
651
|
logger.warn(`Telegram photo download failed: ${err.message}`);
|
|
626
652
|
}
|
|
627
653
|
}
|
|
654
|
+
// Buttons are numbered against the list shown when they were sent; Codex sessions come and go, so the live list may have shifted.
|
|
655
|
+
pendingSession(pending, idx) {
|
|
656
|
+
const id = pending.sessionIds[idx];
|
|
657
|
+
return id ? this.getSessions?.().find((s) => s.id === id) : void 0;
|
|
658
|
+
}
|
|
628
659
|
getLabel(session) {
|
|
629
|
-
return session
|
|
660
|
+
return sessionLabel(session);
|
|
630
661
|
}
|
|
631
662
|
/** Send a permission request with inline buttons */
|
|
632
|
-
async sendPermissionRequest(sessionId2,
|
|
663
|
+
async sendPermissionRequest(sessionId2, sessionLabel2, requestId, toolName, description, inputPreview) {
|
|
633
664
|
const isNotify = toolName.endsWith("__notify") || toolName === "notify";
|
|
634
665
|
let preview = inputPreview;
|
|
635
666
|
let kind = "code";
|
|
@@ -682,7 +713,7 @@ ${this.mdToHtml(message)}`;
|
|
|
682
713
|
const mcpMatch = toolName.match(/__([^_]+)$/);
|
|
683
714
|
if (mcpMatch) displayTool = mcpMatch[1].charAt(0).toUpperCase() + mcpMatch[1].slice(1);
|
|
684
715
|
}
|
|
685
|
-
const text = `\u26A0\uFE0F <b>Permission Request</b> \u2014 ${this.escHtml(
|
|
716
|
+
const text = `\u26A0\uFE0F <b>Permission Request</b> \u2014 ${this.escHtml(sessionLabel2)}
|
|
686
717
|
|
|
687
718
|
\u{1F527} <b>${this.escHtml(displayTool)}</b>
|
|
688
719
|
${previewHtml}${truncNote}`;
|
|
@@ -722,22 +753,18 @@ ${label}`);
|
|
|
722
753
|
const parts = query.data.split(":");
|
|
723
754
|
if (parts.length < 3) return;
|
|
724
755
|
const [, idxStr, chatIdStr] = parts;
|
|
725
|
-
const idx = parseInt(idxStr, 10);
|
|
726
756
|
const chatId = parseInt(chatIdStr, 10);
|
|
727
|
-
const
|
|
728
|
-
|
|
757
|
+
const pending = this.pendingMessages.get(chatId);
|
|
758
|
+
const session = pending ? this.pendingSession(pending, parseInt(idxStr, 10)) : void 0;
|
|
759
|
+
if (!pending || !session) {
|
|
729
760
|
await this.answerCallbackQuery(query.id, "Session not found");
|
|
730
761
|
return;
|
|
731
762
|
}
|
|
732
|
-
const session = sessions[idx];
|
|
733
|
-
const pending = this.pendingMessages.get(chatId);
|
|
734
763
|
this.pendingMessages.delete(chatId);
|
|
735
|
-
if (pending) {
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
this.deliverToSession(session.id, pending.text);
|
|
740
|
-
}
|
|
764
|
+
if (pending.photoFileId) {
|
|
765
|
+
await this.deliverPhotoToSessionByFileId(session.id, pending.photoFileId, pending.caption);
|
|
766
|
+
} else if (pending.text) {
|
|
767
|
+
this.deliverToSession(session.id, pending.text);
|
|
741
768
|
}
|
|
742
769
|
await this.answerCallbackQuery(query.id, `Sent to ${this.getLabel(session)}`);
|
|
743
770
|
if (query.message) {
|
|
@@ -799,8 +826,8 @@ ${label}`);
|
|
|
799
826
|
css: "css",
|
|
800
827
|
scss: "scss"
|
|
801
828
|
};
|
|
802
|
-
langOf(
|
|
803
|
-
const m = /\.([a-z0-9]+)$/i.exec(
|
|
829
|
+
langOf(path10) {
|
|
830
|
+
const m = /\.([a-z0-9]+)$/i.exec(path10 || "");
|
|
804
831
|
return m ? _TelegramBot.LANG_BY_EXT[m[1].toLowerCase()] ?? "" : "";
|
|
805
832
|
}
|
|
806
833
|
/** Convert markdown to Telegram HTML (escape first, then apply formatting) */
|
|
@@ -845,6 +872,77 @@ ${bodyLines.join("\n")}`;
|
|
|
845
872
|
}
|
|
846
873
|
});
|
|
847
874
|
|
|
875
|
+
// src/hub/codex-supervisor.ts
|
|
876
|
+
import { spawn as spawn2 } from "child_process";
|
|
877
|
+
import fs5 from "fs";
|
|
878
|
+
import path5 from "path";
|
|
879
|
+
function resolveAdapterScript(baseDir) {
|
|
880
|
+
return [path5.join(baseDir, "..", "codex", "main.js"), path5.join(baseDir, "codex", "main.js")].find((p) => fs5.existsSync(p));
|
|
881
|
+
}
|
|
882
|
+
var HEALTHY_RUN_MS, CodexSupervisor;
|
|
883
|
+
var init_codex_supervisor = __esm({
|
|
884
|
+
"src/hub/codex-supervisor.ts"() {
|
|
885
|
+
"use strict";
|
|
886
|
+
init_logger();
|
|
887
|
+
HEALTHY_RUN_MS = 6e4;
|
|
888
|
+
CodexSupervisor = class {
|
|
889
|
+
constructor(script, spawnFn = spawn2, minDelayMs = 2e3, maxDelayMs = 6e4, stopGraceMs = 3e3) {
|
|
890
|
+
this.script = script;
|
|
891
|
+
this.spawnFn = spawnFn;
|
|
892
|
+
this.minDelayMs = minDelayMs;
|
|
893
|
+
this.maxDelayMs = maxDelayMs;
|
|
894
|
+
this.stopGraceMs = stopGraceMs;
|
|
895
|
+
this.delay = minDelayMs;
|
|
896
|
+
}
|
|
897
|
+
child;
|
|
898
|
+
timer;
|
|
899
|
+
stopped = true;
|
|
900
|
+
delay;
|
|
901
|
+
start() {
|
|
902
|
+
this.stopped = false;
|
|
903
|
+
this.launch();
|
|
904
|
+
}
|
|
905
|
+
stop() {
|
|
906
|
+
this.stopped = true;
|
|
907
|
+
if (this.timer) clearTimeout(this.timer);
|
|
908
|
+
this.timer = void 0;
|
|
909
|
+
const child = this.child;
|
|
910
|
+
this.child = void 0;
|
|
911
|
+
if (!child) return;
|
|
912
|
+
child.stdin?.end();
|
|
913
|
+
const force = setTimeout(() => child.kill(), this.stopGraceMs);
|
|
914
|
+
force.unref();
|
|
915
|
+
child.once("exit", () => clearTimeout(force));
|
|
916
|
+
}
|
|
917
|
+
launch() {
|
|
918
|
+
const startedAt = Date.now();
|
|
919
|
+
const child = this.spawnFn(process.execPath, [this.script, "--watch-stdin"], {
|
|
920
|
+
detached: true,
|
|
921
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
922
|
+
windowsHide: true
|
|
923
|
+
});
|
|
924
|
+
this.child = child;
|
|
925
|
+
child.stdout?.on("data", (d) => process.stdout.write(d));
|
|
926
|
+
child.stderr?.on("data", (d) => process.stderr.write(d));
|
|
927
|
+
child.stdin?.on("error", () => {
|
|
928
|
+
});
|
|
929
|
+
child.on("error", (err) => logger.warn(`Codex adapter failed to start: ${err.message}`));
|
|
930
|
+
child.on("exit", (code, signal) => {
|
|
931
|
+
if (this.child === child) this.child = void 0;
|
|
932
|
+
if (this.stopped || code === 0) return;
|
|
933
|
+
if (Date.now() - startedAt > HEALTHY_RUN_MS) this.delay = this.minDelayMs;
|
|
934
|
+
logger.warn(`Codex adapter exited (${signal ?? code}); restarting in ${this.delay}ms`);
|
|
935
|
+
this.timer = setTimeout(() => {
|
|
936
|
+
this.timer = void 0;
|
|
937
|
+
if (!this.stopped) this.launch();
|
|
938
|
+
}, this.delay);
|
|
939
|
+
this.delay = Math.min(this.delay * 2, this.maxDelayMs);
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
});
|
|
945
|
+
|
|
848
946
|
// src/hub/auth.ts
|
|
849
947
|
import crypto from "crypto";
|
|
850
948
|
function safeEqual(a, b) {
|
|
@@ -914,10 +1012,10 @@ __export(server_exports, {
|
|
|
914
1012
|
HubServer: () => HubServer
|
|
915
1013
|
});
|
|
916
1014
|
import http from "http";
|
|
917
|
-
import
|
|
918
|
-
import
|
|
1015
|
+
import fs6 from "fs";
|
|
1016
|
+
import path6 from "path";
|
|
919
1017
|
import { fileURLToPath } from "url";
|
|
920
|
-
import { WebSocketServer, WebSocket } from "ws";
|
|
1018
|
+
import { WebSocketServer, WebSocket as WebSocket2 } from "ws";
|
|
921
1019
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
922
1020
|
var __dirname, MAX_WS_PAYLOAD, HubServer;
|
|
923
1021
|
var init_server = __esm({
|
|
@@ -928,10 +1026,12 @@ var init_server = __esm({
|
|
|
928
1026
|
init_session_manager();
|
|
929
1027
|
init_notifier();
|
|
930
1028
|
init_telegram();
|
|
1029
|
+
init_codex_supervisor();
|
|
931
1030
|
init_config();
|
|
1031
|
+
init_session_label();
|
|
932
1032
|
init_crash_guard();
|
|
933
1033
|
init_auth();
|
|
934
|
-
__dirname =
|
|
1034
|
+
__dirname = path6.dirname(fileURLToPath(import.meta.url));
|
|
935
1035
|
MAX_WS_PAYLOAD = 16 * 1024 * 1024;
|
|
936
1036
|
HubServer = class {
|
|
937
1037
|
httpServer;
|
|
@@ -954,10 +1054,13 @@ var init_server = __esm({
|
|
|
954
1054
|
host;
|
|
955
1055
|
port;
|
|
956
1056
|
token;
|
|
1057
|
+
codexEnabled;
|
|
1058
|
+
codexSupervisor;
|
|
957
1059
|
constructor(config2) {
|
|
958
1060
|
this.host = config2?.hub?.host ?? DEFAULT_HUB_HOST;
|
|
959
1061
|
this.port = config2?.hub?.port ?? DEFAULT_HUB_PORT;
|
|
960
1062
|
this.token = config2?.hub?.token;
|
|
1063
|
+
this.codexEnabled = config2?.codex?.enabled === true;
|
|
961
1064
|
if (config2?.notifications) {
|
|
962
1065
|
this.notifier.configure({
|
|
963
1066
|
desktop: config2.notifications.desktop
|
|
@@ -1015,12 +1118,15 @@ var init_server = __esm({
|
|
|
1015
1118
|
this.httpServer.on("error", (err) => logger.error(`HTTP server error: ${err.message}`));
|
|
1016
1119
|
const displayHost = this.host === "0.0.0.0" ? "127.0.0.1" : this.host;
|
|
1017
1120
|
logger.info(`Hub server listening on http://${displayHost}:${this.port}`);
|
|
1121
|
+
this.startCodexAdapter();
|
|
1018
1122
|
resolve();
|
|
1019
1123
|
});
|
|
1020
1124
|
});
|
|
1021
1125
|
}
|
|
1022
1126
|
stop() {
|
|
1023
1127
|
return new Promise((resolve) => {
|
|
1128
|
+
this.codexSupervisor?.stop();
|
|
1129
|
+
this.codexSupervisor = void 0;
|
|
1024
1130
|
if (this.heartbeatInterval) clearInterval(this.heartbeatInterval);
|
|
1025
1131
|
if (this.telegramBot) this.telegramBot.stopPolling();
|
|
1026
1132
|
for (const ws of this.channelSockets.values()) ws.terminate();
|
|
@@ -1121,23 +1227,23 @@ var init_server = __esm({
|
|
|
1121
1227
|
return;
|
|
1122
1228
|
}
|
|
1123
1229
|
const candidates = [
|
|
1124
|
-
|
|
1230
|
+
path6.join(__dirname, "..", "dashboard", "index.html"),
|
|
1125
1231
|
// from dist/hub/
|
|
1126
|
-
|
|
1232
|
+
path6.join(__dirname, "dashboard", "index.html"),
|
|
1127
1233
|
// from dist/ (bundled index.js)
|
|
1128
|
-
|
|
1234
|
+
path6.join(__dirname, "..", "..", "src", "dashboard", "index.html"),
|
|
1129
1235
|
// from dist/hub/ -> src/
|
|
1130
|
-
|
|
1236
|
+
path6.join(__dirname, "..", "src", "dashboard", "index.html"),
|
|
1131
1237
|
// from dist/ -> src/
|
|
1132
|
-
|
|
1238
|
+
path6.join(process.cwd(), "dist", "dashboard", "index.html"),
|
|
1133
1239
|
// from cwd
|
|
1134
|
-
|
|
1240
|
+
path6.join(process.cwd(), "src", "dashboard", "index.html")
|
|
1135
1241
|
// from cwd/src
|
|
1136
1242
|
];
|
|
1137
1243
|
logger.debug(`Dashboard candidates: ${JSON.stringify(candidates)}`);
|
|
1138
1244
|
for (const candidate of candidates) {
|
|
1139
|
-
if (
|
|
1140
|
-
const html =
|
|
1245
|
+
if (fs6.existsSync(candidate)) {
|
|
1246
|
+
const html = fs6.readFileSync(candidate, "utf-8");
|
|
1141
1247
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
1142
1248
|
res.end(html);
|
|
1143
1249
|
return;
|
|
@@ -1172,11 +1278,11 @@ var init_server = __esm({
|
|
|
1172
1278
|
return;
|
|
1173
1279
|
}
|
|
1174
1280
|
const ws = this.channelSockets.get(sessionId2);
|
|
1175
|
-
if (!ws || ws.readyState !==
|
|
1281
|
+
if (!ws || ws.readyState !== WebSocket2.OPEN) {
|
|
1176
1282
|
this.jsonResponse(res, 404, { error: "Session not connected" });
|
|
1177
1283
|
return;
|
|
1178
1284
|
}
|
|
1179
|
-
const msg = { type: "message_to_session", sessionId: sessionId2, content };
|
|
1285
|
+
const msg = { type: "message_to_session", sessionId: sessionId2, content, source: "api" };
|
|
1180
1286
|
ws.send(JSON.stringify(msg));
|
|
1181
1287
|
this.jsonResponse(res, 200, { ok: true });
|
|
1182
1288
|
}
|
|
@@ -1349,14 +1455,14 @@ var init_server = __esm({
|
|
|
1349
1455
|
const msg = JSON.parse(data.toString());
|
|
1350
1456
|
if (msg.type === "message_to_session") {
|
|
1351
1457
|
const channelWs = this.channelSockets.get(msg.sessionId);
|
|
1352
|
-
if (channelWs?.readyState ===
|
|
1353
|
-
channelWs.send(JSON.stringify(msg));
|
|
1458
|
+
if (channelWs?.readyState === WebSocket2.OPEN) {
|
|
1459
|
+
channelWs.send(JSON.stringify({ ...msg, source: "dashboard" }));
|
|
1354
1460
|
}
|
|
1355
1461
|
} else if (msg.type === "image_upload") {
|
|
1356
1462
|
this.handleImageUpload(msg);
|
|
1357
1463
|
} else if (msg.type === "permission_response") {
|
|
1358
1464
|
const channelWs = this.channelSockets.get(msg.sessionId);
|
|
1359
|
-
if (channelWs?.readyState ===
|
|
1465
|
+
if (channelWs?.readyState === WebSocket2.OPEN) {
|
|
1360
1466
|
channelWs.send(JSON.stringify(msg));
|
|
1361
1467
|
logger.info(`Permission verdict [${msg.requestId}]: ${msg.behavior} -> session ${msg.sessionId}`);
|
|
1362
1468
|
}
|
|
@@ -1374,7 +1480,7 @@ var init_server = __esm({
|
|
|
1374
1480
|
broadcastToDashboards(msg) {
|
|
1375
1481
|
const payload = JSON.stringify(msg);
|
|
1376
1482
|
for (const ws of this.dashboardSockets) {
|
|
1377
|
-
if (ws.readyState ===
|
|
1483
|
+
if (ws.readyState === WebSocket2.OPEN) {
|
|
1378
1484
|
ws.send(payload);
|
|
1379
1485
|
}
|
|
1380
1486
|
}
|
|
@@ -1386,7 +1492,7 @@ var init_server = __esm({
|
|
|
1386
1492
|
return;
|
|
1387
1493
|
}
|
|
1388
1494
|
const channelWs = this.channelSockets.get(sessionId2);
|
|
1389
|
-
if (!channelWs || channelWs.readyState !==
|
|
1495
|
+
if (!channelWs || channelWs.readyState !== WebSocket2.OPEN) return;
|
|
1390
1496
|
const allowedTypes = ["image/png", "image/jpeg", "image/gif", "image/webp"];
|
|
1391
1497
|
if (!allowedTypes.includes(mimeType)) return;
|
|
1392
1498
|
const base64Data = imageData.replace(/^data:image\/\w+;base64,/, "");
|
|
@@ -1395,11 +1501,11 @@ var init_server = __esm({
|
|
|
1395
1501
|
logger.warn("Image upload rejected: exceeds 10MB");
|
|
1396
1502
|
return;
|
|
1397
1503
|
}
|
|
1398
|
-
|
|
1504
|
+
fs6.mkdirSync(UPLOADS_DIR, { recursive: true });
|
|
1399
1505
|
const ext = mimeType.split("/")[1] === "jpeg" ? "jpg" : mimeType.split("/")[1];
|
|
1400
1506
|
const filename = `${randomUUID3()}.${ext}`;
|
|
1401
|
-
const filePath =
|
|
1402
|
-
|
|
1507
|
+
const filePath = path6.join(UPLOADS_DIR, filename);
|
|
1508
|
+
fs6.writeFileSync(filePath, buffer);
|
|
1403
1509
|
const forwardMsg = {
|
|
1404
1510
|
type: "image_to_session",
|
|
1405
1511
|
sessionId: sessionId2,
|
|
@@ -1412,7 +1518,7 @@ var init_server = __esm({
|
|
|
1412
1518
|
logger.info(`Image saved and forwarded: ${filename} (${buffer.length} bytes)`);
|
|
1413
1519
|
setTimeout(() => {
|
|
1414
1520
|
try {
|
|
1415
|
-
|
|
1521
|
+
fs6.unlinkSync(filePath);
|
|
1416
1522
|
} catch {
|
|
1417
1523
|
}
|
|
1418
1524
|
}, 5 * 60 * 1e3);
|
|
@@ -1439,15 +1545,15 @@ var init_server = __esm({
|
|
|
1439
1545
|
this.telegramBot.getSessions = () => this.sessions.getAll();
|
|
1440
1546
|
this.telegramBot.onMessageToSession = (sessionId2, content) => {
|
|
1441
1547
|
const channelWs = this.channelSockets.get(sessionId2);
|
|
1442
|
-
if (channelWs?.readyState ===
|
|
1443
|
-
const msg = { type: "message_to_session", sessionId: sessionId2, content };
|
|
1548
|
+
if (channelWs?.readyState === WebSocket2.OPEN) {
|
|
1549
|
+
const msg = { type: "message_to_session", sessionId: sessionId2, content, source: "telegram" };
|
|
1444
1550
|
channelWs.send(JSON.stringify(msg));
|
|
1445
1551
|
logger.info(`Telegram message forwarded to session: ${sessionId2}`);
|
|
1446
1552
|
}
|
|
1447
1553
|
};
|
|
1448
1554
|
this.telegramBot.onImageToSession = (sessionId2, imagePath, mimeType, caption) => {
|
|
1449
1555
|
const channelWs = this.channelSockets.get(sessionId2);
|
|
1450
|
-
if (channelWs?.readyState ===
|
|
1556
|
+
if (channelWs?.readyState === WebSocket2.OPEN) {
|
|
1451
1557
|
const msg = { type: "image_to_session", sessionId: sessionId2, imagePath, mimeType, content: caption };
|
|
1452
1558
|
channelWs.send(JSON.stringify(msg));
|
|
1453
1559
|
logger.info(`Telegram photo forwarded to session: ${sessionId2}`);
|
|
@@ -1455,7 +1561,7 @@ var init_server = __esm({
|
|
|
1455
1561
|
};
|
|
1456
1562
|
this.telegramBot.onPermissionVerdict = (sessionId2, requestId, behavior) => {
|
|
1457
1563
|
const channelWs = this.channelSockets.get(sessionId2);
|
|
1458
|
-
if (channelWs?.readyState ===
|
|
1564
|
+
if (channelWs?.readyState === WebSocket2.OPEN) {
|
|
1459
1565
|
const msg = { type: "permission_response", sessionId: sessionId2, requestId, behavior };
|
|
1460
1566
|
channelWs.send(JSON.stringify(msg));
|
|
1461
1567
|
logger.info(`Telegram permission verdict [${requestId}]: ${behavior} -> session ${sessionId2}`);
|
|
@@ -1567,6 +1673,17 @@ var init_server = __esm({
|
|
|
1567
1673
|
this.jsonResponse(res, 500, { error: err.message });
|
|
1568
1674
|
}
|
|
1569
1675
|
}
|
|
1676
|
+
startCodexAdapter() {
|
|
1677
|
+
if (!this.codexEnabled) return;
|
|
1678
|
+
const script = resolveAdapterScript(__dirname);
|
|
1679
|
+
if (!script) {
|
|
1680
|
+
logger.warn("Codex adapter is enabled but codex/main.js was not found next to the hub");
|
|
1681
|
+
return;
|
|
1682
|
+
}
|
|
1683
|
+
this.codexSupervisor = new CodexSupervisor(script);
|
|
1684
|
+
this.codexSupervisor.start();
|
|
1685
|
+
logger.info("Codex adapter started");
|
|
1686
|
+
}
|
|
1570
1687
|
startHeartbeat() {
|
|
1571
1688
|
this.heartbeatInterval = setInterval(() => {
|
|
1572
1689
|
for (const [sessionId2, ws] of this.channelSockets) {
|
|
@@ -1582,11 +1699,11 @@ var init_server = __esm({
|
|
|
1582
1699
|
}
|
|
1583
1700
|
cleanupUploads() {
|
|
1584
1701
|
try {
|
|
1585
|
-
if (!
|
|
1586
|
-
const files =
|
|
1702
|
+
if (!fs6.existsSync(UPLOADS_DIR)) return;
|
|
1703
|
+
const files = fs6.readdirSync(UPLOADS_DIR);
|
|
1587
1704
|
for (const file of files) {
|
|
1588
1705
|
try {
|
|
1589
|
-
|
|
1706
|
+
fs6.unlinkSync(path6.join(UPLOADS_DIR, file));
|
|
1590
1707
|
} catch {
|
|
1591
1708
|
}
|
|
1592
1709
|
}
|
|
@@ -1595,8 +1712,7 @@ var init_server = __esm({
|
|
|
1595
1712
|
}
|
|
1596
1713
|
}
|
|
1597
1714
|
getSessionLabel(session) {
|
|
1598
|
-
|
|
1599
|
-
return session.displayName || session.cwd?.replace(/^.*[/\\]/, "") || session.name;
|
|
1715
|
+
return session ? sessionLabel(session) : "unknown";
|
|
1600
1716
|
}
|
|
1601
1717
|
jsonResponse(res, status, body) {
|
|
1602
1718
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
@@ -1660,7 +1776,7 @@ Content-Length: 0\r
|
|
|
1660
1776
|
});
|
|
1661
1777
|
|
|
1662
1778
|
// src/channel/hub-client.ts
|
|
1663
|
-
import
|
|
1779
|
+
import WebSocket3 from "ws";
|
|
1664
1780
|
var HubClient;
|
|
1665
1781
|
var init_hub_client = __esm({
|
|
1666
1782
|
"src/channel/hub-client.ts"() {
|
|
@@ -1668,48 +1784,41 @@ var init_hub_client = __esm({
|
|
|
1668
1784
|
init_logger();
|
|
1669
1785
|
init_constants();
|
|
1670
1786
|
HubClient = class {
|
|
1671
|
-
constructor(sessionId2, sessionName2, hubHost2 = DEFAULT_HUB_HOST, hubPort2 = DEFAULT_HUB_PORT, token, getPeerName = () => void 0) {
|
|
1787
|
+
constructor(sessionId2, sessionName2, hubHost2 = DEFAULT_HUB_HOST, hubPort2 = DEFAULT_HUB_PORT, token, getPeerName = () => void 0, getRegistration = () => ({})) {
|
|
1672
1788
|
this.sessionId = sessionId2;
|
|
1673
1789
|
this.sessionName = sessionName2;
|
|
1674
1790
|
this.hubHost = hubHost2;
|
|
1675
1791
|
this.hubPort = hubPort2;
|
|
1676
1792
|
this.token = token;
|
|
1677
1793
|
this.getPeerName = getPeerName;
|
|
1794
|
+
this.getRegistration = getRegistration;
|
|
1678
1795
|
}
|
|
1679
1796
|
ws = null;
|
|
1680
1797
|
reconnectTimer = null;
|
|
1681
1798
|
messageHandlers = [];
|
|
1682
1799
|
queue = [];
|
|
1683
1800
|
connected = false;
|
|
1801
|
+
closed = false;
|
|
1684
1802
|
connect() {
|
|
1803
|
+
this.closed = false;
|
|
1685
1804
|
const tokenQuery = this.token ? `?token=${encodeURIComponent(this.token)}` : "";
|
|
1686
1805
|
const url = `ws://${this.hubHost}:${this.hubPort}${WS_PATH_CHANNEL}${tokenQuery}`;
|
|
1687
1806
|
logger.debug(`Connecting to hub at ${url}`);
|
|
1688
1807
|
try {
|
|
1689
|
-
|
|
1690
|
-
this.ws
|
|
1808
|
+
const ws = new WebSocket3(url);
|
|
1809
|
+
this.ws = ws;
|
|
1810
|
+
ws.on("open", () => {
|
|
1811
|
+
if (this.ws !== ws) return;
|
|
1691
1812
|
logger.info("Connected to hub");
|
|
1692
1813
|
this.connected = true;
|
|
1693
|
-
|
|
1694
|
-
type: "register",
|
|
1695
|
-
session: {
|
|
1696
|
-
id: this.sessionId,
|
|
1697
|
-
name: this.sessionName,
|
|
1698
|
-
status: "idle",
|
|
1699
|
-
connectedAt: Date.now(),
|
|
1700
|
-
lastActivity: Date.now(),
|
|
1701
|
-
cwd: process.cwd(),
|
|
1702
|
-
channelEnabled: true,
|
|
1703
|
-
peerName: this.getPeerName()
|
|
1704
|
-
}
|
|
1705
|
-
};
|
|
1706
|
-
this.ws.send(JSON.stringify(registration));
|
|
1814
|
+
this.ws.send(JSON.stringify(this.registration()));
|
|
1707
1815
|
for (const msg of this.queue) {
|
|
1708
1816
|
this.ws.send(JSON.stringify(msg));
|
|
1709
1817
|
}
|
|
1710
1818
|
this.queue = [];
|
|
1711
1819
|
});
|
|
1712
|
-
|
|
1820
|
+
ws.on("message", (data) => {
|
|
1821
|
+
if (this.ws !== ws) return;
|
|
1713
1822
|
try {
|
|
1714
1823
|
const msg = JSON.parse(data.toString());
|
|
1715
1824
|
for (const handler of this.messageHandlers) {
|
|
@@ -1719,12 +1828,14 @@ var init_hub_client = __esm({
|
|
|
1719
1828
|
logger.warn("Failed to parse hub message:", err);
|
|
1720
1829
|
}
|
|
1721
1830
|
});
|
|
1722
|
-
|
|
1831
|
+
ws.on("close", () => {
|
|
1832
|
+
if (this.ws !== ws) return;
|
|
1723
1833
|
logger.info("Disconnected from hub");
|
|
1724
1834
|
this.connected = false;
|
|
1725
|
-
this.scheduleReconnect();
|
|
1835
|
+
if (!this.closed) this.scheduleReconnect();
|
|
1726
1836
|
});
|
|
1727
|
-
|
|
1837
|
+
ws.on("error", (err) => {
|
|
1838
|
+
if (this.ws !== ws) return;
|
|
1728
1839
|
logger.debug(`Hub connection error: ${err.message}`);
|
|
1729
1840
|
this.connected = false;
|
|
1730
1841
|
});
|
|
@@ -1734,7 +1845,7 @@ var init_hub_client = __esm({
|
|
|
1734
1845
|
}
|
|
1735
1846
|
}
|
|
1736
1847
|
send(msg) {
|
|
1737
|
-
if (this.connected && this.ws?.readyState ===
|
|
1848
|
+
if (this.connected && this.ws?.readyState === WebSocket3.OPEN) {
|
|
1738
1849
|
this.ws.send(JSON.stringify(msg));
|
|
1739
1850
|
} else {
|
|
1740
1851
|
if (this.queue.length < 100) {
|
|
@@ -1747,6 +1858,7 @@ var init_hub_client = __esm({
|
|
|
1747
1858
|
this.messageHandlers.push(handler);
|
|
1748
1859
|
}
|
|
1749
1860
|
disconnect() {
|
|
1861
|
+
this.closed = true;
|
|
1750
1862
|
if (this.reconnectTimer) {
|
|
1751
1863
|
clearTimeout(this.reconnectTimer);
|
|
1752
1864
|
this.reconnectTimer = null;
|
|
@@ -1757,8 +1869,29 @@ var init_hub_client = __esm({
|
|
|
1757
1869
|
}
|
|
1758
1870
|
this.connected = false;
|
|
1759
1871
|
}
|
|
1872
|
+
reregister() {
|
|
1873
|
+
if (this.connected && this.ws?.readyState === WebSocket3.OPEN) {
|
|
1874
|
+
this.ws.send(JSON.stringify(this.registration()));
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
registration() {
|
|
1878
|
+
return {
|
|
1879
|
+
type: "register",
|
|
1880
|
+
session: {
|
|
1881
|
+
id: this.sessionId,
|
|
1882
|
+
name: this.sessionName,
|
|
1883
|
+
status: "idle",
|
|
1884
|
+
connectedAt: Date.now(),
|
|
1885
|
+
lastActivity: Date.now(),
|
|
1886
|
+
cwd: process.cwd(),
|
|
1887
|
+
channelEnabled: true,
|
|
1888
|
+
peerName: this.getPeerName(),
|
|
1889
|
+
...this.getRegistration()
|
|
1890
|
+
}
|
|
1891
|
+
};
|
|
1892
|
+
}
|
|
1760
1893
|
scheduleReconnect() {
|
|
1761
|
-
if (this.reconnectTimer) return;
|
|
1894
|
+
if (this.reconnectTimer || this.closed) return;
|
|
1762
1895
|
this.reconnectTimer = setTimeout(() => {
|
|
1763
1896
|
this.reconnectTimer = null;
|
|
1764
1897
|
this.connect();
|
|
@@ -1769,9 +1902,9 @@ var init_hub_client = __esm({
|
|
|
1769
1902
|
});
|
|
1770
1903
|
|
|
1771
1904
|
// src/channel/peer-name.ts
|
|
1772
|
-
import
|
|
1905
|
+
import fs7 from "fs";
|
|
1773
1906
|
import os2 from "os";
|
|
1774
|
-
import
|
|
1907
|
+
import path7 from "path";
|
|
1775
1908
|
function findPeerName(records, env) {
|
|
1776
1909
|
const valid = records.filter((r) => typeof r === "object" && r !== null);
|
|
1777
1910
|
const nameOf = (match) => {
|
|
@@ -1791,18 +1924,18 @@ function readPeerName(env = process.env) {
|
|
|
1791
1924
|
sessionId: env.CLAUDE_CODE_SESSION_ID
|
|
1792
1925
|
};
|
|
1793
1926
|
if (!lookup.messagingSocket && !lookup.sessionId) return void 0;
|
|
1794
|
-
const configDir = env.CLAUDE_CONFIG_DIR ||
|
|
1795
|
-
const dir =
|
|
1927
|
+
const configDir = env.CLAUDE_CONFIG_DIR || path7.join(os2.homedir(), ".claude");
|
|
1928
|
+
const dir = path7.join(configDir, "sessions");
|
|
1796
1929
|
let files;
|
|
1797
1930
|
try {
|
|
1798
|
-
files =
|
|
1931
|
+
files = fs7.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
1799
1932
|
} catch {
|
|
1800
1933
|
return void 0;
|
|
1801
1934
|
}
|
|
1802
1935
|
const records = [];
|
|
1803
1936
|
for (const f of files) {
|
|
1804
1937
|
try {
|
|
1805
|
-
records.push(JSON.parse(
|
|
1938
|
+
records.push(JSON.parse(fs7.readFileSync(path7.join(dir, f), "utf8")));
|
|
1806
1939
|
} catch {
|
|
1807
1940
|
}
|
|
1808
1941
|
}
|
|
@@ -1825,7 +1958,7 @@ import {
|
|
|
1825
1958
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
1826
1959
|
import { z } from "zod";
|
|
1827
1960
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
1828
|
-
import
|
|
1961
|
+
import path8 from "path";
|
|
1829
1962
|
async function main() {
|
|
1830
1963
|
logger.info(`Starting MCP channel server (session: ${sessionId})`);
|
|
1831
1964
|
hubClient.connect();
|
|
@@ -1894,7 +2027,7 @@ var init_server2 = __esm({
|
|
|
1894
2027
|
init_hub_client();
|
|
1895
2028
|
init_peer_name2();
|
|
1896
2029
|
sessionId = randomUUID4();
|
|
1897
|
-
sessionName = process.env.CLAUDE_ALARM_SESSION_NAME ??
|
|
2030
|
+
sessionName = process.env.CLAUDE_ALARM_SESSION_NAME ?? path8.basename(process.cwd());
|
|
1898
2031
|
peerName = readPeerName();
|
|
1899
2032
|
server = new Server(
|
|
1900
2033
|
{
|
|
@@ -2070,15 +2203,35 @@ NOTIFICATIONS:
|
|
|
2070
2203
|
|
|
2071
2204
|
// src/cli.ts
|
|
2072
2205
|
init_config();
|
|
2206
|
+
import { spawn as spawn3 } from "child_process";
|
|
2207
|
+
import fs8 from "fs";
|
|
2208
|
+
import path9 from "path";
|
|
2209
|
+
import readline from "readline";
|
|
2210
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2211
|
+
|
|
2212
|
+
// src/codex/transport.ts
|
|
2213
|
+
init_logger();
|
|
2214
|
+
import { spawn } from "child_process";
|
|
2215
|
+
import fs2 from "fs";
|
|
2216
|
+
import path3 from "path";
|
|
2217
|
+
import { Duplex } from "stream";
|
|
2218
|
+
import WebSocket from "ws";
|
|
2219
|
+
function findOnPath(command, platform = process.platform, env = process.env) {
|
|
2220
|
+
const dirs = (env.PATH ?? env.Path ?? "").split(path3.delimiter).filter(Boolean);
|
|
2221
|
+
for (const ext of platform === "win32" ? [".exe", ".cmd"] : [""]) {
|
|
2222
|
+
for (const dir of dirs) {
|
|
2223
|
+
const file = path3.join(dir, command + ext);
|
|
2224
|
+
if (fs2.existsSync(file)) return file;
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
return void 0;
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
// src/cli.ts
|
|
2073
2231
|
init_constants();
|
|
2074
2232
|
init_logger();
|
|
2075
2233
|
init_crash_guard();
|
|
2076
|
-
|
|
2077
|
-
import fs6 from "fs";
|
|
2078
|
-
import path7 from "path";
|
|
2079
|
-
import readline from "readline";
|
|
2080
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2081
|
-
var __dirname2 = path7.dirname(fileURLToPath2(import.meta.url));
|
|
2234
|
+
var __dirname2 = path9.dirname(fileURLToPath2(import.meta.url));
|
|
2082
2235
|
function authHeaders(token) {
|
|
2083
2236
|
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
2084
2237
|
}
|
|
@@ -2097,6 +2250,11 @@ Usage:
|
|
|
2097
2250
|
claude-alarm setup [dir] Add claude-alarm to .mcp.json
|
|
2098
2251
|
claude-alarm test Send a test notification
|
|
2099
2252
|
claude-alarm token Show current auth token
|
|
2253
|
+
claude-alarm codex enable Start the Codex adapter together with the hub
|
|
2254
|
+
claude-alarm codex disable Stop starting the Codex adapter with the hub
|
|
2255
|
+
claude-alarm codex start Run the Codex adapter on its own (e.g. Codex on another PC)
|
|
2256
|
+
claude-alarm codex stop Stop a running Codex adapter
|
|
2257
|
+
claude-alarm codex status Show Codex adapter status
|
|
2100
2258
|
claude-alarm help Show this help
|
|
2101
2259
|
|
|
2102
2260
|
Quick start:
|
|
@@ -2105,8 +2263,8 @@ Quick start:
|
|
|
2105
2263
|
}
|
|
2106
2264
|
async function checkForUpdates() {
|
|
2107
2265
|
try {
|
|
2108
|
-
const pkgPath =
|
|
2109
|
-
const pkg = JSON.parse(
|
|
2266
|
+
const pkgPath = path9.join(__dirname2, "..", "package.json");
|
|
2267
|
+
const pkg = JSON.parse(fs8.readFileSync(pkgPath, "utf-8"));
|
|
2110
2268
|
const currentVersion = pkg.version;
|
|
2111
2269
|
const res = await fetch("https://registry.npmjs.org/@delt/claude-alarm/latest", {
|
|
2112
2270
|
signal: AbortSignal.timeout(3e3)
|
|
@@ -2129,25 +2287,25 @@ async function hubStart(daemon) {
|
|
|
2129
2287
|
const port = config2.hub.port ?? DEFAULT_HUB_PORT;
|
|
2130
2288
|
const displayHost = host === "0.0.0.0" ? "127.0.0.1" : host;
|
|
2131
2289
|
checkForUpdates();
|
|
2132
|
-
if (
|
|
2133
|
-
const pid = parseInt(
|
|
2290
|
+
if (fs8.existsSync(PID_FILE)) {
|
|
2291
|
+
const pid = parseInt(fs8.readFileSync(PID_FILE, "utf-8").trim(), 10);
|
|
2134
2292
|
if (isProcessRunning(pid)) {
|
|
2135
2293
|
console.log(`Hub is already running (PID: ${pid}). Dashboard: ${loginLink(displayHost, port, config2.hub.token)}`);
|
|
2136
2294
|
return;
|
|
2137
2295
|
}
|
|
2138
|
-
|
|
2296
|
+
fs8.unlinkSync(PID_FILE);
|
|
2139
2297
|
}
|
|
2140
2298
|
if (daemon) {
|
|
2141
2299
|
ensureConfigDir();
|
|
2142
|
-
const logFd =
|
|
2143
|
-
const hubScript =
|
|
2144
|
-
const child =
|
|
2300
|
+
const logFd = fs8.openSync(LOG_FILE, "a");
|
|
2301
|
+
const hubScript = path9.join(__dirname2, "hub", "server.js");
|
|
2302
|
+
const child = spawn3(process.execPath, [hubScript], {
|
|
2145
2303
|
detached: true,
|
|
2146
2304
|
stdio: ["ignore", logFd, logFd],
|
|
2147
2305
|
env: { ...process.env }
|
|
2148
2306
|
});
|
|
2149
2307
|
if (child.pid) {
|
|
2150
|
-
|
|
2308
|
+
fs8.writeFileSync(PID_FILE, String(child.pid), "utf-8");
|
|
2151
2309
|
child.unref();
|
|
2152
2310
|
console.log(`Hub started as daemon (PID: ${child.pid})`);
|
|
2153
2311
|
console.log(`Dashboard: ${loginLink(displayHost, port, config2.hub.token)}`);
|
|
@@ -2168,11 +2326,11 @@ async function hubStart(daemon) {
|
|
|
2168
2326
|
const hub = new HubServer2(config2);
|
|
2169
2327
|
await hub.start();
|
|
2170
2328
|
ensureConfigDir();
|
|
2171
|
-
|
|
2329
|
+
fs8.writeFileSync(PID_FILE, String(process.pid), "utf-8");
|
|
2172
2330
|
const shutdown = async () => {
|
|
2173
2331
|
console.log("\nShutting down...");
|
|
2174
2332
|
await hub.stop();
|
|
2175
|
-
if (
|
|
2333
|
+
if (fs8.existsSync(PID_FILE)) fs8.unlinkSync(PID_FILE);
|
|
2176
2334
|
process.exit(0);
|
|
2177
2335
|
};
|
|
2178
2336
|
process.on("SIGINT", shutdown);
|
|
@@ -2180,18 +2338,18 @@ async function hubStart(daemon) {
|
|
|
2180
2338
|
}
|
|
2181
2339
|
}
|
|
2182
2340
|
function hubStop() {
|
|
2183
|
-
if (!
|
|
2341
|
+
if (!fs8.existsSync(PID_FILE)) {
|
|
2184
2342
|
console.log("Hub is not running (no PID file found)");
|
|
2185
2343
|
return;
|
|
2186
2344
|
}
|
|
2187
|
-
const pid = parseInt(
|
|
2345
|
+
const pid = parseInt(fs8.readFileSync(PID_FILE, "utf-8").trim(), 10);
|
|
2188
2346
|
try {
|
|
2189
2347
|
process.kill(pid, "SIGTERM");
|
|
2190
2348
|
console.log(`Hub stopped (PID: ${pid})`);
|
|
2191
2349
|
} catch {
|
|
2192
2350
|
console.log("Hub process not found (may have already stopped)");
|
|
2193
2351
|
}
|
|
2194
|
-
|
|
2352
|
+
fs8.unlinkSync(PID_FILE);
|
|
2195
2353
|
}
|
|
2196
2354
|
async function hubStatus() {
|
|
2197
2355
|
const config2 = loadConfig();
|
|
@@ -2199,8 +2357,8 @@ async function hubStatus() {
|
|
|
2199
2357
|
const port = config2.hub.port ?? DEFAULT_HUB_PORT;
|
|
2200
2358
|
const displayHost = host === "0.0.0.0" ? "127.0.0.1" : host;
|
|
2201
2359
|
let pidInfo = "not running";
|
|
2202
|
-
if (
|
|
2203
|
-
const pid = parseInt(
|
|
2360
|
+
if (fs8.existsSync(PID_FILE)) {
|
|
2361
|
+
const pid = parseInt(fs8.readFileSync(PID_FILE, "utf-8").trim(), 10);
|
|
2204
2362
|
if (isProcessRunning(pid)) {
|
|
2205
2363
|
pidInfo = `running (PID: ${pid})`;
|
|
2206
2364
|
} else {
|
|
@@ -2272,7 +2430,7 @@ function ask(question) {
|
|
|
2272
2430
|
}
|
|
2273
2431
|
async function init() {
|
|
2274
2432
|
const dir = process.cwd();
|
|
2275
|
-
const projectName =
|
|
2433
|
+
const projectName = path9.basename(dir);
|
|
2276
2434
|
console.log(`
|
|
2277
2435
|
claude-alarm init for "${projectName}"
|
|
2278
2436
|
`);
|
|
@@ -2292,11 +2450,11 @@ claude-alarm init for "${projectName}"
|
|
|
2292
2450
|
if (port) env.CLAUDE_ALARM_HUB_PORT = port;
|
|
2293
2451
|
if (token) env.CLAUDE_ALARM_HUB_TOKEN = token;
|
|
2294
2452
|
}
|
|
2295
|
-
const mcpPath =
|
|
2453
|
+
const mcpPath = path9.join(dir, ".mcp.json");
|
|
2296
2454
|
let mcpConfig = {};
|
|
2297
|
-
if (
|
|
2455
|
+
if (fs8.existsSync(mcpPath)) {
|
|
2298
2456
|
try {
|
|
2299
|
-
mcpConfig = JSON.parse(
|
|
2457
|
+
mcpConfig = JSON.parse(fs8.readFileSync(mcpPath, "utf-8"));
|
|
2300
2458
|
} catch {
|
|
2301
2459
|
mcpConfig = {};
|
|
2302
2460
|
}
|
|
@@ -2307,7 +2465,7 @@ claude-alarm init for "${projectName}"
|
|
|
2307
2465
|
args: ["-y", "@delt/claude-alarm", "serve"],
|
|
2308
2466
|
env
|
|
2309
2467
|
};
|
|
2310
|
-
|
|
2468
|
+
fs8.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2), "utf-8");
|
|
2311
2469
|
console.log(`
|
|
2312
2470
|
\u2713 Created ${mcpPath}`);
|
|
2313
2471
|
if (remote.toLowerCase() !== "y") {
|
|
@@ -2328,6 +2486,11 @@ claude-alarm init for "${projectName}"
|
|
|
2328
2486
|
console.log(` claude-alarm hub start`);
|
|
2329
2487
|
}
|
|
2330
2488
|
console.log(` Dashboard: http://${displayHost}:${port}`);
|
|
2489
|
+
if (shouldOfferCodex(config2, findOnPath("codex") !== void 0)) {
|
|
2490
|
+
const answer = await ask("\nCodex is installed. Show Codex conversations on the dashboard too? (y/N): ");
|
|
2491
|
+
if (answer.toLowerCase() === "y") codexEnable(true);
|
|
2492
|
+
else setCodexEnabled(false);
|
|
2493
|
+
}
|
|
2331
2494
|
}
|
|
2332
2495
|
console.log(`
|
|
2333
2496
|
Next step:`);
|
|
@@ -2352,6 +2515,54 @@ function isProcessRunning(pid) {
|
|
|
2352
2515
|
return false;
|
|
2353
2516
|
}
|
|
2354
2517
|
}
|
|
2518
|
+
function readCodexPid() {
|
|
2519
|
+
if (!fs8.existsSync(CODEX_PID_FILE)) return void 0;
|
|
2520
|
+
const pid = parseInt(fs8.readFileSync(CODEX_PID_FILE, "utf-8").trim(), 10);
|
|
2521
|
+
return Number.isNaN(pid) ? void 0 : pid;
|
|
2522
|
+
}
|
|
2523
|
+
function codexEnable(enabled) {
|
|
2524
|
+
setCodexEnabled(enabled);
|
|
2525
|
+
console.log(enabled ? "Codex adapter enabled. Restart the hub to apply: claude-alarm hub stop, then claude-alarm hub start" : "Codex adapter disabled. Restart the hub to apply.");
|
|
2526
|
+
}
|
|
2527
|
+
function codexStart() {
|
|
2528
|
+
const pid = readCodexPid();
|
|
2529
|
+
if (pid !== void 0 && isProcessRunning(pid)) {
|
|
2530
|
+
console.log(`Codex adapter is already running (PID: ${pid})`);
|
|
2531
|
+
return;
|
|
2532
|
+
}
|
|
2533
|
+
ensureConfigDir();
|
|
2534
|
+
const logFd = fs8.openSync(CODEX_LOG_FILE, "a");
|
|
2535
|
+
const child = spawn3(process.execPath, [path9.join(__dirname2, "codex", "main.js")], {
|
|
2536
|
+
detached: true,
|
|
2537
|
+
stdio: ["ignore", logFd, logFd],
|
|
2538
|
+
env: { ...process.env }
|
|
2539
|
+
});
|
|
2540
|
+
child.unref();
|
|
2541
|
+
console.log(`Codex adapter started (PID: ${child.pid}). Logs: ${CODEX_LOG_FILE}`);
|
|
2542
|
+
}
|
|
2543
|
+
function codexStop() {
|
|
2544
|
+
const pid = readCodexPid();
|
|
2545
|
+
if (pid === void 0) {
|
|
2546
|
+
console.log("Codex adapter is not running");
|
|
2547
|
+
return;
|
|
2548
|
+
}
|
|
2549
|
+
try {
|
|
2550
|
+
process.kill(pid, "SIGTERM");
|
|
2551
|
+
console.log(`Codex adapter stopped (PID: ${pid})`);
|
|
2552
|
+
} catch {
|
|
2553
|
+
console.log("Codex adapter process not found (may have already stopped)");
|
|
2554
|
+
}
|
|
2555
|
+
try {
|
|
2556
|
+
fs8.unlinkSync(CODEX_PID_FILE);
|
|
2557
|
+
} catch {
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
function codexStatus() {
|
|
2561
|
+
const pid = readCodexPid();
|
|
2562
|
+
const state = pid === void 0 ? "not running" : isProcessRunning(pid) ? `running (PID: ${pid})` : "not running (stale PID file)";
|
|
2563
|
+
console.log(`Codex adapter: ${state}`);
|
|
2564
|
+
console.log(`Start with hub: ${loadConfig().codex?.enabled ? "enabled" : "disabled"}`);
|
|
2565
|
+
}
|
|
2355
2566
|
async function main2() {
|
|
2356
2567
|
const args = process.argv.slice(2);
|
|
2357
2568
|
const cmd = args[0];
|
|
@@ -2383,6 +2594,19 @@ async function main2() {
|
|
|
2383
2594
|
}
|
|
2384
2595
|
return;
|
|
2385
2596
|
}
|
|
2597
|
+
if (cmd === "codex") {
|
|
2598
|
+
if (sub === "enable") codexEnable(true);
|
|
2599
|
+
else if (sub === "disable") codexEnable(false);
|
|
2600
|
+
else if (sub === "start") codexStart();
|
|
2601
|
+
else if (sub === "stop") codexStop();
|
|
2602
|
+
else if (sub === "status") codexStatus();
|
|
2603
|
+
else {
|
|
2604
|
+
console.error(`Unknown codex command: ${sub}`);
|
|
2605
|
+
printUsage();
|
|
2606
|
+
process.exit(1);
|
|
2607
|
+
}
|
|
2608
|
+
return;
|
|
2609
|
+
}
|
|
2386
2610
|
if (cmd === "setup") {
|
|
2387
2611
|
setup(args[1]);
|
|
2388
2612
|
return;
|