@delt/claude-alarm 0.9.1 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -0
- package/dist/channel/server.js +41 -22
- package/dist/channel/server.js.map +1 -1
- package/dist/cli.js +354 -130
- package/dist/cli.js.map +1 -1
- package/dist/codex/main.js +778 -0
- package/dist/codex/main.js.map +1 -0
- package/dist/dashboard/index.html +18 -4
- package/dist/hub/server.js +155 -61
- package/dist/hub/server.js.map +1 -1
- package/dist/index.d.ts +24 -2
- package/dist/index.js +201 -88
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/dashboard/index.html +18 -4
package/dist/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}`;
|
|
@@ -547,22 +565,18 @@ ${label}`);
|
|
|
547
565
|
const parts = query.data.split(":");
|
|
548
566
|
if (parts.length < 3) return;
|
|
549
567
|
const [, idxStr, chatIdStr] = parts;
|
|
550
|
-
const idx = parseInt(idxStr, 10);
|
|
551
568
|
const chatId = parseInt(chatIdStr, 10);
|
|
552
|
-
const
|
|
553
|
-
|
|
569
|
+
const pending = this.pendingMessages.get(chatId);
|
|
570
|
+
const session = pending ? this.pendingSession(pending, parseInt(idxStr, 10)) : void 0;
|
|
571
|
+
if (!pending || !session) {
|
|
554
572
|
await this.answerCallbackQuery(query.id, "Session not found");
|
|
555
573
|
return;
|
|
556
574
|
}
|
|
557
|
-
const session = sessions[idx];
|
|
558
|
-
const pending = this.pendingMessages.get(chatId);
|
|
559
575
|
this.pendingMessages.delete(chatId);
|
|
560
|
-
if (pending) {
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
this.deliverToSession(session.id, pending.text);
|
|
565
|
-
}
|
|
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);
|
|
566
580
|
}
|
|
567
581
|
await this.answerCallbackQuery(query.id, `Sent to ${this.getLabel(session)}`);
|
|
568
582
|
if (query.message) {
|
|
@@ -624,8 +638,8 @@ ${label}`);
|
|
|
624
638
|
css: "css",
|
|
625
639
|
scss: "scss"
|
|
626
640
|
};
|
|
627
|
-
langOf(
|
|
628
|
-
const m = /\.([a-z0-9]+)$/i.exec(
|
|
641
|
+
langOf(path6) {
|
|
642
|
+
const m = /\.([a-z0-9]+)$/i.exec(path6 || "");
|
|
629
643
|
return m ? _TelegramBot.LANG_BY_EXT[m[1].toLowerCase()] ?? "" : "";
|
|
630
644
|
}
|
|
631
645
|
/** Convert markdown to Telegram HTML (escape first, then apply formatting) */
|
|
@@ -668,9 +682,73 @@ ${bodyLines.join("\n")}`;
|
|
|
668
682
|
}
|
|
669
683
|
};
|
|
670
684
|
|
|
671
|
-
// src/
|
|
685
|
+
// src/hub/codex-supervisor.ts
|
|
686
|
+
import { spawn } from "child_process";
|
|
672
687
|
import fs2 from "fs";
|
|
673
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";
|
|
674
752
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
675
753
|
var DEFAULT_CONFIG = {
|
|
676
754
|
hub: {
|
|
@@ -684,18 +762,18 @@ var DEFAULT_CONFIG = {
|
|
|
684
762
|
webhooks: []
|
|
685
763
|
};
|
|
686
764
|
function ensureConfigDir() {
|
|
687
|
-
if (!
|
|
688
|
-
|
|
765
|
+
if (!fs3.existsSync(CONFIG_DIR)) {
|
|
766
|
+
fs3.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
689
767
|
}
|
|
690
768
|
}
|
|
691
769
|
function loadConfig() {
|
|
692
770
|
ensureConfigDir();
|
|
693
771
|
let config;
|
|
694
|
-
if (!
|
|
772
|
+
if (!fs3.existsSync(CONFIG_FILE)) {
|
|
695
773
|
config = { ...DEFAULT_CONFIG, hub: { ...DEFAULT_CONFIG.hub } };
|
|
696
774
|
} else {
|
|
697
775
|
try {
|
|
698
|
-
const raw =
|
|
776
|
+
const raw = fs3.readFileSync(CONFIG_FILE, "utf-8");
|
|
699
777
|
const parsed = JSON.parse(raw);
|
|
700
778
|
config = { ...DEFAULT_CONFIG, ...parsed, hub: { ...DEFAULT_CONFIG.hub, ...parsed.hub }, ...parsed.telegram ? { telegram: parsed.telegram } : {} };
|
|
701
779
|
} catch {
|
|
@@ -710,15 +788,15 @@ function loadConfig() {
|
|
|
710
788
|
}
|
|
711
789
|
function saveConfig(config) {
|
|
712
790
|
ensureConfigDir();
|
|
713
|
-
|
|
791
|
+
fs3.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { encoding: "utf-8", mode: 384 });
|
|
714
792
|
}
|
|
715
793
|
function setupMcpConfig(targetDir) {
|
|
716
794
|
const dir = targetDir ?? process.cwd();
|
|
717
|
-
const mcpPath =
|
|
795
|
+
const mcpPath = path4.join(dir, ".mcp.json");
|
|
718
796
|
let mcpConfig = {};
|
|
719
|
-
if (
|
|
797
|
+
if (fs3.existsSync(mcpPath)) {
|
|
720
798
|
try {
|
|
721
|
-
mcpConfig = JSON.parse(
|
|
799
|
+
mcpConfig = JSON.parse(fs3.readFileSync(mcpPath, "utf-8"));
|
|
722
800
|
} catch {
|
|
723
801
|
mcpConfig = {};
|
|
724
802
|
}
|
|
@@ -730,21 +808,21 @@ function setupMcpConfig(targetDir) {
|
|
|
730
808
|
command: "npx",
|
|
731
809
|
args: ["-y", "@delt/claude-alarm", "serve"],
|
|
732
810
|
env: {
|
|
733
|
-
CLAUDE_ALARM_SESSION_NAME:
|
|
811
|
+
CLAUDE_ALARM_SESSION_NAME: path4.basename(dir)
|
|
734
812
|
}
|
|
735
813
|
};
|
|
736
|
-
|
|
814
|
+
fs3.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2), "utf-8");
|
|
737
815
|
return mcpPath;
|
|
738
816
|
}
|
|
739
817
|
|
|
740
818
|
// src/shared/crash-guard.ts
|
|
741
|
-
import
|
|
819
|
+
import fs4 from "fs";
|
|
742
820
|
var BURST_WINDOW_MS = 6e4;
|
|
743
821
|
var BURST_LIMIT = 10;
|
|
744
822
|
function appendToLog(text) {
|
|
745
823
|
try {
|
|
746
|
-
|
|
747
|
-
|
|
824
|
+
fs4.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
825
|
+
fs4.appendFileSync(LOG_FILE, text, "utf-8");
|
|
748
826
|
} catch {
|
|
749
827
|
}
|
|
750
828
|
}
|
|
@@ -843,7 +921,7 @@ function isSecureRequest(req) {
|
|
|
843
921
|
}
|
|
844
922
|
|
|
845
923
|
// src/hub/server.ts
|
|
846
|
-
var __dirname =
|
|
924
|
+
var __dirname = path5.dirname(fileURLToPath(import.meta.url));
|
|
847
925
|
var MAX_WS_PAYLOAD = 16 * 1024 * 1024;
|
|
848
926
|
var HubServer = class {
|
|
849
927
|
httpServer;
|
|
@@ -866,10 +944,13 @@ var HubServer = class {
|
|
|
866
944
|
host;
|
|
867
945
|
port;
|
|
868
946
|
token;
|
|
947
|
+
codexEnabled;
|
|
948
|
+
codexSupervisor;
|
|
869
949
|
constructor(config) {
|
|
870
950
|
this.host = config?.hub?.host ?? DEFAULT_HUB_HOST;
|
|
871
951
|
this.port = config?.hub?.port ?? DEFAULT_HUB_PORT;
|
|
872
952
|
this.token = config?.hub?.token;
|
|
953
|
+
this.codexEnabled = config?.codex?.enabled === true;
|
|
873
954
|
if (config?.notifications) {
|
|
874
955
|
this.notifier.configure({
|
|
875
956
|
desktop: config.notifications.desktop
|
|
@@ -927,12 +1008,15 @@ var HubServer = class {
|
|
|
927
1008
|
this.httpServer.on("error", (err) => logger.error(`HTTP server error: ${err.message}`));
|
|
928
1009
|
const displayHost = this.host === "0.0.0.0" ? "127.0.0.1" : this.host;
|
|
929
1010
|
logger.info(`Hub server listening on http://${displayHost}:${this.port}`);
|
|
1011
|
+
this.startCodexAdapter();
|
|
930
1012
|
resolve();
|
|
931
1013
|
});
|
|
932
1014
|
});
|
|
933
1015
|
}
|
|
934
1016
|
stop() {
|
|
935
1017
|
return new Promise((resolve) => {
|
|
1018
|
+
this.codexSupervisor?.stop();
|
|
1019
|
+
this.codexSupervisor = void 0;
|
|
936
1020
|
if (this.heartbeatInterval) clearInterval(this.heartbeatInterval);
|
|
937
1021
|
if (this.telegramBot) this.telegramBot.stopPolling();
|
|
938
1022
|
for (const ws of this.channelSockets.values()) ws.terminate();
|
|
@@ -1033,23 +1117,23 @@ var HubServer = class {
|
|
|
1033
1117
|
return;
|
|
1034
1118
|
}
|
|
1035
1119
|
const candidates = [
|
|
1036
|
-
|
|
1120
|
+
path5.join(__dirname, "..", "dashboard", "index.html"),
|
|
1037
1121
|
// from dist/hub/
|
|
1038
|
-
|
|
1122
|
+
path5.join(__dirname, "dashboard", "index.html"),
|
|
1039
1123
|
// from dist/ (bundled index.js)
|
|
1040
|
-
|
|
1124
|
+
path5.join(__dirname, "..", "..", "src", "dashboard", "index.html"),
|
|
1041
1125
|
// from dist/hub/ -> src/
|
|
1042
|
-
|
|
1126
|
+
path5.join(__dirname, "..", "src", "dashboard", "index.html"),
|
|
1043
1127
|
// from dist/ -> src/
|
|
1044
|
-
|
|
1128
|
+
path5.join(process.cwd(), "dist", "dashboard", "index.html"),
|
|
1045
1129
|
// from cwd
|
|
1046
|
-
|
|
1130
|
+
path5.join(process.cwd(), "src", "dashboard", "index.html")
|
|
1047
1131
|
// from cwd/src
|
|
1048
1132
|
];
|
|
1049
1133
|
logger.debug(`Dashboard candidates: ${JSON.stringify(candidates)}`);
|
|
1050
1134
|
for (const candidate of candidates) {
|
|
1051
|
-
if (
|
|
1052
|
-
const html =
|
|
1135
|
+
if (fs5.existsSync(candidate)) {
|
|
1136
|
+
const html = fs5.readFileSync(candidate, "utf-8");
|
|
1053
1137
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
1054
1138
|
res.end(html);
|
|
1055
1139
|
return;
|
|
@@ -1088,7 +1172,7 @@ var HubServer = class {
|
|
|
1088
1172
|
this.jsonResponse(res, 404, { error: "Session not connected" });
|
|
1089
1173
|
return;
|
|
1090
1174
|
}
|
|
1091
|
-
const msg = { type: "message_to_session", sessionId, content };
|
|
1175
|
+
const msg = { type: "message_to_session", sessionId, content, source: "api" };
|
|
1092
1176
|
ws.send(JSON.stringify(msg));
|
|
1093
1177
|
this.jsonResponse(res, 200, { ok: true });
|
|
1094
1178
|
}
|
|
@@ -1262,7 +1346,7 @@ var HubServer = class {
|
|
|
1262
1346
|
if (msg.type === "message_to_session") {
|
|
1263
1347
|
const channelWs = this.channelSockets.get(msg.sessionId);
|
|
1264
1348
|
if (channelWs?.readyState === WebSocket.OPEN) {
|
|
1265
|
-
channelWs.send(JSON.stringify(msg));
|
|
1349
|
+
channelWs.send(JSON.stringify({ ...msg, source: "dashboard" }));
|
|
1266
1350
|
}
|
|
1267
1351
|
} else if (msg.type === "image_upload") {
|
|
1268
1352
|
this.handleImageUpload(msg);
|
|
@@ -1307,11 +1391,11 @@ var HubServer = class {
|
|
|
1307
1391
|
logger.warn("Image upload rejected: exceeds 10MB");
|
|
1308
1392
|
return;
|
|
1309
1393
|
}
|
|
1310
|
-
|
|
1394
|
+
fs5.mkdirSync(UPLOADS_DIR, { recursive: true });
|
|
1311
1395
|
const ext = mimeType.split("/")[1] === "jpeg" ? "jpg" : mimeType.split("/")[1];
|
|
1312
1396
|
const filename = `${randomUUID3()}.${ext}`;
|
|
1313
|
-
const filePath =
|
|
1314
|
-
|
|
1397
|
+
const filePath = path5.join(UPLOADS_DIR, filename);
|
|
1398
|
+
fs5.writeFileSync(filePath, buffer);
|
|
1315
1399
|
const forwardMsg = {
|
|
1316
1400
|
type: "image_to_session",
|
|
1317
1401
|
sessionId,
|
|
@@ -1324,7 +1408,7 @@ var HubServer = class {
|
|
|
1324
1408
|
logger.info(`Image saved and forwarded: ${filename} (${buffer.length} bytes)`);
|
|
1325
1409
|
setTimeout(() => {
|
|
1326
1410
|
try {
|
|
1327
|
-
|
|
1411
|
+
fs5.unlinkSync(filePath);
|
|
1328
1412
|
} catch {
|
|
1329
1413
|
}
|
|
1330
1414
|
}, 5 * 60 * 1e3);
|
|
@@ -1352,7 +1436,7 @@ var HubServer = class {
|
|
|
1352
1436
|
this.telegramBot.onMessageToSession = (sessionId, content) => {
|
|
1353
1437
|
const channelWs = this.channelSockets.get(sessionId);
|
|
1354
1438
|
if (channelWs?.readyState === WebSocket.OPEN) {
|
|
1355
|
-
const msg = { type: "message_to_session", sessionId, content };
|
|
1439
|
+
const msg = { type: "message_to_session", sessionId, content, source: "telegram" };
|
|
1356
1440
|
channelWs.send(JSON.stringify(msg));
|
|
1357
1441
|
logger.info(`Telegram message forwarded to session: ${sessionId}`);
|
|
1358
1442
|
}
|
|
@@ -1479,6 +1563,17 @@ var HubServer = class {
|
|
|
1479
1563
|
this.jsonResponse(res, 500, { error: err.message });
|
|
1480
1564
|
}
|
|
1481
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
|
+
}
|
|
1482
1577
|
startHeartbeat() {
|
|
1483
1578
|
this.heartbeatInterval = setInterval(() => {
|
|
1484
1579
|
for (const [sessionId, ws] of this.channelSockets) {
|
|
@@ -1494,11 +1589,11 @@ var HubServer = class {
|
|
|
1494
1589
|
}
|
|
1495
1590
|
cleanupUploads() {
|
|
1496
1591
|
try {
|
|
1497
|
-
if (!
|
|
1498
|
-
const files =
|
|
1592
|
+
if (!fs5.existsSync(UPLOADS_DIR)) return;
|
|
1593
|
+
const files = fs5.readdirSync(UPLOADS_DIR);
|
|
1499
1594
|
for (const file of files) {
|
|
1500
1595
|
try {
|
|
1501
|
-
|
|
1596
|
+
fs5.unlinkSync(path5.join(UPLOADS_DIR, file));
|
|
1502
1597
|
} catch {
|
|
1503
1598
|
}
|
|
1504
1599
|
}
|
|
@@ -1507,8 +1602,7 @@ var HubServer = class {
|
|
|
1507
1602
|
}
|
|
1508
1603
|
}
|
|
1509
1604
|
getSessionLabel(session) {
|
|
1510
|
-
|
|
1511
|
-
return session.displayName || session.cwd?.replace(/^.*[/\\]/, "") || session.name;
|
|
1605
|
+
return session ? sessionLabel(session) : "unknown";
|
|
1512
1606
|
}
|
|
1513
1607
|
jsonResponse(res, status, body) {
|
|
1514
1608
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
@@ -1572,48 +1666,41 @@ if (process.argv[1] && (process.argv[1].endsWith("hub/server.js") || process.arg
|
|
|
1572
1666
|
// src/channel/hub-client.ts
|
|
1573
1667
|
import WebSocket2 from "ws";
|
|
1574
1668
|
var HubClient = class {
|
|
1575
|
-
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 = () => ({})) {
|
|
1576
1670
|
this.sessionId = sessionId;
|
|
1577
1671
|
this.sessionName = sessionName;
|
|
1578
1672
|
this.hubHost = hubHost;
|
|
1579
1673
|
this.hubPort = hubPort;
|
|
1580
1674
|
this.token = token;
|
|
1581
1675
|
this.getPeerName = getPeerName;
|
|
1676
|
+
this.getRegistration = getRegistration;
|
|
1582
1677
|
}
|
|
1583
1678
|
ws = null;
|
|
1584
1679
|
reconnectTimer = null;
|
|
1585
1680
|
messageHandlers = [];
|
|
1586
1681
|
queue = [];
|
|
1587
1682
|
connected = false;
|
|
1683
|
+
closed = false;
|
|
1588
1684
|
connect() {
|
|
1685
|
+
this.closed = false;
|
|
1589
1686
|
const tokenQuery = this.token ? `?token=${encodeURIComponent(this.token)}` : "";
|
|
1590
1687
|
const url = `ws://${this.hubHost}:${this.hubPort}${WS_PATH_CHANNEL}${tokenQuery}`;
|
|
1591
1688
|
logger.debug(`Connecting to hub at ${url}`);
|
|
1592
1689
|
try {
|
|
1593
|
-
|
|
1594
|
-
this.ws
|
|
1690
|
+
const ws = new WebSocket2(url);
|
|
1691
|
+
this.ws = ws;
|
|
1692
|
+
ws.on("open", () => {
|
|
1693
|
+
if (this.ws !== ws) return;
|
|
1595
1694
|
logger.info("Connected to hub");
|
|
1596
1695
|
this.connected = true;
|
|
1597
|
-
|
|
1598
|
-
type: "register",
|
|
1599
|
-
session: {
|
|
1600
|
-
id: this.sessionId,
|
|
1601
|
-
name: this.sessionName,
|
|
1602
|
-
status: "idle",
|
|
1603
|
-
connectedAt: Date.now(),
|
|
1604
|
-
lastActivity: Date.now(),
|
|
1605
|
-
cwd: process.cwd(),
|
|
1606
|
-
channelEnabled: true,
|
|
1607
|
-
peerName: this.getPeerName()
|
|
1608
|
-
}
|
|
1609
|
-
};
|
|
1610
|
-
this.ws.send(JSON.stringify(registration));
|
|
1696
|
+
this.ws.send(JSON.stringify(this.registration()));
|
|
1611
1697
|
for (const msg of this.queue) {
|
|
1612
1698
|
this.ws.send(JSON.stringify(msg));
|
|
1613
1699
|
}
|
|
1614
1700
|
this.queue = [];
|
|
1615
1701
|
});
|
|
1616
|
-
|
|
1702
|
+
ws.on("message", (data) => {
|
|
1703
|
+
if (this.ws !== ws) return;
|
|
1617
1704
|
try {
|
|
1618
1705
|
const msg = JSON.parse(data.toString());
|
|
1619
1706
|
for (const handler of this.messageHandlers) {
|
|
@@ -1623,12 +1710,14 @@ var HubClient = class {
|
|
|
1623
1710
|
logger.warn("Failed to parse hub message:", err);
|
|
1624
1711
|
}
|
|
1625
1712
|
});
|
|
1626
|
-
|
|
1713
|
+
ws.on("close", () => {
|
|
1714
|
+
if (this.ws !== ws) return;
|
|
1627
1715
|
logger.info("Disconnected from hub");
|
|
1628
1716
|
this.connected = false;
|
|
1629
|
-
this.scheduleReconnect();
|
|
1717
|
+
if (!this.closed) this.scheduleReconnect();
|
|
1630
1718
|
});
|
|
1631
|
-
|
|
1719
|
+
ws.on("error", (err) => {
|
|
1720
|
+
if (this.ws !== ws) return;
|
|
1632
1721
|
logger.debug(`Hub connection error: ${err.message}`);
|
|
1633
1722
|
this.connected = false;
|
|
1634
1723
|
});
|
|
@@ -1651,6 +1740,7 @@ var HubClient = class {
|
|
|
1651
1740
|
this.messageHandlers.push(handler);
|
|
1652
1741
|
}
|
|
1653
1742
|
disconnect() {
|
|
1743
|
+
this.closed = true;
|
|
1654
1744
|
if (this.reconnectTimer) {
|
|
1655
1745
|
clearTimeout(this.reconnectTimer);
|
|
1656
1746
|
this.reconnectTimer = null;
|
|
@@ -1661,8 +1751,29 @@ var HubClient = class {
|
|
|
1661
1751
|
}
|
|
1662
1752
|
this.connected = false;
|
|
1663
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
|
+
}
|
|
1664
1775
|
scheduleReconnect() {
|
|
1665
|
-
if (this.reconnectTimer) return;
|
|
1776
|
+
if (this.reconnectTimer || this.closed) return;
|
|
1666
1777
|
this.reconnectTimer = setTimeout(() => {
|
|
1667
1778
|
this.reconnectTimer = null;
|
|
1668
1779
|
this.connect();
|
|
@@ -1672,6 +1783,8 @@ var HubClient = class {
|
|
|
1672
1783
|
export {
|
|
1673
1784
|
CHANNEL_SERVER_NAME,
|
|
1674
1785
|
CHANNEL_SERVER_VERSION,
|
|
1786
|
+
CODEX_LOG_FILE,
|
|
1787
|
+
CODEX_PID_FILE,
|
|
1675
1788
|
CONFIG_DIR,
|
|
1676
1789
|
CONFIG_FILE,
|
|
1677
1790
|
DEFAULT_HUB_HOST,
|