@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/hub/server.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
// src/hub/server.ts
|
|
4
4
|
import http from "http";
|
|
5
|
-
import
|
|
6
|
-
import
|
|
5
|
+
import fs5 from "fs";
|
|
6
|
+
import path5 from "path";
|
|
7
7
|
import { fileURLToPath } from "url";
|
|
8
8
|
import { WebSocketServer, WebSocket } from "ws";
|
|
9
9
|
|
|
@@ -38,6 +38,8 @@ var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
|
38
38
|
var PID_FILE = path.join(CONFIG_DIR, "hub.pid");
|
|
39
39
|
var LOG_FILE = path.join(CONFIG_DIR, "hub.log");
|
|
40
40
|
var UPLOADS_DIR = path.join(CONFIG_DIR, "uploads");
|
|
41
|
+
var CODEX_PID_FILE = path.join(CONFIG_DIR, "codex.pid");
|
|
42
|
+
var CODEX_LOG_FILE = path.join(CONFIG_DIR, "codex.log");
|
|
41
43
|
var WS_PATH_CHANNEL = "/ws/channel";
|
|
42
44
|
var WS_PATH_DASHBOARD = "/ws/dashboard";
|
|
43
45
|
|
|
@@ -51,9 +53,9 @@ function isValidPeerName(name) {
|
|
|
51
53
|
var SessionManager = class {
|
|
52
54
|
sessions = /* @__PURE__ */ new Map();
|
|
53
55
|
register(session) {
|
|
54
|
-
const baseName =
|
|
56
|
+
const baseName = this.baseName(session);
|
|
55
57
|
const existing = Array.from(this.sessions.values()).filter(
|
|
56
|
-
(s) => s.id !== session.id &&
|
|
58
|
+
(s) => s.id !== session.id && this.baseName(s) === baseName
|
|
57
59
|
);
|
|
58
60
|
if (existing.length > 0) {
|
|
59
61
|
const usedNums = existing.map((s) => {
|
|
@@ -107,6 +109,9 @@ var SessionManager = class {
|
|
|
107
109
|
count() {
|
|
108
110
|
return this.sessions.size;
|
|
109
111
|
}
|
|
112
|
+
baseName(session) {
|
|
113
|
+
return session.title || session.cwd?.replace(/^.*[/\\]/, "") || session.name;
|
|
114
|
+
}
|
|
110
115
|
};
|
|
111
116
|
|
|
112
117
|
// src/hub/notifier.ts
|
|
@@ -127,7 +132,7 @@ var Notifier = class {
|
|
|
127
132
|
async notify(title, message, level = "info") {
|
|
128
133
|
await this.notifyWithSession(void 0, void 0, title, message, level);
|
|
129
134
|
}
|
|
130
|
-
async notifyWithSession(sessionId,
|
|
135
|
+
async notifyWithSession(sessionId, sessionLabel2, title, message, level = "info") {
|
|
131
136
|
const promises = [];
|
|
132
137
|
if (this.desktopEnabled) {
|
|
133
138
|
promises.push(this.sendDesktop(title, message, level));
|
|
@@ -135,8 +140,8 @@ var Notifier = class {
|
|
|
135
140
|
for (const webhook of this.webhooks) {
|
|
136
141
|
promises.push(this.sendWebhook(webhook, title, message, level));
|
|
137
142
|
}
|
|
138
|
-
if (this.telegramBot && sessionId &&
|
|
139
|
-
promises.push(this.telegramBot.sendNotification(sessionId,
|
|
143
|
+
if (this.telegramBot && sessionId && sessionLabel2) {
|
|
144
|
+
promises.push(this.telegramBot.sendNotification(sessionId, sessionLabel2, title, message));
|
|
140
145
|
}
|
|
141
146
|
await Promise.allSettled(promises);
|
|
142
147
|
}
|
|
@@ -222,6 +227,14 @@ var Notifier = class {
|
|
|
222
227
|
import fs from "fs";
|
|
223
228
|
import path2 from "path";
|
|
224
229
|
import { randomUUID } from "crypto";
|
|
230
|
+
|
|
231
|
+
// src/shared/session-label.ts
|
|
232
|
+
function sessionLabel(session) {
|
|
233
|
+
const base = session.displayName || session.cwd?.replace(/^.*[/\\]/, "") || session.name;
|
|
234
|
+
return session.agentKind === "codex" ? `Codex \xB7 ${base}` : base;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// src/hub/telegram.ts
|
|
225
238
|
var TELEGRAM_API = "https://api.telegram.org/bot";
|
|
226
239
|
var TelegramBot = class _TelegramBot {
|
|
227
240
|
config;
|
|
@@ -360,15 +373,14 @@ ${this.mdToHtml(message)}`;
|
|
|
360
373
|
const pending = this.pendingMessages.get(msg.chat.id);
|
|
361
374
|
if (pending) {
|
|
362
375
|
this.pendingMessages.delete(msg.chat.id);
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
if (idx >= 0 && idx < sessions2.length) {
|
|
376
|
+
const session = this.pendingSession(pending, parseInt(selectMatch[1], 10) - 1);
|
|
377
|
+
if (session) {
|
|
366
378
|
if (pending.photoFileId) {
|
|
367
|
-
await this.deliverPhotoToSessionByFileId(
|
|
379
|
+
await this.deliverPhotoToSessionByFileId(session.id, pending.photoFileId, pending.caption);
|
|
368
380
|
} else if (pending.text) {
|
|
369
|
-
this.deliverToSession(
|
|
381
|
+
this.deliverToSession(session.id, pending.text);
|
|
370
382
|
}
|
|
371
|
-
this.sendMessage(`Sent to [${this.getLabel(
|
|
383
|
+
this.sendMessage(`Sent to [${this.getLabel(session)}]`);
|
|
372
384
|
} else {
|
|
373
385
|
this.sendMessage("Invalid session number.");
|
|
374
386
|
}
|
|
@@ -389,11 +401,12 @@ ${this.mdToHtml(message)}`;
|
|
|
389
401
|
}
|
|
390
402
|
return;
|
|
391
403
|
}
|
|
404
|
+
const sessionIds = sessions.map((s) => s.id);
|
|
392
405
|
if (hasPhoto) {
|
|
393
406
|
const largest = msg.photo[msg.photo.length - 1];
|
|
394
|
-
this.pendingMessages.set(msg.chat.id, { photoFileId: largest.file_id, caption: text });
|
|
407
|
+
this.pendingMessages.set(msg.chat.id, { photoFileId: largest.file_id, caption: text, sessionIds });
|
|
395
408
|
} else {
|
|
396
|
-
this.pendingMessages.set(msg.chat.id, { text });
|
|
409
|
+
this.pendingMessages.set(msg.chat.id, { text, sessionIds });
|
|
397
410
|
}
|
|
398
411
|
const buttons = sessions.map((s, i) => ({
|
|
399
412
|
text: this.getLabel(s),
|
|
@@ -450,11 +463,16 @@ ${this.mdToHtml(message)}`;
|
|
|
450
463
|
logger.warn(`Telegram photo download failed: ${err.message}`);
|
|
451
464
|
}
|
|
452
465
|
}
|
|
466
|
+
// Buttons are numbered against the list shown when they were sent; Codex sessions come and go, so the live list may have shifted.
|
|
467
|
+
pendingSession(pending, idx) {
|
|
468
|
+
const id = pending.sessionIds[idx];
|
|
469
|
+
return id ? this.getSessions?.().find((s) => s.id === id) : void 0;
|
|
470
|
+
}
|
|
453
471
|
getLabel(session) {
|
|
454
|
-
return session
|
|
472
|
+
return sessionLabel(session);
|
|
455
473
|
}
|
|
456
474
|
/** Send a permission request with inline buttons */
|
|
457
|
-
async sendPermissionRequest(sessionId,
|
|
475
|
+
async sendPermissionRequest(sessionId, sessionLabel2, requestId, toolName, description, inputPreview) {
|
|
458
476
|
const isNotify = toolName.endsWith("__notify") || toolName === "notify";
|
|
459
477
|
let preview = inputPreview;
|
|
460
478
|
let kind = "code";
|
|
@@ -507,7 +525,7 @@ ${this.mdToHtml(message)}`;
|
|
|
507
525
|
const mcpMatch = toolName.match(/__([^_]+)$/);
|
|
508
526
|
if (mcpMatch) displayTool = mcpMatch[1].charAt(0).toUpperCase() + mcpMatch[1].slice(1);
|
|
509
527
|
}
|
|
510
|
-
const text = `\u26A0\uFE0F <b>Permission Request</b> \u2014 ${this.escHtml(
|
|
528
|
+
const text = `\u26A0\uFE0F <b>Permission Request</b> \u2014 ${this.escHtml(sessionLabel2)}
|
|
511
529
|
|
|
512
530
|
\u{1F527} <b>${this.escHtml(displayTool)}</b>
|
|
513
531
|
${previewHtml}${truncNote}`;
|
|
@@ -521,6 +539,7 @@ ${previewHtml}${truncNote}`;
|
|
|
521
539
|
}
|
|
522
540
|
async handleCallbackQuery(query) {
|
|
523
541
|
if (!query.data) return;
|
|
542
|
+
if (String(query.message?.chat.id) !== String(this.config.chatId)) return;
|
|
524
543
|
if (query.data.startsWith("sess:")) {
|
|
525
544
|
await this.handleSessionSelectCallback(query);
|
|
526
545
|
return;
|
|
@@ -546,22 +565,18 @@ ${label}`);
|
|
|
546
565
|
const parts = query.data.split(":");
|
|
547
566
|
if (parts.length < 3) return;
|
|
548
567
|
const [, idxStr, chatIdStr] = parts;
|
|
549
|
-
const idx = parseInt(idxStr, 10);
|
|
550
568
|
const chatId = parseInt(chatIdStr, 10);
|
|
551
|
-
const
|
|
552
|
-
|
|
569
|
+
const pending = this.pendingMessages.get(chatId);
|
|
570
|
+
const session = pending ? this.pendingSession(pending, parseInt(idxStr, 10)) : void 0;
|
|
571
|
+
if (!pending || !session) {
|
|
553
572
|
await this.answerCallbackQuery(query.id, "Session not found");
|
|
554
573
|
return;
|
|
555
574
|
}
|
|
556
|
-
const session = sessions[idx];
|
|
557
|
-
const pending = this.pendingMessages.get(chatId);
|
|
558
575
|
this.pendingMessages.delete(chatId);
|
|
559
|
-
if (pending) {
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
this.deliverToSession(session.id, pending.text);
|
|
564
|
-
}
|
|
576
|
+
if (pending.photoFileId) {
|
|
577
|
+
await this.deliverPhotoToSessionByFileId(session.id, pending.photoFileId, pending.caption);
|
|
578
|
+
} else if (pending.text) {
|
|
579
|
+
this.deliverToSession(session.id, pending.text);
|
|
565
580
|
}
|
|
566
581
|
await this.answerCallbackQuery(query.id, `Sent to ${this.getLabel(session)}`);
|
|
567
582
|
if (query.message) {
|
|
@@ -623,8 +638,8 @@ ${label}`);
|
|
|
623
638
|
css: "css",
|
|
624
639
|
scss: "scss"
|
|
625
640
|
};
|
|
626
|
-
langOf(
|
|
627
|
-
const m = /\.([a-z0-9]+)$/i.exec(
|
|
641
|
+
langOf(path6) {
|
|
642
|
+
const m = /\.([a-z0-9]+)$/i.exec(path6 || "");
|
|
628
643
|
return m ? _TelegramBot.LANG_BY_EXT[m[1].toLowerCase()] ?? "" : "";
|
|
629
644
|
}
|
|
630
645
|
/** Convert markdown to Telegram HTML (escape first, then apply formatting) */
|
|
@@ -667,9 +682,73 @@ ${bodyLines.join("\n")}`;
|
|
|
667
682
|
}
|
|
668
683
|
};
|
|
669
684
|
|
|
670
|
-
// src/
|
|
685
|
+
// src/hub/codex-supervisor.ts
|
|
686
|
+
import { spawn } from "child_process";
|
|
671
687
|
import fs2 from "fs";
|
|
672
688
|
import path3 from "path";
|
|
689
|
+
var HEALTHY_RUN_MS = 6e4;
|
|
690
|
+
function resolveAdapterScript(baseDir) {
|
|
691
|
+
return [path3.join(baseDir, "..", "codex", "main.js"), path3.join(baseDir, "codex", "main.js")].find((p) => fs2.existsSync(p));
|
|
692
|
+
}
|
|
693
|
+
var CodexSupervisor = class {
|
|
694
|
+
constructor(script, spawnFn = spawn, minDelayMs = 2e3, maxDelayMs = 6e4, stopGraceMs = 3e3) {
|
|
695
|
+
this.script = script;
|
|
696
|
+
this.spawnFn = spawnFn;
|
|
697
|
+
this.minDelayMs = minDelayMs;
|
|
698
|
+
this.maxDelayMs = maxDelayMs;
|
|
699
|
+
this.stopGraceMs = stopGraceMs;
|
|
700
|
+
this.delay = minDelayMs;
|
|
701
|
+
}
|
|
702
|
+
child;
|
|
703
|
+
timer;
|
|
704
|
+
stopped = true;
|
|
705
|
+
delay;
|
|
706
|
+
start() {
|
|
707
|
+
this.stopped = false;
|
|
708
|
+
this.launch();
|
|
709
|
+
}
|
|
710
|
+
stop() {
|
|
711
|
+
this.stopped = true;
|
|
712
|
+
if (this.timer) clearTimeout(this.timer);
|
|
713
|
+
this.timer = void 0;
|
|
714
|
+
const child = this.child;
|
|
715
|
+
this.child = void 0;
|
|
716
|
+
if (!child) return;
|
|
717
|
+
child.stdin?.end();
|
|
718
|
+
const force = setTimeout(() => child.kill(), this.stopGraceMs);
|
|
719
|
+
force.unref();
|
|
720
|
+
child.once("exit", () => clearTimeout(force));
|
|
721
|
+
}
|
|
722
|
+
launch() {
|
|
723
|
+
const startedAt = Date.now();
|
|
724
|
+
const child = this.spawnFn(process.execPath, [this.script, "--watch-stdin"], {
|
|
725
|
+
detached: true,
|
|
726
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
727
|
+
windowsHide: true
|
|
728
|
+
});
|
|
729
|
+
this.child = child;
|
|
730
|
+
child.stdout?.on("data", (d) => process.stdout.write(d));
|
|
731
|
+
child.stderr?.on("data", (d) => process.stderr.write(d));
|
|
732
|
+
child.stdin?.on("error", () => {
|
|
733
|
+
});
|
|
734
|
+
child.on("error", (err) => logger.warn(`Codex adapter failed to start: ${err.message}`));
|
|
735
|
+
child.on("exit", (code, signal) => {
|
|
736
|
+
if (this.child === child) this.child = void 0;
|
|
737
|
+
if (this.stopped || code === 0) return;
|
|
738
|
+
if (Date.now() - startedAt > HEALTHY_RUN_MS) this.delay = this.minDelayMs;
|
|
739
|
+
logger.warn(`Codex adapter exited (${signal ?? code}); restarting in ${this.delay}ms`);
|
|
740
|
+
this.timer = setTimeout(() => {
|
|
741
|
+
this.timer = void 0;
|
|
742
|
+
if (!this.stopped) this.launch();
|
|
743
|
+
}, this.delay);
|
|
744
|
+
this.delay = Math.min(this.delay * 2, this.maxDelayMs);
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
};
|
|
748
|
+
|
|
749
|
+
// src/shared/config.ts
|
|
750
|
+
import fs3 from "fs";
|
|
751
|
+
import path4 from "path";
|
|
673
752
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
674
753
|
var DEFAULT_CONFIG = {
|
|
675
754
|
hub: {
|
|
@@ -683,18 +762,18 @@ var DEFAULT_CONFIG = {
|
|
|
683
762
|
webhooks: []
|
|
684
763
|
};
|
|
685
764
|
function ensureConfigDir() {
|
|
686
|
-
if (!
|
|
687
|
-
|
|
765
|
+
if (!fs3.existsSync(CONFIG_DIR)) {
|
|
766
|
+
fs3.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
688
767
|
}
|
|
689
768
|
}
|
|
690
769
|
function loadConfig() {
|
|
691
770
|
ensureConfigDir();
|
|
692
771
|
let config;
|
|
693
|
-
if (!
|
|
772
|
+
if (!fs3.existsSync(CONFIG_FILE)) {
|
|
694
773
|
config = { ...DEFAULT_CONFIG, hub: { ...DEFAULT_CONFIG.hub } };
|
|
695
774
|
} else {
|
|
696
775
|
try {
|
|
697
|
-
const raw =
|
|
776
|
+
const raw = fs3.readFileSync(CONFIG_FILE, "utf-8");
|
|
698
777
|
const parsed = JSON.parse(raw);
|
|
699
778
|
config = { ...DEFAULT_CONFIG, ...parsed, hub: { ...DEFAULT_CONFIG.hub, ...parsed.hub }, ...parsed.telegram ? { telegram: parsed.telegram } : {} };
|
|
700
779
|
} catch {
|
|
@@ -709,17 +788,17 @@ function loadConfig() {
|
|
|
709
788
|
}
|
|
710
789
|
function saveConfig(config) {
|
|
711
790
|
ensureConfigDir();
|
|
712
|
-
|
|
791
|
+
fs3.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { encoding: "utf-8", mode: 384 });
|
|
713
792
|
}
|
|
714
793
|
|
|
715
794
|
// src/shared/crash-guard.ts
|
|
716
|
-
import
|
|
795
|
+
import fs4 from "fs";
|
|
717
796
|
var BURST_WINDOW_MS = 6e4;
|
|
718
797
|
var BURST_LIMIT = 10;
|
|
719
798
|
function appendToLog(text) {
|
|
720
799
|
try {
|
|
721
|
-
|
|
722
|
-
|
|
800
|
+
fs4.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
801
|
+
fs4.appendFileSync(LOG_FILE, text, "utf-8");
|
|
723
802
|
} catch {
|
|
724
803
|
}
|
|
725
804
|
}
|
|
@@ -760,8 +839,66 @@ function logStartup(context) {
|
|
|
760
839
|
`);
|
|
761
840
|
}
|
|
762
841
|
|
|
842
|
+
// src/hub/auth.ts
|
|
843
|
+
import crypto from "crypto";
|
|
844
|
+
var SESSION_COOKIE = "ca_session";
|
|
845
|
+
var COOKIE_MAX_AGE = 30 * 24 * 60 * 60;
|
|
846
|
+
function safeEqual(a, b) {
|
|
847
|
+
if (typeof a !== "string") return false;
|
|
848
|
+
const x = Buffer.from(a);
|
|
849
|
+
const y = Buffer.from(b);
|
|
850
|
+
return x.length === y.length && crypto.timingSafeEqual(x, y);
|
|
851
|
+
}
|
|
852
|
+
function dashboardCookieValue(token) {
|
|
853
|
+
return crypto.createHmac("sha256", token).update("claude-alarm-dashboard").digest("hex");
|
|
854
|
+
}
|
|
855
|
+
function parseCookies(header) {
|
|
856
|
+
const out = {};
|
|
857
|
+
for (const part of (header ?? "").split(";")) {
|
|
858
|
+
const i = part.indexOf("=");
|
|
859
|
+
if (i <= 0) continue;
|
|
860
|
+
const raw = part.slice(i + 1).trim();
|
|
861
|
+
let value = raw;
|
|
862
|
+
try {
|
|
863
|
+
value = decodeURIComponent(raw);
|
|
864
|
+
} catch {
|
|
865
|
+
}
|
|
866
|
+
out[part.slice(0, i).trim()] = value;
|
|
867
|
+
}
|
|
868
|
+
return out;
|
|
869
|
+
}
|
|
870
|
+
function sessionCookieHeader(token, secure) {
|
|
871
|
+
return `${SESSION_COOKIE}=${dashboardCookieValue(token)}; HttpOnly; SameSite=Strict; Path=/; Max-Age=${COOKIE_MAX_AGE}${secure ? "; Secure" : ""}`;
|
|
872
|
+
}
|
|
873
|
+
function isAuthorized(req, token, allowQueryToken) {
|
|
874
|
+
const auth = req.headers.authorization;
|
|
875
|
+
if (auth?.startsWith("Bearer ") && safeEqual(auth.slice(7), token)) return true;
|
|
876
|
+
if (safeEqual(parseCookies(req.headers.cookie)[SESSION_COOKIE], dashboardCookieValue(token))) return true;
|
|
877
|
+
if (allowQueryToken) {
|
|
878
|
+
const q = new URL(req.url ?? "/", "http://hub").searchParams.get("token");
|
|
879
|
+
if (safeEqual(q, token)) return true;
|
|
880
|
+
}
|
|
881
|
+
return false;
|
|
882
|
+
}
|
|
883
|
+
function isCrossOrigin(req) {
|
|
884
|
+
const origin = req.headers.origin;
|
|
885
|
+
if (!origin) return false;
|
|
886
|
+
try {
|
|
887
|
+
return new URL(origin).host !== req.headers.host;
|
|
888
|
+
} catch {
|
|
889
|
+
return true;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
function isJsonRequest(req) {
|
|
893
|
+
return String(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json");
|
|
894
|
+
}
|
|
895
|
+
function isSecureRequest(req) {
|
|
896
|
+
return req.headers["x-forwarded-proto"] === "https";
|
|
897
|
+
}
|
|
898
|
+
|
|
763
899
|
// src/hub/server.ts
|
|
764
|
-
var __dirname =
|
|
900
|
+
var __dirname = path5.dirname(fileURLToPath(import.meta.url));
|
|
901
|
+
var MAX_WS_PAYLOAD = 16 * 1024 * 1024;
|
|
765
902
|
var HubServer = class {
|
|
766
903
|
httpServer;
|
|
767
904
|
wssChannel;
|
|
@@ -779,13 +916,17 @@ var HubServer = class {
|
|
|
779
916
|
heartbeatInterval;
|
|
780
917
|
channelAlive = /* @__PURE__ */ new Map();
|
|
781
918
|
// sessionId -> alive flag
|
|
919
|
+
socketOwners = /* @__PURE__ */ new WeakMap();
|
|
782
920
|
host;
|
|
783
921
|
port;
|
|
784
922
|
token;
|
|
923
|
+
codexEnabled;
|
|
924
|
+
codexSupervisor;
|
|
785
925
|
constructor(config) {
|
|
786
926
|
this.host = config?.hub?.host ?? DEFAULT_HUB_HOST;
|
|
787
927
|
this.port = config?.hub?.port ?? DEFAULT_HUB_PORT;
|
|
788
928
|
this.token = config?.hub?.token;
|
|
929
|
+
this.codexEnabled = config?.codex?.enabled === true;
|
|
789
930
|
if (config?.notifications) {
|
|
790
931
|
this.notifier.configure({
|
|
791
932
|
desktop: config.notifications.desktop
|
|
@@ -801,22 +942,23 @@ var HubServer = class {
|
|
|
801
942
|
this.initTelegram(fullConfig.telegram);
|
|
802
943
|
}
|
|
803
944
|
this.httpServer = http.createServer((req, res) => this.handleHttp(req, res));
|
|
804
|
-
this.wssChannel = new WebSocketServer({ noServer: true });
|
|
945
|
+
this.wssChannel = new WebSocketServer({ noServer: true, maxPayload: MAX_WS_PAYLOAD });
|
|
805
946
|
this.wssChannel.on("connection", (ws, req) => this.handleChannelConnection(ws, req));
|
|
806
947
|
this.wssChannel.on("error", (err) => logger.warn(`Channel WebSocket server error: ${err.message}`));
|
|
807
|
-
this.wssDashboard = new WebSocketServer({ noServer: true });
|
|
948
|
+
this.wssDashboard = new WebSocketServer({ noServer: true, maxPayload: MAX_WS_PAYLOAD });
|
|
808
949
|
this.wssDashboard.on("connection", (ws) => this.handleDashboardConnection(ws));
|
|
809
950
|
this.wssDashboard.on("error", (err) => logger.warn(`Dashboard WebSocket server error: ${err.message}`));
|
|
810
951
|
this.httpServer.on("upgrade", (req, socket, head) => {
|
|
811
952
|
socket.on("error", (err) => logger.debug(`Upgrade socket error: ${err.message}`));
|
|
812
|
-
const url = new URL(req.url,
|
|
953
|
+
const url = new URL(req.url ?? "/", "http://hub");
|
|
813
954
|
const pathname = url.pathname;
|
|
814
|
-
if (
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
955
|
+
if (isCrossOrigin(req)) {
|
|
956
|
+
this.rejectUpgrade(socket, 403, "Forbidden", pathname, req);
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
if (!this.authorized(req, true)) {
|
|
960
|
+
this.rejectUpgrade(socket, 401, "Unauthorized", pathname, req);
|
|
961
|
+
return;
|
|
820
962
|
}
|
|
821
963
|
if (pathname === WS_PATH_CHANNEL) {
|
|
822
964
|
this.wssChannel.handleUpgrade(req, socket, head, (ws) => {
|
|
@@ -842,12 +984,15 @@ var HubServer = class {
|
|
|
842
984
|
this.httpServer.on("error", (err) => logger.error(`HTTP server error: ${err.message}`));
|
|
843
985
|
const displayHost = this.host === "0.0.0.0" ? "127.0.0.1" : this.host;
|
|
844
986
|
logger.info(`Hub server listening on http://${displayHost}:${this.port}`);
|
|
987
|
+
this.startCodexAdapter();
|
|
845
988
|
resolve();
|
|
846
989
|
});
|
|
847
990
|
});
|
|
848
991
|
}
|
|
849
992
|
stop() {
|
|
850
993
|
return new Promise((resolve) => {
|
|
994
|
+
this.codexSupervisor?.stop();
|
|
995
|
+
this.codexSupervisor = void 0;
|
|
851
996
|
if (this.heartbeatInterval) clearInterval(this.heartbeatInterval);
|
|
852
997
|
if (this.telegramBot) this.telegramBot.stopPolling();
|
|
853
998
|
for (const ws of this.channelSockets.values()) ws.terminate();
|
|
@@ -868,31 +1013,28 @@ var HubServer = class {
|
|
|
868
1013
|
}
|
|
869
1014
|
// --- HTTP Handler ---
|
|
870
1015
|
handleHttp(req, res) {
|
|
871
|
-
const url = new URL(req.url,
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
875
|
-
}
|
|
876
|
-
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
877
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
878
|
-
if (req.method === "OPTIONS") {
|
|
879
|
-
res.writeHead(204);
|
|
880
|
-
res.end();
|
|
1016
|
+
const url = new URL(req.url ?? "/", "http://hub");
|
|
1017
|
+
if (isCrossOrigin(req)) {
|
|
1018
|
+
this.jsonResponse(res, 403, { error: "Cross-origin request rejected" });
|
|
881
1019
|
return;
|
|
882
1020
|
}
|
|
883
|
-
if (
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
const bearerToken = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
|
|
887
|
-
if (bearerToken !== this.token) {
|
|
888
|
-
this.jsonResponse(res, 401, { error: "Unauthorized" });
|
|
889
|
-
return;
|
|
890
|
-
}
|
|
891
|
-
}
|
|
1021
|
+
if (req.method === "POST" && !isJsonRequest(req)) {
|
|
1022
|
+
this.jsonResponse(res, 415, { error: "Content-Type must be application/json" });
|
|
1023
|
+
return;
|
|
892
1024
|
}
|
|
893
1025
|
if (url.pathname === "/" && req.method === "GET") {
|
|
894
|
-
this.serveDashboard(res);
|
|
895
|
-
|
|
1026
|
+
this.serveDashboard(req, res, url);
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
if (url.pathname === "/api/login" && req.method === "POST") {
|
|
1030
|
+
this.handleLogin(req, res);
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
if (!this.authorized(req, false)) {
|
|
1034
|
+
this.jsonResponse(res, 401, { error: "Unauthorized" });
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
if (url.pathname === "/api/sessions" && req.method === "GET") {
|
|
896
1038
|
this.jsonResponse(res, 200, { sessions: this.sessions.getAll() });
|
|
897
1039
|
} else if (url.pathname === "/api/status" && req.method === "GET") {
|
|
898
1040
|
this.jsonResponse(res, 200, {
|
|
@@ -943,25 +1085,31 @@ var HubServer = class {
|
|
|
943
1085
|
this.jsonResponse(res, 404, { error: "Not found" });
|
|
944
1086
|
}
|
|
945
1087
|
}
|
|
946
|
-
serveDashboard(res) {
|
|
1088
|
+
serveDashboard(req, res, url) {
|
|
1089
|
+
const linkToken = url.searchParams.get("token");
|
|
1090
|
+
if (linkToken !== null && this.token && safeEqual(linkToken, this.token)) {
|
|
1091
|
+
res.writeHead(302, { Location: "/", "Set-Cookie": sessionCookieHeader(this.token, isSecureRequest(req)) });
|
|
1092
|
+
res.end();
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
947
1095
|
const candidates = [
|
|
948
|
-
|
|
1096
|
+
path5.join(__dirname, "..", "dashboard", "index.html"),
|
|
949
1097
|
// from dist/hub/
|
|
950
|
-
|
|
1098
|
+
path5.join(__dirname, "dashboard", "index.html"),
|
|
951
1099
|
// from dist/ (bundled index.js)
|
|
952
|
-
|
|
1100
|
+
path5.join(__dirname, "..", "..", "src", "dashboard", "index.html"),
|
|
953
1101
|
// from dist/hub/ -> src/
|
|
954
|
-
|
|
1102
|
+
path5.join(__dirname, "..", "src", "dashboard", "index.html"),
|
|
955
1103
|
// from dist/ -> src/
|
|
956
|
-
|
|
1104
|
+
path5.join(process.cwd(), "dist", "dashboard", "index.html"),
|
|
957
1105
|
// from cwd
|
|
958
|
-
|
|
1106
|
+
path5.join(process.cwd(), "src", "dashboard", "index.html")
|
|
959
1107
|
// from cwd/src
|
|
960
1108
|
];
|
|
961
1109
|
logger.debug(`Dashboard candidates: ${JSON.stringify(candidates)}`);
|
|
962
1110
|
for (const candidate of candidates) {
|
|
963
|
-
if (
|
|
964
|
-
const html =
|
|
1111
|
+
if (fs5.existsSync(candidate)) {
|
|
1112
|
+
const html = fs5.readFileSync(candidate, "utf-8");
|
|
965
1113
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
966
1114
|
res.end(html);
|
|
967
1115
|
return;
|
|
@@ -970,6 +1118,20 @@ var HubServer = class {
|
|
|
970
1118
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
971
1119
|
res.end("<html><body><h1>claude-alarm</h1><p>Dashboard HTML not found. Reinstall the package.</p></body></html>");
|
|
972
1120
|
}
|
|
1121
|
+
async handleLogin(req, res) {
|
|
1122
|
+
const body = await this.readBody(req);
|
|
1123
|
+
if (!this.token) {
|
|
1124
|
+
res.writeHead(204);
|
|
1125
|
+
res.end();
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
if (typeof body?.token !== "string" || !safeEqual(body.token, this.token)) {
|
|
1129
|
+
this.jsonResponse(res, 401, { error: "Unauthorized" });
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
res.writeHead(204, { "Set-Cookie": sessionCookieHeader(this.token, isSecureRequest(req)) });
|
|
1133
|
+
res.end();
|
|
1134
|
+
}
|
|
973
1135
|
async handleApiSend(req, res) {
|
|
974
1136
|
const body = await this.readBody(req);
|
|
975
1137
|
if (!body) {
|
|
@@ -986,7 +1148,7 @@ var HubServer = class {
|
|
|
986
1148
|
this.jsonResponse(res, 404, { error: "Session not connected" });
|
|
987
1149
|
return;
|
|
988
1150
|
}
|
|
989
|
-
const msg = { type: "message_to_session", sessionId, content };
|
|
1151
|
+
const msg = { type: "message_to_session", sessionId, content, source: "api" };
|
|
990
1152
|
ws.send(JSON.stringify(msg));
|
|
991
1153
|
this.jsonResponse(res, 200, { ok: true });
|
|
992
1154
|
}
|
|
@@ -1046,6 +1208,22 @@ var HubServer = class {
|
|
|
1046
1208
|
});
|
|
1047
1209
|
}
|
|
1048
1210
|
handleChannelMessage(ws, isLocal, msg) {
|
|
1211
|
+
if (msg.type === "register") {
|
|
1212
|
+
const id = msg.session.id;
|
|
1213
|
+
const owned = this.socketOwners.get(ws);
|
|
1214
|
+
if (owned && owned !== id) {
|
|
1215
|
+
logger.warn(`Rejected register for ${id}: connection already owns ${owned}`);
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
const holder = this.channelSockets.get(id);
|
|
1219
|
+
if (holder && holder !== ws) {
|
|
1220
|
+
this.socketOwners.delete(holder);
|
|
1221
|
+
holder.terminate();
|
|
1222
|
+
}
|
|
1223
|
+
this.socketOwners.set(ws, id);
|
|
1224
|
+
} else if ("sessionId" in msg && this.socketOwners.get(ws) !== msg.sessionId) {
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1049
1227
|
switch (msg.type) {
|
|
1050
1228
|
case "register": {
|
|
1051
1229
|
const session = msg.session;
|
|
@@ -1069,7 +1247,6 @@ var HubServer = class {
|
|
|
1069
1247
|
break;
|
|
1070
1248
|
}
|
|
1071
1249
|
case "peer_name": {
|
|
1072
|
-
if (this.channelSockets.get(msg.sessionId) !== ws) break;
|
|
1073
1250
|
const updated = this.sessions.setPeerName(msg.sessionId, msg.peerName);
|
|
1074
1251
|
if (updated) {
|
|
1075
1252
|
logger.info(`Peer name for ${msg.sessionId}: ${msg.peerName ?? "-"}`);
|
|
@@ -1145,7 +1322,7 @@ var HubServer = class {
|
|
|
1145
1322
|
if (msg.type === "message_to_session") {
|
|
1146
1323
|
const channelWs = this.channelSockets.get(msg.sessionId);
|
|
1147
1324
|
if (channelWs?.readyState === WebSocket.OPEN) {
|
|
1148
|
-
channelWs.send(JSON.stringify(msg));
|
|
1325
|
+
channelWs.send(JSON.stringify({ ...msg, source: "dashboard" }));
|
|
1149
1326
|
}
|
|
1150
1327
|
} else if (msg.type === "image_upload") {
|
|
1151
1328
|
this.handleImageUpload(msg);
|
|
@@ -1190,11 +1367,11 @@ var HubServer = class {
|
|
|
1190
1367
|
logger.warn("Image upload rejected: exceeds 10MB");
|
|
1191
1368
|
return;
|
|
1192
1369
|
}
|
|
1193
|
-
|
|
1370
|
+
fs5.mkdirSync(UPLOADS_DIR, { recursive: true });
|
|
1194
1371
|
const ext = mimeType.split("/")[1] === "jpeg" ? "jpg" : mimeType.split("/")[1];
|
|
1195
1372
|
const filename = `${randomUUID3()}.${ext}`;
|
|
1196
|
-
const filePath =
|
|
1197
|
-
|
|
1373
|
+
const filePath = path5.join(UPLOADS_DIR, filename);
|
|
1374
|
+
fs5.writeFileSync(filePath, buffer);
|
|
1198
1375
|
const forwardMsg = {
|
|
1199
1376
|
type: "image_to_session",
|
|
1200
1377
|
sessionId,
|
|
@@ -1207,7 +1384,7 @@ var HubServer = class {
|
|
|
1207
1384
|
logger.info(`Image saved and forwarded: ${filename} (${buffer.length} bytes)`);
|
|
1208
1385
|
setTimeout(() => {
|
|
1209
1386
|
try {
|
|
1210
|
-
|
|
1387
|
+
fs5.unlinkSync(filePath);
|
|
1211
1388
|
} catch {
|
|
1212
1389
|
}
|
|
1213
1390
|
}, 5 * 60 * 1e3);
|
|
@@ -1235,7 +1412,7 @@ var HubServer = class {
|
|
|
1235
1412
|
this.telegramBot.onMessageToSession = (sessionId, content) => {
|
|
1236
1413
|
const channelWs = this.channelSockets.get(sessionId);
|
|
1237
1414
|
if (channelWs?.readyState === WebSocket.OPEN) {
|
|
1238
|
-
const msg = { type: "message_to_session", sessionId, content };
|
|
1415
|
+
const msg = { type: "message_to_session", sessionId, content, source: "telegram" };
|
|
1239
1416
|
channelWs.send(JSON.stringify(msg));
|
|
1240
1417
|
logger.info(`Telegram message forwarded to session: ${sessionId}`);
|
|
1241
1418
|
}
|
|
@@ -1362,6 +1539,17 @@ var HubServer = class {
|
|
|
1362
1539
|
this.jsonResponse(res, 500, { error: err.message });
|
|
1363
1540
|
}
|
|
1364
1541
|
}
|
|
1542
|
+
startCodexAdapter() {
|
|
1543
|
+
if (!this.codexEnabled) return;
|
|
1544
|
+
const script = resolveAdapterScript(__dirname);
|
|
1545
|
+
if (!script) {
|
|
1546
|
+
logger.warn("Codex adapter is enabled but codex/main.js was not found next to the hub");
|
|
1547
|
+
return;
|
|
1548
|
+
}
|
|
1549
|
+
this.codexSupervisor = new CodexSupervisor(script);
|
|
1550
|
+
this.codexSupervisor.start();
|
|
1551
|
+
logger.info("Codex adapter started");
|
|
1552
|
+
}
|
|
1365
1553
|
startHeartbeat() {
|
|
1366
1554
|
this.heartbeatInterval = setInterval(() => {
|
|
1367
1555
|
for (const [sessionId, ws] of this.channelSockets) {
|
|
@@ -1377,11 +1565,11 @@ var HubServer = class {
|
|
|
1377
1565
|
}
|
|
1378
1566
|
cleanupUploads() {
|
|
1379
1567
|
try {
|
|
1380
|
-
if (!
|
|
1381
|
-
const files =
|
|
1568
|
+
if (!fs5.existsSync(UPLOADS_DIR)) return;
|
|
1569
|
+
const files = fs5.readdirSync(UPLOADS_DIR);
|
|
1382
1570
|
for (const file of files) {
|
|
1383
1571
|
try {
|
|
1384
|
-
|
|
1572
|
+
fs5.unlinkSync(path5.join(UPLOADS_DIR, file));
|
|
1385
1573
|
} catch {
|
|
1386
1574
|
}
|
|
1387
1575
|
}
|
|
@@ -1390,13 +1578,24 @@ var HubServer = class {
|
|
|
1390
1578
|
}
|
|
1391
1579
|
}
|
|
1392
1580
|
getSessionLabel(session) {
|
|
1393
|
-
|
|
1394
|
-
return session.displayName || session.cwd?.replace(/^.*[/\\]/, "") || session.name;
|
|
1581
|
+
return session ? sessionLabel(session) : "unknown";
|
|
1395
1582
|
}
|
|
1396
1583
|
jsonResponse(res, status, body) {
|
|
1397
1584
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
1398
1585
|
res.end(JSON.stringify(body));
|
|
1399
1586
|
}
|
|
1587
|
+
authorized(req, allowQueryToken) {
|
|
1588
|
+
return !this.token || isAuthorized(req, this.token, allowQueryToken);
|
|
1589
|
+
}
|
|
1590
|
+
rejectUpgrade(socket, status, text, pathname, req) {
|
|
1591
|
+
logger.warn(`Rejected ${pathname} upgrade (${status}) from ${req.socket.remoteAddress}`);
|
|
1592
|
+
socket.write(`HTTP/1.1 ${status} ${text}\r
|
|
1593
|
+
Connection: close\r
|
|
1594
|
+
Content-Length: 0\r
|
|
1595
|
+
\r
|
|
1596
|
+
`);
|
|
1597
|
+
socket.destroy();
|
|
1598
|
+
}
|
|
1400
1599
|
isLocalRequest(req) {
|
|
1401
1600
|
const addr = req.socket.remoteAddress;
|
|
1402
1601
|
return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
|