@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/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/hub/server.ts
|
|
2
2
|
import http from "http";
|
|
3
|
-
import
|
|
4
|
-
import
|
|
3
|
+
import fs5 from "fs";
|
|
4
|
+
import path5 from "path";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
6
|
import { WebSocketServer, WebSocket } from "ws";
|
|
7
7
|
|
|
@@ -36,6 +36,8 @@ var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
|
36
36
|
var PID_FILE = path.join(CONFIG_DIR, "hub.pid");
|
|
37
37
|
var LOG_FILE = path.join(CONFIG_DIR, "hub.log");
|
|
38
38
|
var UPLOADS_DIR = path.join(CONFIG_DIR, "uploads");
|
|
39
|
+
var CODEX_PID_FILE = path.join(CONFIG_DIR, "codex.pid");
|
|
40
|
+
var CODEX_LOG_FILE = path.join(CONFIG_DIR, "codex.log");
|
|
39
41
|
var WS_PATH_CHANNEL = "/ws/channel";
|
|
40
42
|
var WS_PATH_DASHBOARD = "/ws/dashboard";
|
|
41
43
|
var CHANNEL_SERVER_NAME = "claude-alarm";
|
|
@@ -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,15 +788,15 @@ 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
|
function setupMcpConfig(targetDir) {
|
|
715
794
|
const dir = targetDir ?? process.cwd();
|
|
716
|
-
const mcpPath =
|
|
795
|
+
const mcpPath = path4.join(dir, ".mcp.json");
|
|
717
796
|
let mcpConfig = {};
|
|
718
|
-
if (
|
|
797
|
+
if (fs3.existsSync(mcpPath)) {
|
|
719
798
|
try {
|
|
720
|
-
mcpConfig = JSON.parse(
|
|
799
|
+
mcpConfig = JSON.parse(fs3.readFileSync(mcpPath, "utf-8"));
|
|
721
800
|
} catch {
|
|
722
801
|
mcpConfig = {};
|
|
723
802
|
}
|
|
@@ -729,21 +808,21 @@ function setupMcpConfig(targetDir) {
|
|
|
729
808
|
command: "npx",
|
|
730
809
|
args: ["-y", "@delt/claude-alarm", "serve"],
|
|
731
810
|
env: {
|
|
732
|
-
CLAUDE_ALARM_SESSION_NAME:
|
|
811
|
+
CLAUDE_ALARM_SESSION_NAME: path4.basename(dir)
|
|
733
812
|
}
|
|
734
813
|
};
|
|
735
|
-
|
|
814
|
+
fs3.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2), "utf-8");
|
|
736
815
|
return mcpPath;
|
|
737
816
|
}
|
|
738
817
|
|
|
739
818
|
// src/shared/crash-guard.ts
|
|
740
|
-
import
|
|
819
|
+
import fs4 from "fs";
|
|
741
820
|
var BURST_WINDOW_MS = 6e4;
|
|
742
821
|
var BURST_LIMIT = 10;
|
|
743
822
|
function appendToLog(text) {
|
|
744
823
|
try {
|
|
745
|
-
|
|
746
|
-
|
|
824
|
+
fs4.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
825
|
+
fs4.appendFileSync(LOG_FILE, text, "utf-8");
|
|
747
826
|
} catch {
|
|
748
827
|
}
|
|
749
828
|
}
|
|
@@ -784,8 +863,66 @@ function logStartup(context) {
|
|
|
784
863
|
`);
|
|
785
864
|
}
|
|
786
865
|
|
|
866
|
+
// src/hub/auth.ts
|
|
867
|
+
import crypto from "crypto";
|
|
868
|
+
var SESSION_COOKIE = "ca_session";
|
|
869
|
+
var COOKIE_MAX_AGE = 30 * 24 * 60 * 60;
|
|
870
|
+
function safeEqual(a, b) {
|
|
871
|
+
if (typeof a !== "string") return false;
|
|
872
|
+
const x = Buffer.from(a);
|
|
873
|
+
const y = Buffer.from(b);
|
|
874
|
+
return x.length === y.length && crypto.timingSafeEqual(x, y);
|
|
875
|
+
}
|
|
876
|
+
function dashboardCookieValue(token) {
|
|
877
|
+
return crypto.createHmac("sha256", token).update("claude-alarm-dashboard").digest("hex");
|
|
878
|
+
}
|
|
879
|
+
function parseCookies(header) {
|
|
880
|
+
const out = {};
|
|
881
|
+
for (const part of (header ?? "").split(";")) {
|
|
882
|
+
const i = part.indexOf("=");
|
|
883
|
+
if (i <= 0) continue;
|
|
884
|
+
const raw = part.slice(i + 1).trim();
|
|
885
|
+
let value = raw;
|
|
886
|
+
try {
|
|
887
|
+
value = decodeURIComponent(raw);
|
|
888
|
+
} catch {
|
|
889
|
+
}
|
|
890
|
+
out[part.slice(0, i).trim()] = value;
|
|
891
|
+
}
|
|
892
|
+
return out;
|
|
893
|
+
}
|
|
894
|
+
function sessionCookieHeader(token, secure) {
|
|
895
|
+
return `${SESSION_COOKIE}=${dashboardCookieValue(token)}; HttpOnly; SameSite=Strict; Path=/; Max-Age=${COOKIE_MAX_AGE}${secure ? "; Secure" : ""}`;
|
|
896
|
+
}
|
|
897
|
+
function isAuthorized(req, token, allowQueryToken) {
|
|
898
|
+
const auth = req.headers.authorization;
|
|
899
|
+
if (auth?.startsWith("Bearer ") && safeEqual(auth.slice(7), token)) return true;
|
|
900
|
+
if (safeEqual(parseCookies(req.headers.cookie)[SESSION_COOKIE], dashboardCookieValue(token))) return true;
|
|
901
|
+
if (allowQueryToken) {
|
|
902
|
+
const q = new URL(req.url ?? "/", "http://hub").searchParams.get("token");
|
|
903
|
+
if (safeEqual(q, token)) return true;
|
|
904
|
+
}
|
|
905
|
+
return false;
|
|
906
|
+
}
|
|
907
|
+
function isCrossOrigin(req) {
|
|
908
|
+
const origin = req.headers.origin;
|
|
909
|
+
if (!origin) return false;
|
|
910
|
+
try {
|
|
911
|
+
return new URL(origin).host !== req.headers.host;
|
|
912
|
+
} catch {
|
|
913
|
+
return true;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
function isJsonRequest(req) {
|
|
917
|
+
return String(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json");
|
|
918
|
+
}
|
|
919
|
+
function isSecureRequest(req) {
|
|
920
|
+
return req.headers["x-forwarded-proto"] === "https";
|
|
921
|
+
}
|
|
922
|
+
|
|
787
923
|
// src/hub/server.ts
|
|
788
|
-
var __dirname =
|
|
924
|
+
var __dirname = path5.dirname(fileURLToPath(import.meta.url));
|
|
925
|
+
var MAX_WS_PAYLOAD = 16 * 1024 * 1024;
|
|
789
926
|
var HubServer = class {
|
|
790
927
|
httpServer;
|
|
791
928
|
wssChannel;
|
|
@@ -803,13 +940,17 @@ var HubServer = class {
|
|
|
803
940
|
heartbeatInterval;
|
|
804
941
|
channelAlive = /* @__PURE__ */ new Map();
|
|
805
942
|
// sessionId -> alive flag
|
|
943
|
+
socketOwners = /* @__PURE__ */ new WeakMap();
|
|
806
944
|
host;
|
|
807
945
|
port;
|
|
808
946
|
token;
|
|
947
|
+
codexEnabled;
|
|
948
|
+
codexSupervisor;
|
|
809
949
|
constructor(config) {
|
|
810
950
|
this.host = config?.hub?.host ?? DEFAULT_HUB_HOST;
|
|
811
951
|
this.port = config?.hub?.port ?? DEFAULT_HUB_PORT;
|
|
812
952
|
this.token = config?.hub?.token;
|
|
953
|
+
this.codexEnabled = config?.codex?.enabled === true;
|
|
813
954
|
if (config?.notifications) {
|
|
814
955
|
this.notifier.configure({
|
|
815
956
|
desktop: config.notifications.desktop
|
|
@@ -825,22 +966,23 @@ var HubServer = class {
|
|
|
825
966
|
this.initTelegram(fullConfig.telegram);
|
|
826
967
|
}
|
|
827
968
|
this.httpServer = http.createServer((req, res) => this.handleHttp(req, res));
|
|
828
|
-
this.wssChannel = new WebSocketServer({ noServer: true });
|
|
969
|
+
this.wssChannel = new WebSocketServer({ noServer: true, maxPayload: MAX_WS_PAYLOAD });
|
|
829
970
|
this.wssChannel.on("connection", (ws, req) => this.handleChannelConnection(ws, req));
|
|
830
971
|
this.wssChannel.on("error", (err) => logger.warn(`Channel WebSocket server error: ${err.message}`));
|
|
831
|
-
this.wssDashboard = new WebSocketServer({ noServer: true });
|
|
972
|
+
this.wssDashboard = new WebSocketServer({ noServer: true, maxPayload: MAX_WS_PAYLOAD });
|
|
832
973
|
this.wssDashboard.on("connection", (ws) => this.handleDashboardConnection(ws));
|
|
833
974
|
this.wssDashboard.on("error", (err) => logger.warn(`Dashboard WebSocket server error: ${err.message}`));
|
|
834
975
|
this.httpServer.on("upgrade", (req, socket, head) => {
|
|
835
976
|
socket.on("error", (err) => logger.debug(`Upgrade socket error: ${err.message}`));
|
|
836
|
-
const url = new URL(req.url,
|
|
977
|
+
const url = new URL(req.url ?? "/", "http://hub");
|
|
837
978
|
const pathname = url.pathname;
|
|
838
|
-
if (
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
979
|
+
if (isCrossOrigin(req)) {
|
|
980
|
+
this.rejectUpgrade(socket, 403, "Forbidden", pathname, req);
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
if (!this.authorized(req, true)) {
|
|
984
|
+
this.rejectUpgrade(socket, 401, "Unauthorized", pathname, req);
|
|
985
|
+
return;
|
|
844
986
|
}
|
|
845
987
|
if (pathname === WS_PATH_CHANNEL) {
|
|
846
988
|
this.wssChannel.handleUpgrade(req, socket, head, (ws) => {
|
|
@@ -866,12 +1008,15 @@ var HubServer = class {
|
|
|
866
1008
|
this.httpServer.on("error", (err) => logger.error(`HTTP server error: ${err.message}`));
|
|
867
1009
|
const displayHost = this.host === "0.0.0.0" ? "127.0.0.1" : this.host;
|
|
868
1010
|
logger.info(`Hub server listening on http://${displayHost}:${this.port}`);
|
|
1011
|
+
this.startCodexAdapter();
|
|
869
1012
|
resolve();
|
|
870
1013
|
});
|
|
871
1014
|
});
|
|
872
1015
|
}
|
|
873
1016
|
stop() {
|
|
874
1017
|
return new Promise((resolve) => {
|
|
1018
|
+
this.codexSupervisor?.stop();
|
|
1019
|
+
this.codexSupervisor = void 0;
|
|
875
1020
|
if (this.heartbeatInterval) clearInterval(this.heartbeatInterval);
|
|
876
1021
|
if (this.telegramBot) this.telegramBot.stopPolling();
|
|
877
1022
|
for (const ws of this.channelSockets.values()) ws.terminate();
|
|
@@ -892,31 +1037,28 @@ var HubServer = class {
|
|
|
892
1037
|
}
|
|
893
1038
|
// --- HTTP Handler ---
|
|
894
1039
|
handleHttp(req, res) {
|
|
895
|
-
const url = new URL(req.url,
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
899
|
-
}
|
|
900
|
-
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
901
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
902
|
-
if (req.method === "OPTIONS") {
|
|
903
|
-
res.writeHead(204);
|
|
904
|
-
res.end();
|
|
1040
|
+
const url = new URL(req.url ?? "/", "http://hub");
|
|
1041
|
+
if (isCrossOrigin(req)) {
|
|
1042
|
+
this.jsonResponse(res, 403, { error: "Cross-origin request rejected" });
|
|
905
1043
|
return;
|
|
906
1044
|
}
|
|
907
|
-
if (
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
const bearerToken = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
|
|
911
|
-
if (bearerToken !== this.token) {
|
|
912
|
-
this.jsonResponse(res, 401, { error: "Unauthorized" });
|
|
913
|
-
return;
|
|
914
|
-
}
|
|
915
|
-
}
|
|
1045
|
+
if (req.method === "POST" && !isJsonRequest(req)) {
|
|
1046
|
+
this.jsonResponse(res, 415, { error: "Content-Type must be application/json" });
|
|
1047
|
+
return;
|
|
916
1048
|
}
|
|
917
1049
|
if (url.pathname === "/" && req.method === "GET") {
|
|
918
|
-
this.serveDashboard(res);
|
|
919
|
-
|
|
1050
|
+
this.serveDashboard(req, res, url);
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
if (url.pathname === "/api/login" && req.method === "POST") {
|
|
1054
|
+
this.handleLogin(req, res);
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
if (!this.authorized(req, false)) {
|
|
1058
|
+
this.jsonResponse(res, 401, { error: "Unauthorized" });
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
if (url.pathname === "/api/sessions" && req.method === "GET") {
|
|
920
1062
|
this.jsonResponse(res, 200, { sessions: this.sessions.getAll() });
|
|
921
1063
|
} else if (url.pathname === "/api/status" && req.method === "GET") {
|
|
922
1064
|
this.jsonResponse(res, 200, {
|
|
@@ -967,25 +1109,31 @@ var HubServer = class {
|
|
|
967
1109
|
this.jsonResponse(res, 404, { error: "Not found" });
|
|
968
1110
|
}
|
|
969
1111
|
}
|
|
970
|
-
serveDashboard(res) {
|
|
1112
|
+
serveDashboard(req, res, url) {
|
|
1113
|
+
const linkToken = url.searchParams.get("token");
|
|
1114
|
+
if (linkToken !== null && this.token && safeEqual(linkToken, this.token)) {
|
|
1115
|
+
res.writeHead(302, { Location: "/", "Set-Cookie": sessionCookieHeader(this.token, isSecureRequest(req)) });
|
|
1116
|
+
res.end();
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
971
1119
|
const candidates = [
|
|
972
|
-
|
|
1120
|
+
path5.join(__dirname, "..", "dashboard", "index.html"),
|
|
973
1121
|
// from dist/hub/
|
|
974
|
-
|
|
1122
|
+
path5.join(__dirname, "dashboard", "index.html"),
|
|
975
1123
|
// from dist/ (bundled index.js)
|
|
976
|
-
|
|
1124
|
+
path5.join(__dirname, "..", "..", "src", "dashboard", "index.html"),
|
|
977
1125
|
// from dist/hub/ -> src/
|
|
978
|
-
|
|
1126
|
+
path5.join(__dirname, "..", "src", "dashboard", "index.html"),
|
|
979
1127
|
// from dist/ -> src/
|
|
980
|
-
|
|
1128
|
+
path5.join(process.cwd(), "dist", "dashboard", "index.html"),
|
|
981
1129
|
// from cwd
|
|
982
|
-
|
|
1130
|
+
path5.join(process.cwd(), "src", "dashboard", "index.html")
|
|
983
1131
|
// from cwd/src
|
|
984
1132
|
];
|
|
985
1133
|
logger.debug(`Dashboard candidates: ${JSON.stringify(candidates)}`);
|
|
986
1134
|
for (const candidate of candidates) {
|
|
987
|
-
if (
|
|
988
|
-
const html =
|
|
1135
|
+
if (fs5.existsSync(candidate)) {
|
|
1136
|
+
const html = fs5.readFileSync(candidate, "utf-8");
|
|
989
1137
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
990
1138
|
res.end(html);
|
|
991
1139
|
return;
|
|
@@ -994,6 +1142,20 @@ var HubServer = class {
|
|
|
994
1142
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
995
1143
|
res.end("<html><body><h1>claude-alarm</h1><p>Dashboard HTML not found. Reinstall the package.</p></body></html>");
|
|
996
1144
|
}
|
|
1145
|
+
async handleLogin(req, res) {
|
|
1146
|
+
const body = await this.readBody(req);
|
|
1147
|
+
if (!this.token) {
|
|
1148
|
+
res.writeHead(204);
|
|
1149
|
+
res.end();
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
if (typeof body?.token !== "string" || !safeEqual(body.token, this.token)) {
|
|
1153
|
+
this.jsonResponse(res, 401, { error: "Unauthorized" });
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
res.writeHead(204, { "Set-Cookie": sessionCookieHeader(this.token, isSecureRequest(req)) });
|
|
1157
|
+
res.end();
|
|
1158
|
+
}
|
|
997
1159
|
async handleApiSend(req, res) {
|
|
998
1160
|
const body = await this.readBody(req);
|
|
999
1161
|
if (!body) {
|
|
@@ -1010,7 +1172,7 @@ var HubServer = class {
|
|
|
1010
1172
|
this.jsonResponse(res, 404, { error: "Session not connected" });
|
|
1011
1173
|
return;
|
|
1012
1174
|
}
|
|
1013
|
-
const msg = { type: "message_to_session", sessionId, content };
|
|
1175
|
+
const msg = { type: "message_to_session", sessionId, content, source: "api" };
|
|
1014
1176
|
ws.send(JSON.stringify(msg));
|
|
1015
1177
|
this.jsonResponse(res, 200, { ok: true });
|
|
1016
1178
|
}
|
|
@@ -1070,6 +1232,22 @@ var HubServer = class {
|
|
|
1070
1232
|
});
|
|
1071
1233
|
}
|
|
1072
1234
|
handleChannelMessage(ws, isLocal, msg) {
|
|
1235
|
+
if (msg.type === "register") {
|
|
1236
|
+
const id = msg.session.id;
|
|
1237
|
+
const owned = this.socketOwners.get(ws);
|
|
1238
|
+
if (owned && owned !== id) {
|
|
1239
|
+
logger.warn(`Rejected register for ${id}: connection already owns ${owned}`);
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
const holder = this.channelSockets.get(id);
|
|
1243
|
+
if (holder && holder !== ws) {
|
|
1244
|
+
this.socketOwners.delete(holder);
|
|
1245
|
+
holder.terminate();
|
|
1246
|
+
}
|
|
1247
|
+
this.socketOwners.set(ws, id);
|
|
1248
|
+
} else if ("sessionId" in msg && this.socketOwners.get(ws) !== msg.sessionId) {
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1073
1251
|
switch (msg.type) {
|
|
1074
1252
|
case "register": {
|
|
1075
1253
|
const session = msg.session;
|
|
@@ -1093,7 +1271,6 @@ var HubServer = class {
|
|
|
1093
1271
|
break;
|
|
1094
1272
|
}
|
|
1095
1273
|
case "peer_name": {
|
|
1096
|
-
if (this.channelSockets.get(msg.sessionId) !== ws) break;
|
|
1097
1274
|
const updated = this.sessions.setPeerName(msg.sessionId, msg.peerName);
|
|
1098
1275
|
if (updated) {
|
|
1099
1276
|
logger.info(`Peer name for ${msg.sessionId}: ${msg.peerName ?? "-"}`);
|
|
@@ -1169,7 +1346,7 @@ var HubServer = class {
|
|
|
1169
1346
|
if (msg.type === "message_to_session") {
|
|
1170
1347
|
const channelWs = this.channelSockets.get(msg.sessionId);
|
|
1171
1348
|
if (channelWs?.readyState === WebSocket.OPEN) {
|
|
1172
|
-
channelWs.send(JSON.stringify(msg));
|
|
1349
|
+
channelWs.send(JSON.stringify({ ...msg, source: "dashboard" }));
|
|
1173
1350
|
}
|
|
1174
1351
|
} else if (msg.type === "image_upload") {
|
|
1175
1352
|
this.handleImageUpload(msg);
|
|
@@ -1214,11 +1391,11 @@ var HubServer = class {
|
|
|
1214
1391
|
logger.warn("Image upload rejected: exceeds 10MB");
|
|
1215
1392
|
return;
|
|
1216
1393
|
}
|
|
1217
|
-
|
|
1394
|
+
fs5.mkdirSync(UPLOADS_DIR, { recursive: true });
|
|
1218
1395
|
const ext = mimeType.split("/")[1] === "jpeg" ? "jpg" : mimeType.split("/")[1];
|
|
1219
1396
|
const filename = `${randomUUID3()}.${ext}`;
|
|
1220
|
-
const filePath =
|
|
1221
|
-
|
|
1397
|
+
const filePath = path5.join(UPLOADS_DIR, filename);
|
|
1398
|
+
fs5.writeFileSync(filePath, buffer);
|
|
1222
1399
|
const forwardMsg = {
|
|
1223
1400
|
type: "image_to_session",
|
|
1224
1401
|
sessionId,
|
|
@@ -1231,7 +1408,7 @@ var HubServer = class {
|
|
|
1231
1408
|
logger.info(`Image saved and forwarded: ${filename} (${buffer.length} bytes)`);
|
|
1232
1409
|
setTimeout(() => {
|
|
1233
1410
|
try {
|
|
1234
|
-
|
|
1411
|
+
fs5.unlinkSync(filePath);
|
|
1235
1412
|
} catch {
|
|
1236
1413
|
}
|
|
1237
1414
|
}, 5 * 60 * 1e3);
|
|
@@ -1259,7 +1436,7 @@ var HubServer = class {
|
|
|
1259
1436
|
this.telegramBot.onMessageToSession = (sessionId, content) => {
|
|
1260
1437
|
const channelWs = this.channelSockets.get(sessionId);
|
|
1261
1438
|
if (channelWs?.readyState === WebSocket.OPEN) {
|
|
1262
|
-
const msg = { type: "message_to_session", sessionId, content };
|
|
1439
|
+
const msg = { type: "message_to_session", sessionId, content, source: "telegram" };
|
|
1263
1440
|
channelWs.send(JSON.stringify(msg));
|
|
1264
1441
|
logger.info(`Telegram message forwarded to session: ${sessionId}`);
|
|
1265
1442
|
}
|
|
@@ -1386,6 +1563,17 @@ var HubServer = class {
|
|
|
1386
1563
|
this.jsonResponse(res, 500, { error: err.message });
|
|
1387
1564
|
}
|
|
1388
1565
|
}
|
|
1566
|
+
startCodexAdapter() {
|
|
1567
|
+
if (!this.codexEnabled) return;
|
|
1568
|
+
const script = resolveAdapterScript(__dirname);
|
|
1569
|
+
if (!script) {
|
|
1570
|
+
logger.warn("Codex adapter is enabled but codex/main.js was not found next to the hub");
|
|
1571
|
+
return;
|
|
1572
|
+
}
|
|
1573
|
+
this.codexSupervisor = new CodexSupervisor(script);
|
|
1574
|
+
this.codexSupervisor.start();
|
|
1575
|
+
logger.info("Codex adapter started");
|
|
1576
|
+
}
|
|
1389
1577
|
startHeartbeat() {
|
|
1390
1578
|
this.heartbeatInterval = setInterval(() => {
|
|
1391
1579
|
for (const [sessionId, ws] of this.channelSockets) {
|
|
@@ -1401,11 +1589,11 @@ var HubServer = class {
|
|
|
1401
1589
|
}
|
|
1402
1590
|
cleanupUploads() {
|
|
1403
1591
|
try {
|
|
1404
|
-
if (!
|
|
1405
|
-
const files =
|
|
1592
|
+
if (!fs5.existsSync(UPLOADS_DIR)) return;
|
|
1593
|
+
const files = fs5.readdirSync(UPLOADS_DIR);
|
|
1406
1594
|
for (const file of files) {
|
|
1407
1595
|
try {
|
|
1408
|
-
|
|
1596
|
+
fs5.unlinkSync(path5.join(UPLOADS_DIR, file));
|
|
1409
1597
|
} catch {
|
|
1410
1598
|
}
|
|
1411
1599
|
}
|
|
@@ -1414,13 +1602,24 @@ var HubServer = class {
|
|
|
1414
1602
|
}
|
|
1415
1603
|
}
|
|
1416
1604
|
getSessionLabel(session) {
|
|
1417
|
-
|
|
1418
|
-
return session.displayName || session.cwd?.replace(/^.*[/\\]/, "") || session.name;
|
|
1605
|
+
return session ? sessionLabel(session) : "unknown";
|
|
1419
1606
|
}
|
|
1420
1607
|
jsonResponse(res, status, body) {
|
|
1421
1608
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
1422
1609
|
res.end(JSON.stringify(body));
|
|
1423
1610
|
}
|
|
1611
|
+
authorized(req, allowQueryToken) {
|
|
1612
|
+
return !this.token || isAuthorized(req, this.token, allowQueryToken);
|
|
1613
|
+
}
|
|
1614
|
+
rejectUpgrade(socket, status, text, pathname, req) {
|
|
1615
|
+
logger.warn(`Rejected ${pathname} upgrade (${status}) from ${req.socket.remoteAddress}`);
|
|
1616
|
+
socket.write(`HTTP/1.1 ${status} ${text}\r
|
|
1617
|
+
Connection: close\r
|
|
1618
|
+
Content-Length: 0\r
|
|
1619
|
+
\r
|
|
1620
|
+
`);
|
|
1621
|
+
socket.destroy();
|
|
1622
|
+
}
|
|
1424
1623
|
isLocalRequest(req) {
|
|
1425
1624
|
const addr = req.socket.remoteAddress;
|
|
1426
1625
|
return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
|
|
@@ -1467,48 +1666,41 @@ if (process.argv[1] && (process.argv[1].endsWith("hub/server.js") || process.arg
|
|
|
1467
1666
|
// src/channel/hub-client.ts
|
|
1468
1667
|
import WebSocket2 from "ws";
|
|
1469
1668
|
var HubClient = class {
|
|
1470
|
-
constructor(sessionId, sessionName, hubHost = DEFAULT_HUB_HOST, hubPort = DEFAULT_HUB_PORT, token, getPeerName = () => void 0) {
|
|
1669
|
+
constructor(sessionId, sessionName, hubHost = DEFAULT_HUB_HOST, hubPort = DEFAULT_HUB_PORT, token, getPeerName = () => void 0, getRegistration = () => ({})) {
|
|
1471
1670
|
this.sessionId = sessionId;
|
|
1472
1671
|
this.sessionName = sessionName;
|
|
1473
1672
|
this.hubHost = hubHost;
|
|
1474
1673
|
this.hubPort = hubPort;
|
|
1475
1674
|
this.token = token;
|
|
1476
1675
|
this.getPeerName = getPeerName;
|
|
1676
|
+
this.getRegistration = getRegistration;
|
|
1477
1677
|
}
|
|
1478
1678
|
ws = null;
|
|
1479
1679
|
reconnectTimer = null;
|
|
1480
1680
|
messageHandlers = [];
|
|
1481
1681
|
queue = [];
|
|
1482
1682
|
connected = false;
|
|
1683
|
+
closed = false;
|
|
1483
1684
|
connect() {
|
|
1685
|
+
this.closed = false;
|
|
1484
1686
|
const tokenQuery = this.token ? `?token=${encodeURIComponent(this.token)}` : "";
|
|
1485
1687
|
const url = `ws://${this.hubHost}:${this.hubPort}${WS_PATH_CHANNEL}${tokenQuery}`;
|
|
1486
1688
|
logger.debug(`Connecting to hub at ${url}`);
|
|
1487
1689
|
try {
|
|
1488
|
-
|
|
1489
|
-
this.ws
|
|
1690
|
+
const ws = new WebSocket2(url);
|
|
1691
|
+
this.ws = ws;
|
|
1692
|
+
ws.on("open", () => {
|
|
1693
|
+
if (this.ws !== ws) return;
|
|
1490
1694
|
logger.info("Connected to hub");
|
|
1491
1695
|
this.connected = true;
|
|
1492
|
-
|
|
1493
|
-
type: "register",
|
|
1494
|
-
session: {
|
|
1495
|
-
id: this.sessionId,
|
|
1496
|
-
name: this.sessionName,
|
|
1497
|
-
status: "idle",
|
|
1498
|
-
connectedAt: Date.now(),
|
|
1499
|
-
lastActivity: Date.now(),
|
|
1500
|
-
cwd: process.cwd(),
|
|
1501
|
-
channelEnabled: true,
|
|
1502
|
-
peerName: this.getPeerName()
|
|
1503
|
-
}
|
|
1504
|
-
};
|
|
1505
|
-
this.ws.send(JSON.stringify(registration));
|
|
1696
|
+
this.ws.send(JSON.stringify(this.registration()));
|
|
1506
1697
|
for (const msg of this.queue) {
|
|
1507
1698
|
this.ws.send(JSON.stringify(msg));
|
|
1508
1699
|
}
|
|
1509
1700
|
this.queue = [];
|
|
1510
1701
|
});
|
|
1511
|
-
|
|
1702
|
+
ws.on("message", (data) => {
|
|
1703
|
+
if (this.ws !== ws) return;
|
|
1512
1704
|
try {
|
|
1513
1705
|
const msg = JSON.parse(data.toString());
|
|
1514
1706
|
for (const handler of this.messageHandlers) {
|
|
@@ -1518,12 +1710,14 @@ var HubClient = class {
|
|
|
1518
1710
|
logger.warn("Failed to parse hub message:", err);
|
|
1519
1711
|
}
|
|
1520
1712
|
});
|
|
1521
|
-
|
|
1713
|
+
ws.on("close", () => {
|
|
1714
|
+
if (this.ws !== ws) return;
|
|
1522
1715
|
logger.info("Disconnected from hub");
|
|
1523
1716
|
this.connected = false;
|
|
1524
|
-
this.scheduleReconnect();
|
|
1717
|
+
if (!this.closed) this.scheduleReconnect();
|
|
1525
1718
|
});
|
|
1526
|
-
|
|
1719
|
+
ws.on("error", (err) => {
|
|
1720
|
+
if (this.ws !== ws) return;
|
|
1527
1721
|
logger.debug(`Hub connection error: ${err.message}`);
|
|
1528
1722
|
this.connected = false;
|
|
1529
1723
|
});
|
|
@@ -1546,6 +1740,7 @@ var HubClient = class {
|
|
|
1546
1740
|
this.messageHandlers.push(handler);
|
|
1547
1741
|
}
|
|
1548
1742
|
disconnect() {
|
|
1743
|
+
this.closed = true;
|
|
1549
1744
|
if (this.reconnectTimer) {
|
|
1550
1745
|
clearTimeout(this.reconnectTimer);
|
|
1551
1746
|
this.reconnectTimer = null;
|
|
@@ -1556,8 +1751,29 @@ var HubClient = class {
|
|
|
1556
1751
|
}
|
|
1557
1752
|
this.connected = false;
|
|
1558
1753
|
}
|
|
1754
|
+
reregister() {
|
|
1755
|
+
if (this.connected && this.ws?.readyState === WebSocket2.OPEN) {
|
|
1756
|
+
this.ws.send(JSON.stringify(this.registration()));
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
registration() {
|
|
1760
|
+
return {
|
|
1761
|
+
type: "register",
|
|
1762
|
+
session: {
|
|
1763
|
+
id: this.sessionId,
|
|
1764
|
+
name: this.sessionName,
|
|
1765
|
+
status: "idle",
|
|
1766
|
+
connectedAt: Date.now(),
|
|
1767
|
+
lastActivity: Date.now(),
|
|
1768
|
+
cwd: process.cwd(),
|
|
1769
|
+
channelEnabled: true,
|
|
1770
|
+
peerName: this.getPeerName(),
|
|
1771
|
+
...this.getRegistration()
|
|
1772
|
+
}
|
|
1773
|
+
};
|
|
1774
|
+
}
|
|
1559
1775
|
scheduleReconnect() {
|
|
1560
|
-
if (this.reconnectTimer) return;
|
|
1776
|
+
if (this.reconnectTimer || this.closed) return;
|
|
1561
1777
|
this.reconnectTimer = setTimeout(() => {
|
|
1562
1778
|
this.reconnectTimer = null;
|
|
1563
1779
|
this.connect();
|
|
@@ -1567,6 +1783,8 @@ var HubClient = class {
|
|
|
1567
1783
|
export {
|
|
1568
1784
|
CHANNEL_SERVER_NAME,
|
|
1569
1785
|
CHANNEL_SERVER_VERSION,
|
|
1786
|
+
CODEX_LOG_FILE,
|
|
1787
|
+
CODEX_PID_FILE,
|
|
1570
1788
|
CONFIG_DIR,
|
|
1571
1789
|
CONFIG_FILE,
|
|
1572
1790
|
DEFAULT_HUB_HOST,
|