@delt/claude-alarm 0.9.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -4
- package/dist/channel/server.js +41 -22
- package/dist/channel/server.js.map +1 -1
- package/dist/cli.js +460 -145
- package/dist/cli.js.map +1 -1
- package/dist/codex/main.js +774 -0
- package/dist/codex/main.js.map +1 -0
- package/dist/dashboard/index.html +44 -37
- package/dist/hub/server.js +292 -93
- package/dist/hub/server.js.map +1 -1
- package/dist/index.d.ts +28 -2
- package/dist/index.js +338 -120
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/dashboard/index.html +44 -37
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,12 @@ 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
|
+
}
|
|
70
78
|
function setupMcpConfig(targetDir) {
|
|
71
79
|
const dir = targetDir ?? process.cwd();
|
|
72
80
|
const mcpPath = path2.join(dir, ".mcp.json");
|
|
@@ -211,9 +219,9 @@ var init_session_manager = __esm({
|
|
|
211
219
|
SessionManager = class {
|
|
212
220
|
sessions = /* @__PURE__ */ new Map();
|
|
213
221
|
register(session) {
|
|
214
|
-
const baseName =
|
|
222
|
+
const baseName = this.baseName(session);
|
|
215
223
|
const existing = Array.from(this.sessions.values()).filter(
|
|
216
|
-
(s) => s.id !== session.id &&
|
|
224
|
+
(s) => s.id !== session.id && this.baseName(s) === baseName
|
|
217
225
|
);
|
|
218
226
|
if (existing.length > 0) {
|
|
219
227
|
const usedNums = existing.map((s) => {
|
|
@@ -267,6 +275,9 @@ var init_session_manager = __esm({
|
|
|
267
275
|
count() {
|
|
268
276
|
return this.sessions.size;
|
|
269
277
|
}
|
|
278
|
+
baseName(session) {
|
|
279
|
+
return session.title || session.cwd?.replace(/^.*[/\\]/, "") || session.name;
|
|
280
|
+
}
|
|
270
281
|
};
|
|
271
282
|
}
|
|
272
283
|
});
|
|
@@ -294,7 +305,7 @@ var init_notifier = __esm({
|
|
|
294
305
|
async notify(title, message, level = "info") {
|
|
295
306
|
await this.notifyWithSession(void 0, void 0, title, message, level);
|
|
296
307
|
}
|
|
297
|
-
async notifyWithSession(sessionId2,
|
|
308
|
+
async notifyWithSession(sessionId2, sessionLabel2, title, message, level = "info") {
|
|
298
309
|
const promises = [];
|
|
299
310
|
if (this.desktopEnabled) {
|
|
300
311
|
promises.push(this.sendDesktop(title, message, level));
|
|
@@ -302,8 +313,8 @@ var init_notifier = __esm({
|
|
|
302
313
|
for (const webhook of this.webhooks) {
|
|
303
314
|
promises.push(this.sendWebhook(webhook, title, message, level));
|
|
304
315
|
}
|
|
305
|
-
if (this.telegramBot && sessionId2 &&
|
|
306
|
-
promises.push(this.telegramBot.sendNotification(sessionId2,
|
|
316
|
+
if (this.telegramBot && sessionId2 && sessionLabel2) {
|
|
317
|
+
promises.push(this.telegramBot.sendNotification(sessionId2, sessionLabel2, title, message));
|
|
307
318
|
}
|
|
308
319
|
await Promise.allSettled(promises);
|
|
309
320
|
}
|
|
@@ -387,6 +398,17 @@ var init_notifier = __esm({
|
|
|
387
398
|
}
|
|
388
399
|
});
|
|
389
400
|
|
|
401
|
+
// src/shared/session-label.ts
|
|
402
|
+
function sessionLabel(session) {
|
|
403
|
+
const base = session.displayName || session.cwd?.replace(/^.*[/\\]/, "") || session.name;
|
|
404
|
+
return session.agentKind === "codex" ? `Codex \xB7 ${base}` : base;
|
|
405
|
+
}
|
|
406
|
+
var init_session_label = __esm({
|
|
407
|
+
"src/shared/session-label.ts"() {
|
|
408
|
+
"use strict";
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
|
|
390
412
|
// src/hub/telegram.ts
|
|
391
413
|
import fs3 from "fs";
|
|
392
414
|
import path3 from "path";
|
|
@@ -397,6 +419,7 @@ var init_telegram = __esm({
|
|
|
397
419
|
"use strict";
|
|
398
420
|
init_logger();
|
|
399
421
|
init_constants();
|
|
422
|
+
init_session_label();
|
|
400
423
|
TELEGRAM_API = "https://api.telegram.org/bot";
|
|
401
424
|
TelegramBot = class _TelegramBot {
|
|
402
425
|
config;
|
|
@@ -535,15 +558,14 @@ ${this.mdToHtml(message)}`;
|
|
|
535
558
|
const pending = this.pendingMessages.get(msg.chat.id);
|
|
536
559
|
if (pending) {
|
|
537
560
|
this.pendingMessages.delete(msg.chat.id);
|
|
538
|
-
const
|
|
539
|
-
|
|
540
|
-
if (idx >= 0 && idx < sessions2.length) {
|
|
561
|
+
const session = this.pendingSession(pending, parseInt(selectMatch[1], 10) - 1);
|
|
562
|
+
if (session) {
|
|
541
563
|
if (pending.photoFileId) {
|
|
542
|
-
await this.deliverPhotoToSessionByFileId(
|
|
564
|
+
await this.deliverPhotoToSessionByFileId(session.id, pending.photoFileId, pending.caption);
|
|
543
565
|
} else if (pending.text) {
|
|
544
|
-
this.deliverToSession(
|
|
566
|
+
this.deliverToSession(session.id, pending.text);
|
|
545
567
|
}
|
|
546
|
-
this.sendMessage(`Sent to [${this.getLabel(
|
|
568
|
+
this.sendMessage(`Sent to [${this.getLabel(session)}]`);
|
|
547
569
|
} else {
|
|
548
570
|
this.sendMessage("Invalid session number.");
|
|
549
571
|
}
|
|
@@ -564,11 +586,12 @@ ${this.mdToHtml(message)}`;
|
|
|
564
586
|
}
|
|
565
587
|
return;
|
|
566
588
|
}
|
|
589
|
+
const sessionIds = sessions.map((s) => s.id);
|
|
567
590
|
if (hasPhoto) {
|
|
568
591
|
const largest = msg.photo[msg.photo.length - 1];
|
|
569
|
-
this.pendingMessages.set(msg.chat.id, { photoFileId: largest.file_id, caption: text });
|
|
592
|
+
this.pendingMessages.set(msg.chat.id, { photoFileId: largest.file_id, caption: text, sessionIds });
|
|
570
593
|
} else {
|
|
571
|
-
this.pendingMessages.set(msg.chat.id, { text });
|
|
594
|
+
this.pendingMessages.set(msg.chat.id, { text, sessionIds });
|
|
572
595
|
}
|
|
573
596
|
const buttons = sessions.map((s, i) => ({
|
|
574
597
|
text: this.getLabel(s),
|
|
@@ -625,11 +648,16 @@ ${this.mdToHtml(message)}`;
|
|
|
625
648
|
logger.warn(`Telegram photo download failed: ${err.message}`);
|
|
626
649
|
}
|
|
627
650
|
}
|
|
651
|
+
// Buttons are numbered against the list shown when they were sent; Codex sessions come and go, so the live list may have shifted.
|
|
652
|
+
pendingSession(pending, idx) {
|
|
653
|
+
const id = pending.sessionIds[idx];
|
|
654
|
+
return id ? this.getSessions?.().find((s) => s.id === id) : void 0;
|
|
655
|
+
}
|
|
628
656
|
getLabel(session) {
|
|
629
|
-
return session
|
|
657
|
+
return sessionLabel(session);
|
|
630
658
|
}
|
|
631
659
|
/** Send a permission request with inline buttons */
|
|
632
|
-
async sendPermissionRequest(sessionId2,
|
|
660
|
+
async sendPermissionRequest(sessionId2, sessionLabel2, requestId, toolName, description, inputPreview) {
|
|
633
661
|
const isNotify = toolName.endsWith("__notify") || toolName === "notify";
|
|
634
662
|
let preview = inputPreview;
|
|
635
663
|
let kind = "code";
|
|
@@ -682,7 +710,7 @@ ${this.mdToHtml(message)}`;
|
|
|
682
710
|
const mcpMatch = toolName.match(/__([^_]+)$/);
|
|
683
711
|
if (mcpMatch) displayTool = mcpMatch[1].charAt(0).toUpperCase() + mcpMatch[1].slice(1);
|
|
684
712
|
}
|
|
685
|
-
const text = `\u26A0\uFE0F <b>Permission Request</b> \u2014 ${this.escHtml(
|
|
713
|
+
const text = `\u26A0\uFE0F <b>Permission Request</b> \u2014 ${this.escHtml(sessionLabel2)}
|
|
686
714
|
|
|
687
715
|
\u{1F527} <b>${this.escHtml(displayTool)}</b>
|
|
688
716
|
${previewHtml}${truncNote}`;
|
|
@@ -696,6 +724,7 @@ ${previewHtml}${truncNote}`;
|
|
|
696
724
|
}
|
|
697
725
|
async handleCallbackQuery(query) {
|
|
698
726
|
if (!query.data) return;
|
|
727
|
+
if (String(query.message?.chat.id) !== String(this.config.chatId)) return;
|
|
699
728
|
if (query.data.startsWith("sess:")) {
|
|
700
729
|
await this.handleSessionSelectCallback(query);
|
|
701
730
|
return;
|
|
@@ -721,22 +750,18 @@ ${label}`);
|
|
|
721
750
|
const parts = query.data.split(":");
|
|
722
751
|
if (parts.length < 3) return;
|
|
723
752
|
const [, idxStr, chatIdStr] = parts;
|
|
724
|
-
const idx = parseInt(idxStr, 10);
|
|
725
753
|
const chatId = parseInt(chatIdStr, 10);
|
|
726
|
-
const
|
|
727
|
-
|
|
754
|
+
const pending = this.pendingMessages.get(chatId);
|
|
755
|
+
const session = pending ? this.pendingSession(pending, parseInt(idxStr, 10)) : void 0;
|
|
756
|
+
if (!pending || !session) {
|
|
728
757
|
await this.answerCallbackQuery(query.id, "Session not found");
|
|
729
758
|
return;
|
|
730
759
|
}
|
|
731
|
-
const session = sessions[idx];
|
|
732
|
-
const pending = this.pendingMessages.get(chatId);
|
|
733
760
|
this.pendingMessages.delete(chatId);
|
|
734
|
-
if (pending) {
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
this.deliverToSession(session.id, pending.text);
|
|
739
|
-
}
|
|
761
|
+
if (pending.photoFileId) {
|
|
762
|
+
await this.deliverPhotoToSessionByFileId(session.id, pending.photoFileId, pending.caption);
|
|
763
|
+
} else if (pending.text) {
|
|
764
|
+
this.deliverToSession(session.id, pending.text);
|
|
740
765
|
}
|
|
741
766
|
await this.answerCallbackQuery(query.id, `Sent to ${this.getLabel(session)}`);
|
|
742
767
|
if (query.message) {
|
|
@@ -798,8 +823,8 @@ ${label}`);
|
|
|
798
823
|
css: "css",
|
|
799
824
|
scss: "scss"
|
|
800
825
|
};
|
|
801
|
-
langOf(
|
|
802
|
-
const m = /\.([a-z0-9]+)$/i.exec(
|
|
826
|
+
langOf(path9) {
|
|
827
|
+
const m = /\.([a-z0-9]+)$/i.exec(path9 || "");
|
|
803
828
|
return m ? _TelegramBot.LANG_BY_EXT[m[1].toLowerCase()] ?? "" : "";
|
|
804
829
|
}
|
|
805
830
|
/** Convert markdown to Telegram HTML (escape first, then apply formatting) */
|
|
@@ -844,18 +869,152 @@ ${bodyLines.join("\n")}`;
|
|
|
844
869
|
}
|
|
845
870
|
});
|
|
846
871
|
|
|
872
|
+
// src/hub/codex-supervisor.ts
|
|
873
|
+
import { spawn } from "child_process";
|
|
874
|
+
import fs4 from "fs";
|
|
875
|
+
import path4 from "path";
|
|
876
|
+
function resolveAdapterScript(baseDir) {
|
|
877
|
+
return [path4.join(baseDir, "..", "codex", "main.js"), path4.join(baseDir, "codex", "main.js")].find((p) => fs4.existsSync(p));
|
|
878
|
+
}
|
|
879
|
+
var HEALTHY_RUN_MS, CodexSupervisor;
|
|
880
|
+
var init_codex_supervisor = __esm({
|
|
881
|
+
"src/hub/codex-supervisor.ts"() {
|
|
882
|
+
"use strict";
|
|
883
|
+
init_logger();
|
|
884
|
+
HEALTHY_RUN_MS = 6e4;
|
|
885
|
+
CodexSupervisor = class {
|
|
886
|
+
constructor(script, spawnFn = spawn, minDelayMs = 2e3, maxDelayMs = 6e4, stopGraceMs = 3e3) {
|
|
887
|
+
this.script = script;
|
|
888
|
+
this.spawnFn = spawnFn;
|
|
889
|
+
this.minDelayMs = minDelayMs;
|
|
890
|
+
this.maxDelayMs = maxDelayMs;
|
|
891
|
+
this.stopGraceMs = stopGraceMs;
|
|
892
|
+
this.delay = minDelayMs;
|
|
893
|
+
}
|
|
894
|
+
child;
|
|
895
|
+
timer;
|
|
896
|
+
stopped = true;
|
|
897
|
+
delay;
|
|
898
|
+
start() {
|
|
899
|
+
this.stopped = false;
|
|
900
|
+
this.launch();
|
|
901
|
+
}
|
|
902
|
+
stop() {
|
|
903
|
+
this.stopped = true;
|
|
904
|
+
if (this.timer) clearTimeout(this.timer);
|
|
905
|
+
this.timer = void 0;
|
|
906
|
+
const child = this.child;
|
|
907
|
+
this.child = void 0;
|
|
908
|
+
if (!child) return;
|
|
909
|
+
child.stdin?.end();
|
|
910
|
+
const force = setTimeout(() => child.kill(), this.stopGraceMs);
|
|
911
|
+
force.unref();
|
|
912
|
+
child.once("exit", () => clearTimeout(force));
|
|
913
|
+
}
|
|
914
|
+
launch() {
|
|
915
|
+
const startedAt = Date.now();
|
|
916
|
+
const child = this.spawnFn(process.execPath, [this.script, "--watch-stdin"], {
|
|
917
|
+
detached: true,
|
|
918
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
919
|
+
windowsHide: true
|
|
920
|
+
});
|
|
921
|
+
this.child = child;
|
|
922
|
+
child.stdout?.on("data", (d) => process.stdout.write(d));
|
|
923
|
+
child.stderr?.on("data", (d) => process.stderr.write(d));
|
|
924
|
+
child.stdin?.on("error", () => {
|
|
925
|
+
});
|
|
926
|
+
child.on("error", (err) => logger.warn(`Codex adapter failed to start: ${err.message}`));
|
|
927
|
+
child.on("exit", (code, signal) => {
|
|
928
|
+
if (this.child === child) this.child = void 0;
|
|
929
|
+
if (this.stopped || code === 0) return;
|
|
930
|
+
if (Date.now() - startedAt > HEALTHY_RUN_MS) this.delay = this.minDelayMs;
|
|
931
|
+
logger.warn(`Codex adapter exited (${signal ?? code}); restarting in ${this.delay}ms`);
|
|
932
|
+
this.timer = setTimeout(() => {
|
|
933
|
+
this.timer = void 0;
|
|
934
|
+
if (!this.stopped) this.launch();
|
|
935
|
+
}, this.delay);
|
|
936
|
+
this.delay = Math.min(this.delay * 2, this.maxDelayMs);
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
// src/hub/auth.ts
|
|
944
|
+
import crypto from "crypto";
|
|
945
|
+
function safeEqual(a, b) {
|
|
946
|
+
if (typeof a !== "string") return false;
|
|
947
|
+
const x = Buffer.from(a);
|
|
948
|
+
const y = Buffer.from(b);
|
|
949
|
+
return x.length === y.length && crypto.timingSafeEqual(x, y);
|
|
950
|
+
}
|
|
951
|
+
function dashboardCookieValue(token) {
|
|
952
|
+
return crypto.createHmac("sha256", token).update("claude-alarm-dashboard").digest("hex");
|
|
953
|
+
}
|
|
954
|
+
function parseCookies(header) {
|
|
955
|
+
const out = {};
|
|
956
|
+
for (const part of (header ?? "").split(";")) {
|
|
957
|
+
const i = part.indexOf("=");
|
|
958
|
+
if (i <= 0) continue;
|
|
959
|
+
const raw = part.slice(i + 1).trim();
|
|
960
|
+
let value = raw;
|
|
961
|
+
try {
|
|
962
|
+
value = decodeURIComponent(raw);
|
|
963
|
+
} catch {
|
|
964
|
+
}
|
|
965
|
+
out[part.slice(0, i).trim()] = value;
|
|
966
|
+
}
|
|
967
|
+
return out;
|
|
968
|
+
}
|
|
969
|
+
function sessionCookieHeader(token, secure) {
|
|
970
|
+
return `${SESSION_COOKIE}=${dashboardCookieValue(token)}; HttpOnly; SameSite=Strict; Path=/; Max-Age=${COOKIE_MAX_AGE}${secure ? "; Secure" : ""}`;
|
|
971
|
+
}
|
|
972
|
+
function isAuthorized(req, token, allowQueryToken) {
|
|
973
|
+
const auth = req.headers.authorization;
|
|
974
|
+
if (auth?.startsWith("Bearer ") && safeEqual(auth.slice(7), token)) return true;
|
|
975
|
+
if (safeEqual(parseCookies(req.headers.cookie)[SESSION_COOKIE], dashboardCookieValue(token))) return true;
|
|
976
|
+
if (allowQueryToken) {
|
|
977
|
+
const q = new URL(req.url ?? "/", "http://hub").searchParams.get("token");
|
|
978
|
+
if (safeEqual(q, token)) return true;
|
|
979
|
+
}
|
|
980
|
+
return false;
|
|
981
|
+
}
|
|
982
|
+
function isCrossOrigin(req) {
|
|
983
|
+
const origin = req.headers.origin;
|
|
984
|
+
if (!origin) return false;
|
|
985
|
+
try {
|
|
986
|
+
return new URL(origin).host !== req.headers.host;
|
|
987
|
+
} catch {
|
|
988
|
+
return true;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
function isJsonRequest(req) {
|
|
992
|
+
return String(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json");
|
|
993
|
+
}
|
|
994
|
+
function isSecureRequest(req) {
|
|
995
|
+
return req.headers["x-forwarded-proto"] === "https";
|
|
996
|
+
}
|
|
997
|
+
var SESSION_COOKIE, COOKIE_MAX_AGE;
|
|
998
|
+
var init_auth = __esm({
|
|
999
|
+
"src/hub/auth.ts"() {
|
|
1000
|
+
"use strict";
|
|
1001
|
+
SESSION_COOKIE = "ca_session";
|
|
1002
|
+
COOKIE_MAX_AGE = 30 * 24 * 60 * 60;
|
|
1003
|
+
}
|
|
1004
|
+
});
|
|
1005
|
+
|
|
847
1006
|
// src/hub/server.ts
|
|
848
1007
|
var server_exports = {};
|
|
849
1008
|
__export(server_exports, {
|
|
850
1009
|
HubServer: () => HubServer
|
|
851
1010
|
});
|
|
852
1011
|
import http from "http";
|
|
853
|
-
import
|
|
854
|
-
import
|
|
1012
|
+
import fs5 from "fs";
|
|
1013
|
+
import path5 from "path";
|
|
855
1014
|
import { fileURLToPath } from "url";
|
|
856
1015
|
import { WebSocketServer, WebSocket } from "ws";
|
|
857
1016
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
858
|
-
var __dirname, HubServer;
|
|
1017
|
+
var __dirname, MAX_WS_PAYLOAD, HubServer;
|
|
859
1018
|
var init_server = __esm({
|
|
860
1019
|
"src/hub/server.ts"() {
|
|
861
1020
|
"use strict";
|
|
@@ -864,9 +1023,13 @@ var init_server = __esm({
|
|
|
864
1023
|
init_session_manager();
|
|
865
1024
|
init_notifier();
|
|
866
1025
|
init_telegram();
|
|
1026
|
+
init_codex_supervisor();
|
|
867
1027
|
init_config();
|
|
1028
|
+
init_session_label();
|
|
868
1029
|
init_crash_guard();
|
|
869
|
-
|
|
1030
|
+
init_auth();
|
|
1031
|
+
__dirname = path5.dirname(fileURLToPath(import.meta.url));
|
|
1032
|
+
MAX_WS_PAYLOAD = 16 * 1024 * 1024;
|
|
870
1033
|
HubServer = class {
|
|
871
1034
|
httpServer;
|
|
872
1035
|
wssChannel;
|
|
@@ -884,13 +1047,17 @@ var init_server = __esm({
|
|
|
884
1047
|
heartbeatInterval;
|
|
885
1048
|
channelAlive = /* @__PURE__ */ new Map();
|
|
886
1049
|
// sessionId -> alive flag
|
|
1050
|
+
socketOwners = /* @__PURE__ */ new WeakMap();
|
|
887
1051
|
host;
|
|
888
1052
|
port;
|
|
889
1053
|
token;
|
|
1054
|
+
codexEnabled;
|
|
1055
|
+
codexSupervisor;
|
|
890
1056
|
constructor(config2) {
|
|
891
1057
|
this.host = config2?.hub?.host ?? DEFAULT_HUB_HOST;
|
|
892
1058
|
this.port = config2?.hub?.port ?? DEFAULT_HUB_PORT;
|
|
893
1059
|
this.token = config2?.hub?.token;
|
|
1060
|
+
this.codexEnabled = config2?.codex?.enabled === true;
|
|
894
1061
|
if (config2?.notifications) {
|
|
895
1062
|
this.notifier.configure({
|
|
896
1063
|
desktop: config2.notifications.desktop
|
|
@@ -906,22 +1073,23 @@ var init_server = __esm({
|
|
|
906
1073
|
this.initTelegram(fullConfig.telegram);
|
|
907
1074
|
}
|
|
908
1075
|
this.httpServer = http.createServer((req, res) => this.handleHttp(req, res));
|
|
909
|
-
this.wssChannel = new WebSocketServer({ noServer: true });
|
|
1076
|
+
this.wssChannel = new WebSocketServer({ noServer: true, maxPayload: MAX_WS_PAYLOAD });
|
|
910
1077
|
this.wssChannel.on("connection", (ws, req) => this.handleChannelConnection(ws, req));
|
|
911
1078
|
this.wssChannel.on("error", (err) => logger.warn(`Channel WebSocket server error: ${err.message}`));
|
|
912
|
-
this.wssDashboard = new WebSocketServer({ noServer: true });
|
|
1079
|
+
this.wssDashboard = new WebSocketServer({ noServer: true, maxPayload: MAX_WS_PAYLOAD });
|
|
913
1080
|
this.wssDashboard.on("connection", (ws) => this.handleDashboardConnection(ws));
|
|
914
1081
|
this.wssDashboard.on("error", (err) => logger.warn(`Dashboard WebSocket server error: ${err.message}`));
|
|
915
1082
|
this.httpServer.on("upgrade", (req, socket, head) => {
|
|
916
1083
|
socket.on("error", (err) => logger.debug(`Upgrade socket error: ${err.message}`));
|
|
917
|
-
const url = new URL(req.url,
|
|
1084
|
+
const url = new URL(req.url ?? "/", "http://hub");
|
|
918
1085
|
const pathname = url.pathname;
|
|
919
|
-
if (
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
1086
|
+
if (isCrossOrigin(req)) {
|
|
1087
|
+
this.rejectUpgrade(socket, 403, "Forbidden", pathname, req);
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
if (!this.authorized(req, true)) {
|
|
1091
|
+
this.rejectUpgrade(socket, 401, "Unauthorized", pathname, req);
|
|
1092
|
+
return;
|
|
925
1093
|
}
|
|
926
1094
|
if (pathname === WS_PATH_CHANNEL) {
|
|
927
1095
|
this.wssChannel.handleUpgrade(req, socket, head, (ws) => {
|
|
@@ -947,12 +1115,15 @@ var init_server = __esm({
|
|
|
947
1115
|
this.httpServer.on("error", (err) => logger.error(`HTTP server error: ${err.message}`));
|
|
948
1116
|
const displayHost = this.host === "0.0.0.0" ? "127.0.0.1" : this.host;
|
|
949
1117
|
logger.info(`Hub server listening on http://${displayHost}:${this.port}`);
|
|
1118
|
+
this.startCodexAdapter();
|
|
950
1119
|
resolve();
|
|
951
1120
|
});
|
|
952
1121
|
});
|
|
953
1122
|
}
|
|
954
1123
|
stop() {
|
|
955
1124
|
return new Promise((resolve) => {
|
|
1125
|
+
this.codexSupervisor?.stop();
|
|
1126
|
+
this.codexSupervisor = void 0;
|
|
956
1127
|
if (this.heartbeatInterval) clearInterval(this.heartbeatInterval);
|
|
957
1128
|
if (this.telegramBot) this.telegramBot.stopPolling();
|
|
958
1129
|
for (const ws of this.channelSockets.values()) ws.terminate();
|
|
@@ -973,31 +1144,28 @@ var init_server = __esm({
|
|
|
973
1144
|
}
|
|
974
1145
|
// --- HTTP Handler ---
|
|
975
1146
|
handleHttp(req, res) {
|
|
976
|
-
const url = new URL(req.url,
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
980
|
-
}
|
|
981
|
-
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
982
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
983
|
-
if (req.method === "OPTIONS") {
|
|
984
|
-
res.writeHead(204);
|
|
985
|
-
res.end();
|
|
1147
|
+
const url = new URL(req.url ?? "/", "http://hub");
|
|
1148
|
+
if (isCrossOrigin(req)) {
|
|
1149
|
+
this.jsonResponse(res, 403, { error: "Cross-origin request rejected" });
|
|
986
1150
|
return;
|
|
987
1151
|
}
|
|
988
|
-
if (
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
const bearerToken = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
|
|
992
|
-
if (bearerToken !== this.token) {
|
|
993
|
-
this.jsonResponse(res, 401, { error: "Unauthorized" });
|
|
994
|
-
return;
|
|
995
|
-
}
|
|
996
|
-
}
|
|
1152
|
+
if (req.method === "POST" && !isJsonRequest(req)) {
|
|
1153
|
+
this.jsonResponse(res, 415, { error: "Content-Type must be application/json" });
|
|
1154
|
+
return;
|
|
997
1155
|
}
|
|
998
1156
|
if (url.pathname === "/" && req.method === "GET") {
|
|
999
|
-
this.serveDashboard(res);
|
|
1000
|
-
|
|
1157
|
+
this.serveDashboard(req, res, url);
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
1160
|
+
if (url.pathname === "/api/login" && req.method === "POST") {
|
|
1161
|
+
this.handleLogin(req, res);
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
if (!this.authorized(req, false)) {
|
|
1165
|
+
this.jsonResponse(res, 401, { error: "Unauthorized" });
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
if (url.pathname === "/api/sessions" && req.method === "GET") {
|
|
1001
1169
|
this.jsonResponse(res, 200, { sessions: this.sessions.getAll() });
|
|
1002
1170
|
} else if (url.pathname === "/api/status" && req.method === "GET") {
|
|
1003
1171
|
this.jsonResponse(res, 200, {
|
|
@@ -1048,25 +1216,31 @@ var init_server = __esm({
|
|
|
1048
1216
|
this.jsonResponse(res, 404, { error: "Not found" });
|
|
1049
1217
|
}
|
|
1050
1218
|
}
|
|
1051
|
-
serveDashboard(res) {
|
|
1219
|
+
serveDashboard(req, res, url) {
|
|
1220
|
+
const linkToken = url.searchParams.get("token");
|
|
1221
|
+
if (linkToken !== null && this.token && safeEqual(linkToken, this.token)) {
|
|
1222
|
+
res.writeHead(302, { Location: "/", "Set-Cookie": sessionCookieHeader(this.token, isSecureRequest(req)) });
|
|
1223
|
+
res.end();
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1052
1226
|
const candidates = [
|
|
1053
|
-
|
|
1227
|
+
path5.join(__dirname, "..", "dashboard", "index.html"),
|
|
1054
1228
|
// from dist/hub/
|
|
1055
|
-
|
|
1229
|
+
path5.join(__dirname, "dashboard", "index.html"),
|
|
1056
1230
|
// from dist/ (bundled index.js)
|
|
1057
|
-
|
|
1231
|
+
path5.join(__dirname, "..", "..", "src", "dashboard", "index.html"),
|
|
1058
1232
|
// from dist/hub/ -> src/
|
|
1059
|
-
|
|
1233
|
+
path5.join(__dirname, "..", "src", "dashboard", "index.html"),
|
|
1060
1234
|
// from dist/ -> src/
|
|
1061
|
-
|
|
1235
|
+
path5.join(process.cwd(), "dist", "dashboard", "index.html"),
|
|
1062
1236
|
// from cwd
|
|
1063
|
-
|
|
1237
|
+
path5.join(process.cwd(), "src", "dashboard", "index.html")
|
|
1064
1238
|
// from cwd/src
|
|
1065
1239
|
];
|
|
1066
1240
|
logger.debug(`Dashboard candidates: ${JSON.stringify(candidates)}`);
|
|
1067
1241
|
for (const candidate of candidates) {
|
|
1068
|
-
if (
|
|
1069
|
-
const html =
|
|
1242
|
+
if (fs5.existsSync(candidate)) {
|
|
1243
|
+
const html = fs5.readFileSync(candidate, "utf-8");
|
|
1070
1244
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
1071
1245
|
res.end(html);
|
|
1072
1246
|
return;
|
|
@@ -1075,6 +1249,20 @@ var init_server = __esm({
|
|
|
1075
1249
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
1076
1250
|
res.end("<html><body><h1>claude-alarm</h1><p>Dashboard HTML not found. Reinstall the package.</p></body></html>");
|
|
1077
1251
|
}
|
|
1252
|
+
async handleLogin(req, res) {
|
|
1253
|
+
const body = await this.readBody(req);
|
|
1254
|
+
if (!this.token) {
|
|
1255
|
+
res.writeHead(204);
|
|
1256
|
+
res.end();
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
if (typeof body?.token !== "string" || !safeEqual(body.token, this.token)) {
|
|
1260
|
+
this.jsonResponse(res, 401, { error: "Unauthorized" });
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
res.writeHead(204, { "Set-Cookie": sessionCookieHeader(this.token, isSecureRequest(req)) });
|
|
1264
|
+
res.end();
|
|
1265
|
+
}
|
|
1078
1266
|
async handleApiSend(req, res) {
|
|
1079
1267
|
const body = await this.readBody(req);
|
|
1080
1268
|
if (!body) {
|
|
@@ -1091,7 +1279,7 @@ var init_server = __esm({
|
|
|
1091
1279
|
this.jsonResponse(res, 404, { error: "Session not connected" });
|
|
1092
1280
|
return;
|
|
1093
1281
|
}
|
|
1094
|
-
const msg = { type: "message_to_session", sessionId: sessionId2, content };
|
|
1282
|
+
const msg = { type: "message_to_session", sessionId: sessionId2, content, source: "api" };
|
|
1095
1283
|
ws.send(JSON.stringify(msg));
|
|
1096
1284
|
this.jsonResponse(res, 200, { ok: true });
|
|
1097
1285
|
}
|
|
@@ -1151,6 +1339,22 @@ var init_server = __esm({
|
|
|
1151
1339
|
});
|
|
1152
1340
|
}
|
|
1153
1341
|
handleChannelMessage(ws, isLocal, msg) {
|
|
1342
|
+
if (msg.type === "register") {
|
|
1343
|
+
const id = msg.session.id;
|
|
1344
|
+
const owned = this.socketOwners.get(ws);
|
|
1345
|
+
if (owned && owned !== id) {
|
|
1346
|
+
logger.warn(`Rejected register for ${id}: connection already owns ${owned}`);
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
const holder = this.channelSockets.get(id);
|
|
1350
|
+
if (holder && holder !== ws) {
|
|
1351
|
+
this.socketOwners.delete(holder);
|
|
1352
|
+
holder.terminate();
|
|
1353
|
+
}
|
|
1354
|
+
this.socketOwners.set(ws, id);
|
|
1355
|
+
} else if ("sessionId" in msg && this.socketOwners.get(ws) !== msg.sessionId) {
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1154
1358
|
switch (msg.type) {
|
|
1155
1359
|
case "register": {
|
|
1156
1360
|
const session = msg.session;
|
|
@@ -1174,7 +1378,6 @@ var init_server = __esm({
|
|
|
1174
1378
|
break;
|
|
1175
1379
|
}
|
|
1176
1380
|
case "peer_name": {
|
|
1177
|
-
if (this.channelSockets.get(msg.sessionId) !== ws) break;
|
|
1178
1381
|
const updated = this.sessions.setPeerName(msg.sessionId, msg.peerName);
|
|
1179
1382
|
if (updated) {
|
|
1180
1383
|
logger.info(`Peer name for ${msg.sessionId}: ${msg.peerName ?? "-"}`);
|
|
@@ -1250,7 +1453,7 @@ var init_server = __esm({
|
|
|
1250
1453
|
if (msg.type === "message_to_session") {
|
|
1251
1454
|
const channelWs = this.channelSockets.get(msg.sessionId);
|
|
1252
1455
|
if (channelWs?.readyState === WebSocket.OPEN) {
|
|
1253
|
-
channelWs.send(JSON.stringify(msg));
|
|
1456
|
+
channelWs.send(JSON.stringify({ ...msg, source: "dashboard" }));
|
|
1254
1457
|
}
|
|
1255
1458
|
} else if (msg.type === "image_upload") {
|
|
1256
1459
|
this.handleImageUpload(msg);
|
|
@@ -1295,11 +1498,11 @@ var init_server = __esm({
|
|
|
1295
1498
|
logger.warn("Image upload rejected: exceeds 10MB");
|
|
1296
1499
|
return;
|
|
1297
1500
|
}
|
|
1298
|
-
|
|
1501
|
+
fs5.mkdirSync(UPLOADS_DIR, { recursive: true });
|
|
1299
1502
|
const ext = mimeType.split("/")[1] === "jpeg" ? "jpg" : mimeType.split("/")[1];
|
|
1300
1503
|
const filename = `${randomUUID3()}.${ext}`;
|
|
1301
|
-
const filePath =
|
|
1302
|
-
|
|
1504
|
+
const filePath = path5.join(UPLOADS_DIR, filename);
|
|
1505
|
+
fs5.writeFileSync(filePath, buffer);
|
|
1303
1506
|
const forwardMsg = {
|
|
1304
1507
|
type: "image_to_session",
|
|
1305
1508
|
sessionId: sessionId2,
|
|
@@ -1312,7 +1515,7 @@ var init_server = __esm({
|
|
|
1312
1515
|
logger.info(`Image saved and forwarded: ${filename} (${buffer.length} bytes)`);
|
|
1313
1516
|
setTimeout(() => {
|
|
1314
1517
|
try {
|
|
1315
|
-
|
|
1518
|
+
fs5.unlinkSync(filePath);
|
|
1316
1519
|
} catch {
|
|
1317
1520
|
}
|
|
1318
1521
|
}, 5 * 60 * 1e3);
|
|
@@ -1340,7 +1543,7 @@ var init_server = __esm({
|
|
|
1340
1543
|
this.telegramBot.onMessageToSession = (sessionId2, content) => {
|
|
1341
1544
|
const channelWs = this.channelSockets.get(sessionId2);
|
|
1342
1545
|
if (channelWs?.readyState === WebSocket.OPEN) {
|
|
1343
|
-
const msg = { type: "message_to_session", sessionId: sessionId2, content };
|
|
1546
|
+
const msg = { type: "message_to_session", sessionId: sessionId2, content, source: "telegram" };
|
|
1344
1547
|
channelWs.send(JSON.stringify(msg));
|
|
1345
1548
|
logger.info(`Telegram message forwarded to session: ${sessionId2}`);
|
|
1346
1549
|
}
|
|
@@ -1467,6 +1670,17 @@ var init_server = __esm({
|
|
|
1467
1670
|
this.jsonResponse(res, 500, { error: err.message });
|
|
1468
1671
|
}
|
|
1469
1672
|
}
|
|
1673
|
+
startCodexAdapter() {
|
|
1674
|
+
if (!this.codexEnabled) return;
|
|
1675
|
+
const script = resolveAdapterScript(__dirname);
|
|
1676
|
+
if (!script) {
|
|
1677
|
+
logger.warn("Codex adapter is enabled but codex/main.js was not found next to the hub");
|
|
1678
|
+
return;
|
|
1679
|
+
}
|
|
1680
|
+
this.codexSupervisor = new CodexSupervisor(script);
|
|
1681
|
+
this.codexSupervisor.start();
|
|
1682
|
+
logger.info("Codex adapter started");
|
|
1683
|
+
}
|
|
1470
1684
|
startHeartbeat() {
|
|
1471
1685
|
this.heartbeatInterval = setInterval(() => {
|
|
1472
1686
|
for (const [sessionId2, ws] of this.channelSockets) {
|
|
@@ -1482,11 +1696,11 @@ var init_server = __esm({
|
|
|
1482
1696
|
}
|
|
1483
1697
|
cleanupUploads() {
|
|
1484
1698
|
try {
|
|
1485
|
-
if (!
|
|
1486
|
-
const files =
|
|
1699
|
+
if (!fs5.existsSync(UPLOADS_DIR)) return;
|
|
1700
|
+
const files = fs5.readdirSync(UPLOADS_DIR);
|
|
1487
1701
|
for (const file of files) {
|
|
1488
1702
|
try {
|
|
1489
|
-
|
|
1703
|
+
fs5.unlinkSync(path5.join(UPLOADS_DIR, file));
|
|
1490
1704
|
} catch {
|
|
1491
1705
|
}
|
|
1492
1706
|
}
|
|
@@ -1495,13 +1709,24 @@ var init_server = __esm({
|
|
|
1495
1709
|
}
|
|
1496
1710
|
}
|
|
1497
1711
|
getSessionLabel(session) {
|
|
1498
|
-
|
|
1499
|
-
return session.displayName || session.cwd?.replace(/^.*[/\\]/, "") || session.name;
|
|
1712
|
+
return session ? sessionLabel(session) : "unknown";
|
|
1500
1713
|
}
|
|
1501
1714
|
jsonResponse(res, status, body) {
|
|
1502
1715
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
1503
1716
|
res.end(JSON.stringify(body));
|
|
1504
1717
|
}
|
|
1718
|
+
authorized(req, allowQueryToken) {
|
|
1719
|
+
return !this.token || isAuthorized(req, this.token, allowQueryToken);
|
|
1720
|
+
}
|
|
1721
|
+
rejectUpgrade(socket, status, text, pathname, req) {
|
|
1722
|
+
logger.warn(`Rejected ${pathname} upgrade (${status}) from ${req.socket.remoteAddress}`);
|
|
1723
|
+
socket.write(`HTTP/1.1 ${status} ${text}\r
|
|
1724
|
+
Connection: close\r
|
|
1725
|
+
Content-Length: 0\r
|
|
1726
|
+
\r
|
|
1727
|
+
`);
|
|
1728
|
+
socket.destroy();
|
|
1729
|
+
}
|
|
1505
1730
|
isLocalRequest(req) {
|
|
1506
1731
|
const addr = req.socket.remoteAddress;
|
|
1507
1732
|
return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
|
|
@@ -1556,48 +1781,41 @@ var init_hub_client = __esm({
|
|
|
1556
1781
|
init_logger();
|
|
1557
1782
|
init_constants();
|
|
1558
1783
|
HubClient = class {
|
|
1559
|
-
constructor(sessionId2, sessionName2, hubHost2 = DEFAULT_HUB_HOST, hubPort2 = DEFAULT_HUB_PORT, token, getPeerName = () => void 0) {
|
|
1784
|
+
constructor(sessionId2, sessionName2, hubHost2 = DEFAULT_HUB_HOST, hubPort2 = DEFAULT_HUB_PORT, token, getPeerName = () => void 0, getRegistration = () => ({})) {
|
|
1560
1785
|
this.sessionId = sessionId2;
|
|
1561
1786
|
this.sessionName = sessionName2;
|
|
1562
1787
|
this.hubHost = hubHost2;
|
|
1563
1788
|
this.hubPort = hubPort2;
|
|
1564
1789
|
this.token = token;
|
|
1565
1790
|
this.getPeerName = getPeerName;
|
|
1791
|
+
this.getRegistration = getRegistration;
|
|
1566
1792
|
}
|
|
1567
1793
|
ws = null;
|
|
1568
1794
|
reconnectTimer = null;
|
|
1569
1795
|
messageHandlers = [];
|
|
1570
1796
|
queue = [];
|
|
1571
1797
|
connected = false;
|
|
1798
|
+
closed = false;
|
|
1572
1799
|
connect() {
|
|
1800
|
+
this.closed = false;
|
|
1573
1801
|
const tokenQuery = this.token ? `?token=${encodeURIComponent(this.token)}` : "";
|
|
1574
1802
|
const url = `ws://${this.hubHost}:${this.hubPort}${WS_PATH_CHANNEL}${tokenQuery}`;
|
|
1575
1803
|
logger.debug(`Connecting to hub at ${url}`);
|
|
1576
1804
|
try {
|
|
1577
|
-
|
|
1578
|
-
this.ws
|
|
1805
|
+
const ws = new WebSocket2(url);
|
|
1806
|
+
this.ws = ws;
|
|
1807
|
+
ws.on("open", () => {
|
|
1808
|
+
if (this.ws !== ws) return;
|
|
1579
1809
|
logger.info("Connected to hub");
|
|
1580
1810
|
this.connected = true;
|
|
1581
|
-
|
|
1582
|
-
type: "register",
|
|
1583
|
-
session: {
|
|
1584
|
-
id: this.sessionId,
|
|
1585
|
-
name: this.sessionName,
|
|
1586
|
-
status: "idle",
|
|
1587
|
-
connectedAt: Date.now(),
|
|
1588
|
-
lastActivity: Date.now(),
|
|
1589
|
-
cwd: process.cwd(),
|
|
1590
|
-
channelEnabled: true,
|
|
1591
|
-
peerName: this.getPeerName()
|
|
1592
|
-
}
|
|
1593
|
-
};
|
|
1594
|
-
this.ws.send(JSON.stringify(registration));
|
|
1811
|
+
this.ws.send(JSON.stringify(this.registration()));
|
|
1595
1812
|
for (const msg of this.queue) {
|
|
1596
1813
|
this.ws.send(JSON.stringify(msg));
|
|
1597
1814
|
}
|
|
1598
1815
|
this.queue = [];
|
|
1599
1816
|
});
|
|
1600
|
-
|
|
1817
|
+
ws.on("message", (data) => {
|
|
1818
|
+
if (this.ws !== ws) return;
|
|
1601
1819
|
try {
|
|
1602
1820
|
const msg = JSON.parse(data.toString());
|
|
1603
1821
|
for (const handler of this.messageHandlers) {
|
|
@@ -1607,12 +1825,14 @@ var init_hub_client = __esm({
|
|
|
1607
1825
|
logger.warn("Failed to parse hub message:", err);
|
|
1608
1826
|
}
|
|
1609
1827
|
});
|
|
1610
|
-
|
|
1828
|
+
ws.on("close", () => {
|
|
1829
|
+
if (this.ws !== ws) return;
|
|
1611
1830
|
logger.info("Disconnected from hub");
|
|
1612
1831
|
this.connected = false;
|
|
1613
|
-
this.scheduleReconnect();
|
|
1832
|
+
if (!this.closed) this.scheduleReconnect();
|
|
1614
1833
|
});
|
|
1615
|
-
|
|
1834
|
+
ws.on("error", (err) => {
|
|
1835
|
+
if (this.ws !== ws) return;
|
|
1616
1836
|
logger.debug(`Hub connection error: ${err.message}`);
|
|
1617
1837
|
this.connected = false;
|
|
1618
1838
|
});
|
|
@@ -1635,6 +1855,7 @@ var init_hub_client = __esm({
|
|
|
1635
1855
|
this.messageHandlers.push(handler);
|
|
1636
1856
|
}
|
|
1637
1857
|
disconnect() {
|
|
1858
|
+
this.closed = true;
|
|
1638
1859
|
if (this.reconnectTimer) {
|
|
1639
1860
|
clearTimeout(this.reconnectTimer);
|
|
1640
1861
|
this.reconnectTimer = null;
|
|
@@ -1645,8 +1866,29 @@ var init_hub_client = __esm({
|
|
|
1645
1866
|
}
|
|
1646
1867
|
this.connected = false;
|
|
1647
1868
|
}
|
|
1869
|
+
reregister() {
|
|
1870
|
+
if (this.connected && this.ws?.readyState === WebSocket2.OPEN) {
|
|
1871
|
+
this.ws.send(JSON.stringify(this.registration()));
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
registration() {
|
|
1875
|
+
return {
|
|
1876
|
+
type: "register",
|
|
1877
|
+
session: {
|
|
1878
|
+
id: this.sessionId,
|
|
1879
|
+
name: this.sessionName,
|
|
1880
|
+
status: "idle",
|
|
1881
|
+
connectedAt: Date.now(),
|
|
1882
|
+
lastActivity: Date.now(),
|
|
1883
|
+
cwd: process.cwd(),
|
|
1884
|
+
channelEnabled: true,
|
|
1885
|
+
peerName: this.getPeerName(),
|
|
1886
|
+
...this.getRegistration()
|
|
1887
|
+
}
|
|
1888
|
+
};
|
|
1889
|
+
}
|
|
1648
1890
|
scheduleReconnect() {
|
|
1649
|
-
if (this.reconnectTimer) return;
|
|
1891
|
+
if (this.reconnectTimer || this.closed) return;
|
|
1650
1892
|
this.reconnectTimer = setTimeout(() => {
|
|
1651
1893
|
this.reconnectTimer = null;
|
|
1652
1894
|
this.connect();
|
|
@@ -1657,9 +1899,9 @@ var init_hub_client = __esm({
|
|
|
1657
1899
|
});
|
|
1658
1900
|
|
|
1659
1901
|
// src/channel/peer-name.ts
|
|
1660
|
-
import
|
|
1902
|
+
import fs6 from "fs";
|
|
1661
1903
|
import os2 from "os";
|
|
1662
|
-
import
|
|
1904
|
+
import path6 from "path";
|
|
1663
1905
|
function findPeerName(records, env) {
|
|
1664
1906
|
const valid = records.filter((r) => typeof r === "object" && r !== null);
|
|
1665
1907
|
const nameOf = (match) => {
|
|
@@ -1679,18 +1921,18 @@ function readPeerName(env = process.env) {
|
|
|
1679
1921
|
sessionId: env.CLAUDE_CODE_SESSION_ID
|
|
1680
1922
|
};
|
|
1681
1923
|
if (!lookup.messagingSocket && !lookup.sessionId) return void 0;
|
|
1682
|
-
const configDir = env.CLAUDE_CONFIG_DIR ||
|
|
1683
|
-
const dir =
|
|
1924
|
+
const configDir = env.CLAUDE_CONFIG_DIR || path6.join(os2.homedir(), ".claude");
|
|
1925
|
+
const dir = path6.join(configDir, "sessions");
|
|
1684
1926
|
let files;
|
|
1685
1927
|
try {
|
|
1686
|
-
files =
|
|
1928
|
+
files = fs6.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
1687
1929
|
} catch {
|
|
1688
1930
|
return void 0;
|
|
1689
1931
|
}
|
|
1690
1932
|
const records = [];
|
|
1691
1933
|
for (const f of files) {
|
|
1692
1934
|
try {
|
|
1693
|
-
records.push(JSON.parse(
|
|
1935
|
+
records.push(JSON.parse(fs6.readFileSync(path6.join(dir, f), "utf8")));
|
|
1694
1936
|
} catch {
|
|
1695
1937
|
}
|
|
1696
1938
|
}
|
|
@@ -1713,7 +1955,7 @@ import {
|
|
|
1713
1955
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
1714
1956
|
import { z } from "zod";
|
|
1715
1957
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
1716
|
-
import
|
|
1958
|
+
import path7 from "path";
|
|
1717
1959
|
async function main() {
|
|
1718
1960
|
logger.info(`Starting MCP channel server (session: ${sessionId})`);
|
|
1719
1961
|
hubClient.connect();
|
|
@@ -1782,7 +2024,7 @@ var init_server2 = __esm({
|
|
|
1782
2024
|
init_hub_client();
|
|
1783
2025
|
init_peer_name2();
|
|
1784
2026
|
sessionId = randomUUID4();
|
|
1785
|
-
sessionName = process.env.CLAUDE_ALARM_SESSION_NAME ??
|
|
2027
|
+
sessionName = process.env.CLAUDE_ALARM_SESSION_NAME ?? path7.basename(process.cwd());
|
|
1786
2028
|
peerName = readPeerName();
|
|
1787
2029
|
server = new Server(
|
|
1788
2030
|
{
|
|
@@ -1961,12 +2203,18 @@ init_config();
|
|
|
1961
2203
|
init_constants();
|
|
1962
2204
|
init_logger();
|
|
1963
2205
|
init_crash_guard();
|
|
1964
|
-
import { spawn } from "child_process";
|
|
1965
|
-
import
|
|
1966
|
-
import
|
|
2206
|
+
import { spawn as spawn2 } from "child_process";
|
|
2207
|
+
import fs7 from "fs";
|
|
2208
|
+
import path8 from "path";
|
|
1967
2209
|
import readline from "readline";
|
|
1968
2210
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1969
|
-
var __dirname2 =
|
|
2211
|
+
var __dirname2 = path8.dirname(fileURLToPath2(import.meta.url));
|
|
2212
|
+
function authHeaders(token) {
|
|
2213
|
+
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
2214
|
+
}
|
|
2215
|
+
function loginLink(displayHost, port, token) {
|
|
2216
|
+
return `http://${displayHost}:${port}/${token ? `?token=${encodeURIComponent(token)}` : ""}`;
|
|
2217
|
+
}
|
|
1970
2218
|
function printUsage() {
|
|
1971
2219
|
console.log(`
|
|
1972
2220
|
claude-alarm - Monitor Claude Code sessions with notifications
|
|
@@ -1979,6 +2227,11 @@ Usage:
|
|
|
1979
2227
|
claude-alarm setup [dir] Add claude-alarm to .mcp.json
|
|
1980
2228
|
claude-alarm test Send a test notification
|
|
1981
2229
|
claude-alarm token Show current auth token
|
|
2230
|
+
claude-alarm codex enable Start the Codex adapter together with the hub
|
|
2231
|
+
claude-alarm codex disable Stop starting the Codex adapter with the hub
|
|
2232
|
+
claude-alarm codex start Run the Codex adapter on its own (e.g. Codex on another PC)
|
|
2233
|
+
claude-alarm codex stop Stop a running Codex adapter
|
|
2234
|
+
claude-alarm codex status Show Codex adapter status
|
|
1982
2235
|
claude-alarm help Show this help
|
|
1983
2236
|
|
|
1984
2237
|
Quick start:
|
|
@@ -1987,8 +2240,8 @@ Quick start:
|
|
|
1987
2240
|
}
|
|
1988
2241
|
async function checkForUpdates() {
|
|
1989
2242
|
try {
|
|
1990
|
-
const pkgPath =
|
|
1991
|
-
const pkg = JSON.parse(
|
|
2243
|
+
const pkgPath = path8.join(__dirname2, "..", "package.json");
|
|
2244
|
+
const pkg = JSON.parse(fs7.readFileSync(pkgPath, "utf-8"));
|
|
1992
2245
|
const currentVersion = pkg.version;
|
|
1993
2246
|
const res = await fetch("https://registry.npmjs.org/@delt/claude-alarm/latest", {
|
|
1994
2247
|
signal: AbortSignal.timeout(3e3)
|
|
@@ -2011,28 +2264,28 @@ async function hubStart(daemon) {
|
|
|
2011
2264
|
const port = config2.hub.port ?? DEFAULT_HUB_PORT;
|
|
2012
2265
|
const displayHost = host === "0.0.0.0" ? "127.0.0.1" : host;
|
|
2013
2266
|
checkForUpdates();
|
|
2014
|
-
if (
|
|
2015
|
-
const pid = parseInt(
|
|
2267
|
+
if (fs7.existsSync(PID_FILE)) {
|
|
2268
|
+
const pid = parseInt(fs7.readFileSync(PID_FILE, "utf-8").trim(), 10);
|
|
2016
2269
|
if (isProcessRunning(pid)) {
|
|
2017
|
-
console.log(`Hub is already running (PID: ${pid})
|
|
2270
|
+
console.log(`Hub is already running (PID: ${pid}). Dashboard: ${loginLink(displayHost, port, config2.hub.token)}`);
|
|
2018
2271
|
return;
|
|
2019
2272
|
}
|
|
2020
|
-
|
|
2273
|
+
fs7.unlinkSync(PID_FILE);
|
|
2021
2274
|
}
|
|
2022
2275
|
if (daemon) {
|
|
2023
2276
|
ensureConfigDir();
|
|
2024
|
-
const logFd =
|
|
2025
|
-
const hubScript =
|
|
2026
|
-
const child =
|
|
2277
|
+
const logFd = fs7.openSync(LOG_FILE, "a");
|
|
2278
|
+
const hubScript = path8.join(__dirname2, "hub", "server.js");
|
|
2279
|
+
const child = spawn2(process.execPath, [hubScript], {
|
|
2027
2280
|
detached: true,
|
|
2028
2281
|
stdio: ["ignore", logFd, logFd],
|
|
2029
2282
|
env: { ...process.env }
|
|
2030
2283
|
});
|
|
2031
2284
|
if (child.pid) {
|
|
2032
|
-
|
|
2285
|
+
fs7.writeFileSync(PID_FILE, String(child.pid), "utf-8");
|
|
2033
2286
|
child.unref();
|
|
2034
2287
|
console.log(`Hub started as daemon (PID: ${child.pid})`);
|
|
2035
|
-
console.log(`Dashboard:
|
|
2288
|
+
console.log(`Dashboard: ${loginLink(displayHost, port, config2.hub.token)}`);
|
|
2036
2289
|
console.log(`Token: ${config2.hub.token}`);
|
|
2037
2290
|
console.log(`Logs: ${LOG_FILE}`);
|
|
2038
2291
|
} else {
|
|
@@ -2041,6 +2294,7 @@ async function hubStart(daemon) {
|
|
|
2041
2294
|
}
|
|
2042
2295
|
} else {
|
|
2043
2296
|
console.log(`Starting hub on http://${displayHost}:${port} (press Ctrl+C to stop)`);
|
|
2297
|
+
console.log(`Dashboard: ${loginLink(displayHost, port, config2.hub.token)}`);
|
|
2044
2298
|
console.log(`Token: ${config2.hub.token}`);
|
|
2045
2299
|
console.log(`Logs: ${LOG_FILE}`);
|
|
2046
2300
|
installCrashGuard("hub foreground");
|
|
@@ -2049,11 +2303,11 @@ async function hubStart(daemon) {
|
|
|
2049
2303
|
const hub = new HubServer2(config2);
|
|
2050
2304
|
await hub.start();
|
|
2051
2305
|
ensureConfigDir();
|
|
2052
|
-
|
|
2306
|
+
fs7.writeFileSync(PID_FILE, String(process.pid), "utf-8");
|
|
2053
2307
|
const shutdown = async () => {
|
|
2054
2308
|
console.log("\nShutting down...");
|
|
2055
2309
|
await hub.stop();
|
|
2056
|
-
if (
|
|
2310
|
+
if (fs7.existsSync(PID_FILE)) fs7.unlinkSync(PID_FILE);
|
|
2057
2311
|
process.exit(0);
|
|
2058
2312
|
};
|
|
2059
2313
|
process.on("SIGINT", shutdown);
|
|
@@ -2061,18 +2315,18 @@ async function hubStart(daemon) {
|
|
|
2061
2315
|
}
|
|
2062
2316
|
}
|
|
2063
2317
|
function hubStop() {
|
|
2064
|
-
if (!
|
|
2318
|
+
if (!fs7.existsSync(PID_FILE)) {
|
|
2065
2319
|
console.log("Hub is not running (no PID file found)");
|
|
2066
2320
|
return;
|
|
2067
2321
|
}
|
|
2068
|
-
const pid = parseInt(
|
|
2322
|
+
const pid = parseInt(fs7.readFileSync(PID_FILE, "utf-8").trim(), 10);
|
|
2069
2323
|
try {
|
|
2070
2324
|
process.kill(pid, "SIGTERM");
|
|
2071
2325
|
console.log(`Hub stopped (PID: ${pid})`);
|
|
2072
2326
|
} catch {
|
|
2073
2327
|
console.log("Hub process not found (may have already stopped)");
|
|
2074
2328
|
}
|
|
2075
|
-
|
|
2329
|
+
fs7.unlinkSync(PID_FILE);
|
|
2076
2330
|
}
|
|
2077
2331
|
async function hubStatus() {
|
|
2078
2332
|
const config2 = loadConfig();
|
|
@@ -2080,8 +2334,8 @@ async function hubStatus() {
|
|
|
2080
2334
|
const port = config2.hub.port ?? DEFAULT_HUB_PORT;
|
|
2081
2335
|
const displayHost = host === "0.0.0.0" ? "127.0.0.1" : host;
|
|
2082
2336
|
let pidInfo = "not running";
|
|
2083
|
-
if (
|
|
2084
|
-
const pid = parseInt(
|
|
2337
|
+
if (fs7.existsSync(PID_FILE)) {
|
|
2338
|
+
const pid = parseInt(fs7.readFileSync(PID_FILE, "utf-8").trim(), 10);
|
|
2085
2339
|
if (isProcessRunning(pid)) {
|
|
2086
2340
|
pidInfo = `running (PID: ${pid})`;
|
|
2087
2341
|
} else {
|
|
@@ -2089,7 +2343,7 @@ async function hubStatus() {
|
|
|
2089
2343
|
}
|
|
2090
2344
|
}
|
|
2091
2345
|
try {
|
|
2092
|
-
const res = await fetch(`http://${host}:${port}/api/status
|
|
2346
|
+
const res = await fetch(`http://${host}:${port}/api/status`, { headers: authHeaders(config2.hub.token) });
|
|
2093
2347
|
if (res.ok) {
|
|
2094
2348
|
const data = await res.json();
|
|
2095
2349
|
console.log(`Hub: running (PID: ${data.pid})`);
|
|
@@ -2153,7 +2407,7 @@ function ask(question) {
|
|
|
2153
2407
|
}
|
|
2154
2408
|
async function init() {
|
|
2155
2409
|
const dir = process.cwd();
|
|
2156
|
-
const projectName =
|
|
2410
|
+
const projectName = path8.basename(dir);
|
|
2157
2411
|
console.log(`
|
|
2158
2412
|
claude-alarm init for "${projectName}"
|
|
2159
2413
|
`);
|
|
@@ -2173,11 +2427,11 @@ claude-alarm init for "${projectName}"
|
|
|
2173
2427
|
if (port) env.CLAUDE_ALARM_HUB_PORT = port;
|
|
2174
2428
|
if (token) env.CLAUDE_ALARM_HUB_TOKEN = token;
|
|
2175
2429
|
}
|
|
2176
|
-
const mcpPath =
|
|
2430
|
+
const mcpPath = path8.join(dir, ".mcp.json");
|
|
2177
2431
|
let mcpConfig = {};
|
|
2178
|
-
if (
|
|
2432
|
+
if (fs7.existsSync(mcpPath)) {
|
|
2179
2433
|
try {
|
|
2180
|
-
mcpConfig = JSON.parse(
|
|
2434
|
+
mcpConfig = JSON.parse(fs7.readFileSync(mcpPath, "utf-8"));
|
|
2181
2435
|
} catch {
|
|
2182
2436
|
mcpConfig = {};
|
|
2183
2437
|
}
|
|
@@ -2188,7 +2442,7 @@ claude-alarm init for "${projectName}"
|
|
|
2188
2442
|
args: ["-y", "@delt/claude-alarm", "serve"],
|
|
2189
2443
|
env
|
|
2190
2444
|
};
|
|
2191
|
-
|
|
2445
|
+
fs7.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2), "utf-8");
|
|
2192
2446
|
console.log(`
|
|
2193
2447
|
\u2713 Created ${mcpPath}`);
|
|
2194
2448
|
if (remote.toLowerCase() !== "y") {
|
|
@@ -2198,7 +2452,7 @@ claude-alarm init for "${projectName}"
|
|
|
2198
2452
|
const displayHost = host === "0.0.0.0" ? "127.0.0.1" : host;
|
|
2199
2453
|
let hubRunning = false;
|
|
2200
2454
|
try {
|
|
2201
|
-
const res = await fetch(`http://${host}:${port}/api/status
|
|
2455
|
+
const res = await fetch(`http://${host}:${port}/api/status`, { headers: authHeaders(config2.hub.token) });
|
|
2202
2456
|
hubRunning = res.ok;
|
|
2203
2457
|
} catch {
|
|
2204
2458
|
}
|
|
@@ -2233,6 +2487,54 @@ function isProcessRunning(pid) {
|
|
|
2233
2487
|
return false;
|
|
2234
2488
|
}
|
|
2235
2489
|
}
|
|
2490
|
+
function readCodexPid() {
|
|
2491
|
+
if (!fs7.existsSync(CODEX_PID_FILE)) return void 0;
|
|
2492
|
+
const pid = parseInt(fs7.readFileSync(CODEX_PID_FILE, "utf-8").trim(), 10);
|
|
2493
|
+
return Number.isNaN(pid) ? void 0 : pid;
|
|
2494
|
+
}
|
|
2495
|
+
function codexEnable(enabled) {
|
|
2496
|
+
setCodexEnabled(enabled);
|
|
2497
|
+
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.");
|
|
2498
|
+
}
|
|
2499
|
+
function codexStart() {
|
|
2500
|
+
const pid = readCodexPid();
|
|
2501
|
+
if (pid !== void 0 && isProcessRunning(pid)) {
|
|
2502
|
+
console.log(`Codex adapter is already running (PID: ${pid})`);
|
|
2503
|
+
return;
|
|
2504
|
+
}
|
|
2505
|
+
ensureConfigDir();
|
|
2506
|
+
const logFd = fs7.openSync(CODEX_LOG_FILE, "a");
|
|
2507
|
+
const child = spawn2(process.execPath, [path8.join(__dirname2, "codex", "main.js")], {
|
|
2508
|
+
detached: true,
|
|
2509
|
+
stdio: ["ignore", logFd, logFd],
|
|
2510
|
+
env: { ...process.env }
|
|
2511
|
+
});
|
|
2512
|
+
child.unref();
|
|
2513
|
+
console.log(`Codex adapter started (PID: ${child.pid}). Logs: ${CODEX_LOG_FILE}`);
|
|
2514
|
+
}
|
|
2515
|
+
function codexStop() {
|
|
2516
|
+
const pid = readCodexPid();
|
|
2517
|
+
if (pid === void 0) {
|
|
2518
|
+
console.log("Codex adapter is not running");
|
|
2519
|
+
return;
|
|
2520
|
+
}
|
|
2521
|
+
try {
|
|
2522
|
+
process.kill(pid, "SIGTERM");
|
|
2523
|
+
console.log(`Codex adapter stopped (PID: ${pid})`);
|
|
2524
|
+
} catch {
|
|
2525
|
+
console.log("Codex adapter process not found (may have already stopped)");
|
|
2526
|
+
}
|
|
2527
|
+
try {
|
|
2528
|
+
fs7.unlinkSync(CODEX_PID_FILE);
|
|
2529
|
+
} catch {
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
function codexStatus() {
|
|
2533
|
+
const pid = readCodexPid();
|
|
2534
|
+
const state = pid === void 0 ? "not running" : isProcessRunning(pid) ? `running (PID: ${pid})` : "not running (stale PID file)";
|
|
2535
|
+
console.log(`Codex adapter: ${state}`);
|
|
2536
|
+
console.log(`Start with hub: ${loadConfig().codex?.enabled ? "enabled" : "disabled"}`);
|
|
2537
|
+
}
|
|
2236
2538
|
async function main2() {
|
|
2237
2539
|
const args = process.argv.slice(2);
|
|
2238
2540
|
const cmd = args[0];
|
|
@@ -2264,6 +2566,19 @@ async function main2() {
|
|
|
2264
2566
|
}
|
|
2265
2567
|
return;
|
|
2266
2568
|
}
|
|
2569
|
+
if (cmd === "codex") {
|
|
2570
|
+
if (sub === "enable") codexEnable(true);
|
|
2571
|
+
else if (sub === "disable") codexEnable(false);
|
|
2572
|
+
else if (sub === "start") codexStart();
|
|
2573
|
+
else if (sub === "stop") codexStop();
|
|
2574
|
+
else if (sub === "status") codexStatus();
|
|
2575
|
+
else {
|
|
2576
|
+
console.error(`Unknown codex command: ${sub}`);
|
|
2577
|
+
printUsage();
|
|
2578
|
+
process.exit(1);
|
|
2579
|
+
}
|
|
2580
|
+
return;
|
|
2581
|
+
}
|
|
2267
2582
|
if (cmd === "setup") {
|
|
2268
2583
|
setup(args[1]);
|
|
2269
2584
|
return;
|