@delt/claude-alarm 1.1.1 → 1.3.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 +18 -10
- package/dist/channel/server.js.map +1 -1
- package/dist/cli.js +1258 -180
- package/dist/cli.js.map +1 -1
- package/dist/codex/main.js +660 -106
- package/dist/codex/main.js.map +1 -1
- package/dist/dashboard/index.html +176 -1
- package/dist/hub/server.js +308 -56
- package/dist/hub/server.js.map +1 -1
- package/dist/index.d.ts +71 -3
- package/dist/index.js +309 -56
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/dashboard/index.html +176 -1
package/dist/index.js
CHANGED
|
@@ -40,6 +40,7 @@ var CODEX_PID_FILE = path.join(CONFIG_DIR, "codex.pid");
|
|
|
40
40
|
var CODEX_LOG_FILE = path.join(CONFIG_DIR, "codex.log");
|
|
41
41
|
var WS_PATH_CHANNEL = "/ws/channel";
|
|
42
42
|
var WS_PATH_DASHBOARD = "/ws/dashboard";
|
|
43
|
+
var WS_PATH_CODEX = "/ws/codex";
|
|
43
44
|
var CHANNEL_SERVER_NAME = "claude-alarm";
|
|
44
45
|
var CHANNEL_SERVER_VERSION = "0.1.0";
|
|
45
46
|
|
|
@@ -175,6 +176,7 @@ var Notifier = class {
|
|
|
175
176
|
execFile(
|
|
176
177
|
"powershell",
|
|
177
178
|
["-Command", '(Get-ItemProperty -Path "HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\PushNotifications" -Name ToastEnabled -ErrorAction SilentlyContinue).ToastEnabled'],
|
|
179
|
+
{ windowsHide: true },
|
|
178
180
|
(err, stdout) => {
|
|
179
181
|
if (err) {
|
|
180
182
|
resolve(true);
|
|
@@ -192,7 +194,7 @@ var Notifier = class {
|
|
|
192
194
|
logger.warn("Windows notifications are disabled. Opening notification settings...");
|
|
193
195
|
logger.warn("Please enable notifications for this app, then try again.");
|
|
194
196
|
if (process.platform === "win32") {
|
|
195
|
-
execFile("powershell", ["-Command", "Start-Process ms-settings:notifications"]);
|
|
197
|
+
execFile("powershell", ["-Command", "Start-Process ms-settings:notifications"], { windowsHide: true });
|
|
196
198
|
}
|
|
197
199
|
setTimeout(() => {
|
|
198
200
|
this.notificationSettingsOpened = false;
|
|
@@ -243,6 +245,13 @@ ${requestId}`;
|
|
|
243
245
|
// src/hub/telegram.ts
|
|
244
246
|
var TELEGRAM_API = "https://api.telegram.org/bot";
|
|
245
247
|
var MAX_CHOICE_MESSAGES = 200;
|
|
248
|
+
var MAX_SELECTIONS = 20;
|
|
249
|
+
var MAX_VISIBLE_CHARS = 4e3;
|
|
250
|
+
var TRUNCATED = "\u2026(truncated)";
|
|
251
|
+
var MAX_PHOTO_BYTES = 10 * 1024 * 1024;
|
|
252
|
+
function visibleLength(html) {
|
|
253
|
+
return html.replace(/<[^>]*>/g, "").replace(/&(?:amp|lt|gt);/g, "&").length;
|
|
254
|
+
}
|
|
246
255
|
var TelegramBot = class _TelegramBot {
|
|
247
256
|
config;
|
|
248
257
|
offset = 0;
|
|
@@ -263,9 +272,9 @@ var TelegramBot = class _TelegramBot {
|
|
|
263
272
|
choiceMessages = /* @__PURE__ */ new Map();
|
|
264
273
|
// Callback: get current sessions list
|
|
265
274
|
getSessions;
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
275
|
+
latestSelection;
|
|
276
|
+
// Keyed by prompt, not by chat: buttons from an older prompt must still send that prompt's message.
|
|
277
|
+
selections = /* @__PURE__ */ new Map();
|
|
269
278
|
constructor(config) {
|
|
270
279
|
this.config = config;
|
|
271
280
|
}
|
|
@@ -274,8 +283,7 @@ var TelegramBot = class _TelegramBot {
|
|
|
274
283
|
}
|
|
275
284
|
/** Send a notification message to Telegram */
|
|
276
285
|
async sendNotification(sessionId, _sessionLabel, title, message) {
|
|
277
|
-
const text =
|
|
278
|
-
${this.mdToHtml(message)}`;
|
|
286
|
+
const text = this.fitNotification(title, message);
|
|
279
287
|
const result = await this.sendMessage(text);
|
|
280
288
|
if (result?.message_id) {
|
|
281
289
|
this.messageSessionMap.set(result.message_id, sessionId);
|
|
@@ -287,6 +295,25 @@ ${this.mdToHtml(message)}`;
|
|
|
287
295
|
}
|
|
288
296
|
}
|
|
289
297
|
}
|
|
298
|
+
fitNotification(title, message) {
|
|
299
|
+
const render = (body) => `<b>${this.escHtml(title)}</b>
|
|
300
|
+
${this.mdToHtml(body)}`;
|
|
301
|
+
const full = render(message);
|
|
302
|
+
if (visibleLength(full) <= MAX_VISIBLE_CHARS) return full;
|
|
303
|
+
const cut = (n) => {
|
|
304
|
+
const end = n > 0 && /[\uD800-\uDBFF]/.test(message[n - 1]) ? n - 1 : n;
|
|
305
|
+
return render(`${message.slice(0, end)}
|
|
306
|
+
${TRUNCATED}`);
|
|
307
|
+
};
|
|
308
|
+
let lo = 0;
|
|
309
|
+
let hi = message.length;
|
|
310
|
+
while (lo < hi) {
|
|
311
|
+
const mid = Math.ceil((lo + hi) / 2);
|
|
312
|
+
if (visibleLength(cut(mid)) <= MAX_VISIBLE_CHARS) lo = mid;
|
|
313
|
+
else hi = mid - 1;
|
|
314
|
+
}
|
|
315
|
+
return cut(lo);
|
|
316
|
+
}
|
|
290
317
|
/** Send a text message to the configured chat */
|
|
291
318
|
async sendMessage(text, replyToMessageId, replyMarkup) {
|
|
292
319
|
try {
|
|
@@ -368,6 +395,13 @@ ${this.mdToHtml(message)}`;
|
|
|
368
395
|
const hasPhoto = msg.photo && msg.photo.length > 0;
|
|
369
396
|
const text = (msg.text || msg.caption || "").trim();
|
|
370
397
|
if (!text && !hasPhoto) return;
|
|
398
|
+
if (hasPhoto) {
|
|
399
|
+
const size = msg.photo[msg.photo.length - 1].file_size;
|
|
400
|
+
if (size !== void 0 && size > MAX_PHOTO_BYTES) {
|
|
401
|
+
this.photoNotDelivered("it is larger than 10 MB");
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
371
405
|
if (msg.reply_to_message) {
|
|
372
406
|
const sessionId = this.messageSessionMap.get(msg.reply_to_message.message_id);
|
|
373
407
|
if (sessionId) {
|
|
@@ -381,18 +415,20 @@ ${this.mdToHtml(message)}`;
|
|
|
381
415
|
}
|
|
382
416
|
if (text) {
|
|
383
417
|
const selectMatch = text.match(/^\/s_(\d+)$/);
|
|
384
|
-
|
|
385
|
-
|
|
418
|
+
const latest = this.latestSelection;
|
|
419
|
+
if (selectMatch && latest !== void 0) {
|
|
420
|
+
const pending = this.selections.get(latest);
|
|
386
421
|
if (pending) {
|
|
387
|
-
this.
|
|
422
|
+
this.selections.delete(latest);
|
|
388
423
|
const session = this.pendingSession(pending, parseInt(selectMatch[1], 10) - 1);
|
|
389
424
|
if (session) {
|
|
425
|
+
let delivered = true;
|
|
390
426
|
if (pending.photoFileId) {
|
|
391
|
-
await this.deliverPhotoToSessionByFileId(session.id, pending.photoFileId, pending.caption);
|
|
427
|
+
delivered = await this.deliverPhotoToSessionByFileId(session.id, pending.photoFileId, pending.caption);
|
|
392
428
|
} else if (pending.text) {
|
|
393
429
|
this.deliverToSession(session.id, pending.text);
|
|
394
430
|
}
|
|
395
|
-
this.sendMessage(`Sent to [${this.getLabel(session)}]`);
|
|
431
|
+
if (delivered) this.sendMessage(`Sent to [${this.getLabel(session)}]`);
|
|
396
432
|
} else {
|
|
397
433
|
this.sendMessage("Invalid session number.");
|
|
398
434
|
}
|
|
@@ -413,16 +449,19 @@ ${this.mdToHtml(message)}`;
|
|
|
413
449
|
}
|
|
414
450
|
return;
|
|
415
451
|
}
|
|
452
|
+
const selectionId = randomUUID().replace(/-/g, "").slice(0, 8);
|
|
453
|
+
this.latestSelection = selectionId;
|
|
416
454
|
const sessionIds = sessions.map((s) => s.id);
|
|
417
455
|
if (hasPhoto) {
|
|
418
456
|
const largest = msg.photo[msg.photo.length - 1];
|
|
419
|
-
this.
|
|
457
|
+
this.selections.set(selectionId, { photoFileId: largest.file_id, caption: text, sessionIds });
|
|
420
458
|
} else {
|
|
421
|
-
this.
|
|
459
|
+
this.selections.set(selectionId, { text, sessionIds });
|
|
422
460
|
}
|
|
461
|
+
while (this.selections.size > MAX_SELECTIONS) this.selections.delete(this.selections.keys().next().value);
|
|
423
462
|
const buttons = sessions.map((s, i) => ({
|
|
424
463
|
text: this.getLabel(s),
|
|
425
|
-
callback_data: `
|
|
464
|
+
callback_data: `sel:${selectionId}:${i}`
|
|
426
465
|
}));
|
|
427
466
|
const rows = [];
|
|
428
467
|
for (let i = 0; i < buttons.length; i += 2) {
|
|
@@ -437,24 +476,27 @@ ${this.mdToHtml(message)}`;
|
|
|
437
476
|
}
|
|
438
477
|
async deliverPhotoToSession(sessionId, photos, caption) {
|
|
439
478
|
const largest = photos[photos.length - 1];
|
|
440
|
-
|
|
479
|
+
return this.deliverPhotoToSessionByFileId(sessionId, largest.file_id, caption);
|
|
441
480
|
}
|
|
442
481
|
async deliverPhotoToSessionByFileId(sessionId, fileId, caption) {
|
|
443
482
|
try {
|
|
444
483
|
const fileRes = await fetch(`${this.apiUrl}/getFile?file_id=${fileId}`);
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
return;
|
|
484
|
+
const fileData = fileRes.ok ? await fileRes.json() : void 0;
|
|
485
|
+
if (!fileData?.ok || !fileData.result) {
|
|
486
|
+
this.photoNotDelivered("Telegram did not return the file");
|
|
487
|
+
return false;
|
|
448
488
|
}
|
|
449
|
-
const fileData = await fileRes.json();
|
|
450
|
-
if (!fileData.ok) return;
|
|
451
489
|
const downloadUrl = `https://api.telegram.org/file/bot${this.config.botToken}/${fileData.result.file_path}`;
|
|
452
490
|
const imgRes = await fetch(downloadUrl);
|
|
453
491
|
if (!imgRes.ok) {
|
|
454
|
-
|
|
455
|
-
return;
|
|
492
|
+
this.photoNotDelivered("the download failed");
|
|
493
|
+
return false;
|
|
456
494
|
}
|
|
457
495
|
const buffer = Buffer.from(await imgRes.arrayBuffer());
|
|
496
|
+
if (buffer.length > MAX_PHOTO_BYTES) {
|
|
497
|
+
this.photoNotDelivered("it is larger than 10 MB");
|
|
498
|
+
return false;
|
|
499
|
+
}
|
|
458
500
|
const ext = fileData.result.file_path.split(".").pop() || "jpg";
|
|
459
501
|
const mimeType = ext === "png" ? "image/png" : ext === "gif" ? "image/gif" : ext === "webp" ? "image/webp" : "image/jpeg";
|
|
460
502
|
fs.mkdirSync(UPLOADS_DIR, { recursive: true });
|
|
@@ -470,11 +512,18 @@ ${this.mdToHtml(message)}`;
|
|
|
470
512
|
fs.unlinkSync(filePath);
|
|
471
513
|
} catch {
|
|
472
514
|
}
|
|
473
|
-
}, 5 * 60 * 1e3);
|
|
515
|
+
}, 5 * 60 * 1e3).unref();
|
|
516
|
+
return this.onImageToSession !== void 0;
|
|
474
517
|
} catch (err) {
|
|
475
518
|
logger.warn(`Telegram photo download failed: ${err.message}`);
|
|
519
|
+
this.photoNotDelivered("the download failed");
|
|
520
|
+
return false;
|
|
476
521
|
}
|
|
477
522
|
}
|
|
523
|
+
photoNotDelivered(reason) {
|
|
524
|
+
logger.warn(`Telegram photo not delivered: ${reason}`);
|
|
525
|
+
void this.sendMessage(`Photo not delivered: ${reason}`);
|
|
526
|
+
}
|
|
478
527
|
// Buttons are numbered against the list shown when they were sent; Codex sessions come and go, so the live list may have shifted.
|
|
479
528
|
pendingSession(pending, idx) {
|
|
480
529
|
const id = pending.sessionIds[idx];
|
|
@@ -588,7 +637,7 @@ ${this.escHtml(description)}` : "") + (slice ? `
|
|
|
588
637
|
async handleChoiceCallback(query) {
|
|
589
638
|
const choice = this.choiceTokens.get(query.data.slice("pc:".length));
|
|
590
639
|
if (!choice) {
|
|
591
|
-
await this.
|
|
640
|
+
await this.expire(query);
|
|
592
641
|
return;
|
|
593
642
|
}
|
|
594
643
|
this.dropChoiceTokens(choice.sessionId, choice.requestId);
|
|
@@ -617,10 +666,14 @@ ${this.escHtml(description)}` : "") + (slice ? `
|
|
|
617
666
|
async handleCallbackQuery(query) {
|
|
618
667
|
if (!query.data) return;
|
|
619
668
|
if (String(query.message?.chat.id) !== String(this.config.chatId)) return;
|
|
620
|
-
if (query.data.startsWith("
|
|
669
|
+
if (query.data.startsWith("sel:")) {
|
|
621
670
|
await this.handleSessionSelectCallback(query);
|
|
622
671
|
return;
|
|
623
672
|
}
|
|
673
|
+
if (query.data.startsWith("sess:")) {
|
|
674
|
+
await this.expire(query);
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
624
677
|
if (query.data.startsWith("pc:")) {
|
|
625
678
|
await this.handleChoiceCallback(query);
|
|
626
679
|
return;
|
|
@@ -643,19 +696,25 @@ ${label}`);
|
|
|
643
696
|
}
|
|
644
697
|
}
|
|
645
698
|
async handleSessionSelectCallback(query) {
|
|
646
|
-
const
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
699
|
+
const [, selectionId, idxStr] = query.data.split(":");
|
|
700
|
+
const pending = this.selections.get(selectionId);
|
|
701
|
+
if (!pending) {
|
|
702
|
+
await this.expire(query);
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
const session = this.pendingSession(pending, parseInt(idxStr, 10));
|
|
706
|
+
if (!session) {
|
|
653
707
|
await this.answerCallbackQuery(query.id, "Session not found");
|
|
654
708
|
return;
|
|
655
709
|
}
|
|
656
|
-
this.
|
|
710
|
+
this.selections.delete(selectionId);
|
|
657
711
|
if (pending.photoFileId) {
|
|
658
|
-
await this.deliverPhotoToSessionByFileId(session.id, pending.photoFileId, pending.caption);
|
|
712
|
+
const delivered = await this.deliverPhotoToSessionByFileId(session.id, pending.photoFileId, pending.caption);
|
|
713
|
+
if (!delivered) {
|
|
714
|
+
await this.answerCallbackQuery(query.id, "Photo not delivered");
|
|
715
|
+
if (query.message) await this.removeButtons(query.message.chat.id, query.message.message_id);
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
659
718
|
} else if (pending.text) {
|
|
660
719
|
this.deliverToSession(session.id, pending.text);
|
|
661
720
|
}
|
|
@@ -675,6 +734,21 @@ ${label}`);
|
|
|
675
734
|
logger.warn(`Telegram answerCallbackQuery error: ${err.message}`);
|
|
676
735
|
}
|
|
677
736
|
}
|
|
737
|
+
async expire(query) {
|
|
738
|
+
await this.answerCallbackQuery(query.id, "Expired");
|
|
739
|
+
if (query.message) await this.removeButtons(query.message.chat.id, query.message.message_id);
|
|
740
|
+
}
|
|
741
|
+
async removeButtons(chatId, messageId) {
|
|
742
|
+
try {
|
|
743
|
+
await fetch(`${this.apiUrl}/editMessageReplyMarkup`, {
|
|
744
|
+
method: "POST",
|
|
745
|
+
headers: { "Content-Type": "application/json" },
|
|
746
|
+
body: JSON.stringify({ chat_id: chatId, message_id: messageId, reply_markup: { inline_keyboard: [] } })
|
|
747
|
+
});
|
|
748
|
+
} catch (err) {
|
|
749
|
+
logger.warn(`Telegram editMessageReplyMarkup error: ${err.message}`);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
678
752
|
async editMessageText(chatId, messageId, text) {
|
|
679
753
|
try {
|
|
680
754
|
await fetch(`${this.apiUrl}/editMessageText`, {
|
|
@@ -771,19 +845,30 @@ var HEALTHY_RUN_MS = 6e4;
|
|
|
771
845
|
function resolveAdapterScript(baseDir) {
|
|
772
846
|
return [path3.join(baseDir, "..", "codex", "main.js"), path3.join(baseDir, "codex", "main.js")].find((p) => fs2.existsSync(p));
|
|
773
847
|
}
|
|
848
|
+
function adapterEnv(base, hub) {
|
|
849
|
+
const env = { ...base, CLAUDE_ALARM_HUB_HOST: hub.host, CLAUDE_ALARM_HUB_PORT: String(hub.port) };
|
|
850
|
+
delete env.CLAUDE_ALARM_HUB_TOKEN;
|
|
851
|
+
return env;
|
|
852
|
+
}
|
|
774
853
|
var CodexSupervisor = class {
|
|
775
|
-
constructor(script,
|
|
854
|
+
constructor(script, opts = {}) {
|
|
776
855
|
this.script = script;
|
|
777
|
-
this.spawnFn = spawnFn;
|
|
778
|
-
this.minDelayMs = minDelayMs;
|
|
779
|
-
this.maxDelayMs = maxDelayMs;
|
|
780
|
-
this.stopGraceMs = stopGraceMs;
|
|
781
|
-
this.
|
|
856
|
+
this.spawnFn = opts.spawnFn ?? spawn;
|
|
857
|
+
this.minDelayMs = opts.minDelayMs ?? 2e3;
|
|
858
|
+
this.maxDelayMs = opts.maxDelayMs ?? 6e4;
|
|
859
|
+
this.stopGraceMs = opts.stopGraceMs ?? 3e3;
|
|
860
|
+
this.env = opts.env;
|
|
861
|
+
this.delay = this.minDelayMs;
|
|
782
862
|
}
|
|
783
863
|
child;
|
|
784
864
|
timer;
|
|
785
865
|
stopped = true;
|
|
786
866
|
delay;
|
|
867
|
+
spawnFn;
|
|
868
|
+
minDelayMs;
|
|
869
|
+
maxDelayMs;
|
|
870
|
+
stopGraceMs;
|
|
871
|
+
env;
|
|
787
872
|
start() {
|
|
788
873
|
this.stopped = false;
|
|
789
874
|
this.launch();
|
|
@@ -805,7 +890,8 @@ var CodexSupervisor = class {
|
|
|
805
890
|
const child = this.spawnFn(process.execPath, [this.script, "--watch-stdin"], {
|
|
806
891
|
detached: true,
|
|
807
892
|
stdio: ["pipe", "pipe", "pipe"],
|
|
808
|
-
windowsHide: true
|
|
893
|
+
windowsHide: true,
|
|
894
|
+
...this.env ? { env: this.env } : {}
|
|
809
895
|
});
|
|
810
896
|
this.child = child;
|
|
811
897
|
child.stdout?.on("data", (d) => process.stdout.write(d));
|
|
@@ -944,6 +1030,21 @@ function logStartup(context) {
|
|
|
944
1030
|
`);
|
|
945
1031
|
}
|
|
946
1032
|
|
|
1033
|
+
// src/shared/entry.ts
|
|
1034
|
+
function isEntryScript(argv1, suffixes) {
|
|
1035
|
+
if (!argv1) return false;
|
|
1036
|
+
const normalized = argv1.replace(/\\/g, "/");
|
|
1037
|
+
return suffixes.some((suffix) => normalized.endsWith(suffix));
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// src/shared/hub-url.ts
|
|
1041
|
+
import net from "net";
|
|
1042
|
+
function hubUrlHost(host) {
|
|
1043
|
+
if (host === "" || host === "0.0.0.0") return "127.0.0.1";
|
|
1044
|
+
if (host === "::") return "[::1]";
|
|
1045
|
+
return net.isIPv6(host) ? `[${host}]` : host;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
947
1048
|
// src/hub/auth.ts
|
|
948
1049
|
import crypto from "crypto";
|
|
949
1050
|
var SESSION_COOKIE = "ca_session";
|
|
@@ -1009,6 +1110,7 @@ function validChoices(choices) {
|
|
|
1009
1110
|
const valid = choices.filter((c) => typeof c?.id === "string" && typeof c?.label === "string");
|
|
1010
1111
|
return valid.length ? valid : void 0;
|
|
1011
1112
|
}
|
|
1113
|
+
var nonEmpty = (v) => typeof v === "string" && v.trim() !== "";
|
|
1012
1114
|
var HubServer = class {
|
|
1013
1115
|
httpServer;
|
|
1014
1116
|
wssChannel;
|
|
@@ -1034,7 +1136,13 @@ var HubServer = class {
|
|
|
1034
1136
|
token;
|
|
1035
1137
|
codexEnabled;
|
|
1036
1138
|
codexSupervisor;
|
|
1037
|
-
|
|
1139
|
+
wssCodex;
|
|
1140
|
+
codexAdapters = /* @__PURE__ */ new Map();
|
|
1141
|
+
codexAlive = /* @__PURE__ */ new WeakMap();
|
|
1142
|
+
codexCallTimeoutMs;
|
|
1143
|
+
codexCalls = /* @__PURE__ */ new Map();
|
|
1144
|
+
constructor(config, options = {}) {
|
|
1145
|
+
this.codexCallTimeoutMs = options.codexCallTimeoutMs ?? 6e4;
|
|
1038
1146
|
this.host = config?.hub?.host ?? DEFAULT_HUB_HOST;
|
|
1039
1147
|
this.port = config?.hub?.port ?? DEFAULT_HUB_PORT;
|
|
1040
1148
|
this.token = config?.hub?.token;
|
|
@@ -1047,7 +1155,7 @@ var HubServer = class {
|
|
|
1047
1155
|
if (config?.webhooks) {
|
|
1048
1156
|
this.notifier.configure({ webhooks: config.webhooks });
|
|
1049
1157
|
}
|
|
1050
|
-
const displayHost = this.host
|
|
1158
|
+
const displayHost = hubUrlHost(this.host);
|
|
1051
1159
|
this.notifier.configure({ dashboardUrl: `http://${displayHost}:${this.port}` });
|
|
1052
1160
|
const fullConfig = loadConfig();
|
|
1053
1161
|
if (fullConfig.telegram?.enabled && fullConfig.telegram.botToken && fullConfig.telegram.chatId) {
|
|
@@ -1060,6 +1168,9 @@ var HubServer = class {
|
|
|
1060
1168
|
this.wssDashboard = new WebSocketServer({ noServer: true, maxPayload: MAX_WS_PAYLOAD });
|
|
1061
1169
|
this.wssDashboard.on("connection", (ws) => this.handleDashboardConnection(ws));
|
|
1062
1170
|
this.wssDashboard.on("error", (err) => logger.warn(`Dashboard WebSocket server error: ${err.message}`));
|
|
1171
|
+
this.wssCodex = new WebSocketServer({ noServer: true, maxPayload: MAX_WS_PAYLOAD });
|
|
1172
|
+
this.wssCodex.on("connection", (ws, req) => this.handleCodexConnection(ws, req));
|
|
1173
|
+
this.wssCodex.on("error", (err) => logger.warn(`Codex WebSocket server error: ${err.message}`));
|
|
1063
1174
|
this.httpServer.on("upgrade", (req, socket, head) => {
|
|
1064
1175
|
socket.on("error", (err) => logger.debug(`Upgrade socket error: ${err.message}`));
|
|
1065
1176
|
const url = new URL(req.url ?? "/", "http://hub");
|
|
@@ -1080,6 +1191,10 @@ var HubServer = class {
|
|
|
1080
1191
|
this.wssDashboard.handleUpgrade(req, socket, head, (ws) => {
|
|
1081
1192
|
this.wssDashboard.emit("connection", ws, req);
|
|
1082
1193
|
});
|
|
1194
|
+
} else if (pathname === WS_PATH_CODEX) {
|
|
1195
|
+
this.wssCodex.handleUpgrade(req, socket, head, (ws) => {
|
|
1196
|
+
this.wssCodex.emit("connection", ws, req);
|
|
1197
|
+
});
|
|
1083
1198
|
} else {
|
|
1084
1199
|
socket.destroy();
|
|
1085
1200
|
}
|
|
@@ -1094,7 +1209,7 @@ var HubServer = class {
|
|
|
1094
1209
|
this.httpServer.listen(this.port, this.host, () => {
|
|
1095
1210
|
this.httpServer.removeListener("error", onStartupError);
|
|
1096
1211
|
this.httpServer.on("error", (err) => logger.error(`HTTP server error: ${err.message}`));
|
|
1097
|
-
const displayHost = this.host
|
|
1212
|
+
const displayHost = hubUrlHost(this.host);
|
|
1098
1213
|
logger.info(`Hub server listening on http://${displayHost}:${this.port}`);
|
|
1099
1214
|
this.startCodexAdapter();
|
|
1100
1215
|
resolve();
|
|
@@ -1109,10 +1224,12 @@ var HubServer = class {
|
|
|
1109
1224
|
if (this.telegramBot) this.telegramBot.stopPolling();
|
|
1110
1225
|
for (const ws of this.channelSockets.values()) ws.terminate();
|
|
1111
1226
|
for (const ws of this.dashboardSockets) ws.terminate();
|
|
1227
|
+
for (const { ws } of this.codexAdapters.values()) ws.terminate();
|
|
1112
1228
|
this.channelSockets.clear();
|
|
1113
1229
|
this.dashboardSockets.clear();
|
|
1114
1230
|
this.wssChannel.close();
|
|
1115
1231
|
this.wssDashboard.close();
|
|
1232
|
+
this.wssCodex.close();
|
|
1116
1233
|
this.httpServer.close(() => {
|
|
1117
1234
|
logger.info("Hub server stopped");
|
|
1118
1235
|
resolve();
|
|
@@ -1177,6 +1294,12 @@ var HubServer = class {
|
|
|
1177
1294
|
this.handleApiSend(req, res);
|
|
1178
1295
|
} else if (url.pathname === "/api/notify" && req.method === "POST") {
|
|
1179
1296
|
this.handleApiNotify(req, res);
|
|
1297
|
+
} else if (url.pathname === "/api/codex/folders" && req.method === "GET") {
|
|
1298
|
+
this.handleCodexFolders(url, res);
|
|
1299
|
+
} else if (url.pathname === "/api/codex/threads" && req.method === "POST") {
|
|
1300
|
+
this.handleCodexCreate(req, res);
|
|
1301
|
+
} else if (url.pathname === "/api/codex/threads/close" && req.method === "POST") {
|
|
1302
|
+
this.handleCodexClose(req, res);
|
|
1180
1303
|
} else if (url.pathname === "/api/webhooks" && req.method === "GET") {
|
|
1181
1304
|
const config = loadConfig();
|
|
1182
1305
|
this.jsonResponse(res, 200, { webhooks: config.webhooks || [] });
|
|
@@ -1440,6 +1563,114 @@ var HubServer = class {
|
|
|
1440
1563
|
}
|
|
1441
1564
|
}
|
|
1442
1565
|
}
|
|
1566
|
+
handleCodexConnection(ws, req) {
|
|
1567
|
+
const isLocal = this.isLocalRequest(req);
|
|
1568
|
+
let adapterId;
|
|
1569
|
+
ws.on("error", (err) => {
|
|
1570
|
+
logger.warn(`Codex adapter WebSocket error: ${err.message}`);
|
|
1571
|
+
ws.terminate();
|
|
1572
|
+
});
|
|
1573
|
+
ws.on("pong", () => this.codexAlive.set(ws, true));
|
|
1574
|
+
ws.on("message", (data) => {
|
|
1575
|
+
let msg;
|
|
1576
|
+
try {
|
|
1577
|
+
msg = JSON.parse(data.toString());
|
|
1578
|
+
if (msg === null || typeof msg !== "object") {
|
|
1579
|
+
logger.warn("Invalid message from Codex adapter");
|
|
1580
|
+
return;
|
|
1581
|
+
}
|
|
1582
|
+
} catch {
|
|
1583
|
+
logger.warn("Invalid message from Codex adapter");
|
|
1584
|
+
return;
|
|
1585
|
+
}
|
|
1586
|
+
if (msg.type === "adapter_hello") {
|
|
1587
|
+
const a = msg.adapter;
|
|
1588
|
+
if (typeof a?.id !== "string" || !a.id || typeof a.host !== "string") return;
|
|
1589
|
+
if (adapterId && adapterId !== a.id) return;
|
|
1590
|
+
const holder = this.codexAdapters.get(a.id);
|
|
1591
|
+
if (holder && holder.ws !== ws) holder.ws.terminate();
|
|
1592
|
+
if (!adapterId) logger.info(`Codex adapter connected: ${a.host} (${a.id}, local: ${isLocal})`);
|
|
1593
|
+
adapterId = a.id;
|
|
1594
|
+
this.codexAdapters.set(a.id, { ws, info: { id: a.id, host: a.host, ready: a.ready === true, isLocal } });
|
|
1595
|
+
this.broadcastCodexAdapters();
|
|
1596
|
+
} else if (msg.type === "adapter_result" && adapterId) {
|
|
1597
|
+
const call = this.codexCalls.get(msg.requestId);
|
|
1598
|
+
if (!call || call.adapterId !== adapterId) return;
|
|
1599
|
+
call.settle(msg.ok ? { status: 200, body: msg.data } : { status: 422, body: { error: String(msg.error) } });
|
|
1600
|
+
}
|
|
1601
|
+
});
|
|
1602
|
+
ws.on("close", () => {
|
|
1603
|
+
if (!adapterId || this.codexAdapters.get(adapterId)?.ws !== ws) return;
|
|
1604
|
+
this.codexAdapters.delete(adapterId);
|
|
1605
|
+
for (const call of [...this.codexCalls.values()]) {
|
|
1606
|
+
if (call.adapterId === adapterId) call.settle({ status: 502, body: { error: "Codex adapter disconnected" } });
|
|
1607
|
+
}
|
|
1608
|
+
logger.info(`Codex adapter disconnected: ${adapterId}`);
|
|
1609
|
+
this.broadcastCodexAdapters();
|
|
1610
|
+
});
|
|
1611
|
+
}
|
|
1612
|
+
callCodexAdapter(adapterId, call) {
|
|
1613
|
+
const adapter = this.codexAdapters.get(adapterId);
|
|
1614
|
+
if (!adapter || adapter.ws.readyState !== WebSocket.OPEN) {
|
|
1615
|
+
return Promise.resolve({ status: 404, body: { error: "Codex adapter is not connected" } });
|
|
1616
|
+
}
|
|
1617
|
+
const requestId = randomUUID3();
|
|
1618
|
+
return new Promise((resolve) => {
|
|
1619
|
+
const timer = setTimeout(
|
|
1620
|
+
() => settle({ status: 504, body: { error: "Codex did not respond in time. The conversation may still appear." } }),
|
|
1621
|
+
this.codexCallTimeoutMs
|
|
1622
|
+
);
|
|
1623
|
+
const settle = (outcome) => {
|
|
1624
|
+
clearTimeout(timer);
|
|
1625
|
+
this.codexCalls.delete(requestId);
|
|
1626
|
+
resolve(outcome);
|
|
1627
|
+
};
|
|
1628
|
+
this.codexCalls.set(requestId, { adapterId, settle });
|
|
1629
|
+
adapter.ws.send(JSON.stringify({ type: "adapter_call", requestId, call }));
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1632
|
+
async handleCodexFolders(url, res) {
|
|
1633
|
+
const adapterId = url.searchParams.get("adapterId");
|
|
1634
|
+
if (!nonEmpty(adapterId)) {
|
|
1635
|
+
this.jsonResponse(res, 400, { error: "adapterId is required" });
|
|
1636
|
+
return;
|
|
1637
|
+
}
|
|
1638
|
+
const out = await this.callCodexAdapter(adapterId, { kind: "folders" });
|
|
1639
|
+
this.jsonResponse(res, out.status, out.body);
|
|
1640
|
+
}
|
|
1641
|
+
async handleCodexCreate(req, res) {
|
|
1642
|
+
const body = await this.readBody(req);
|
|
1643
|
+
const adapterId = body?.adapterId;
|
|
1644
|
+
const cwd = body?.cwd;
|
|
1645
|
+
if (!nonEmpty(adapterId) || !nonEmpty(cwd)) {
|
|
1646
|
+
this.jsonResponse(res, 400, { error: "adapterId and cwd are required" });
|
|
1647
|
+
return;
|
|
1648
|
+
}
|
|
1649
|
+
const out = await this.callCodexAdapter(adapterId, { kind: "create", cwd });
|
|
1650
|
+
this.jsonResponse(res, out.status, out.body);
|
|
1651
|
+
}
|
|
1652
|
+
async handleCodexClose(req, res) {
|
|
1653
|
+
const body = await this.readBody(req);
|
|
1654
|
+
const sessionId = body?.sessionId;
|
|
1655
|
+
if (!nonEmpty(sessionId)) {
|
|
1656
|
+
this.jsonResponse(res, 400, { error: "sessionId is required" });
|
|
1657
|
+
return;
|
|
1658
|
+
}
|
|
1659
|
+
const session = this.sessions.get(sessionId);
|
|
1660
|
+
const ws = this.channelSockets.get(sessionId);
|
|
1661
|
+
if (session?.agentKind !== "codex" || !session.closable || ws?.readyState !== WebSocket.OPEN) {
|
|
1662
|
+
this.jsonResponse(res, 404, { error: "No closable Codex conversation" });
|
|
1663
|
+
return;
|
|
1664
|
+
}
|
|
1665
|
+
ws.send(JSON.stringify({ type: "codex_close", sessionId }));
|
|
1666
|
+
this.jsonResponse(res, 200, { ok: true });
|
|
1667
|
+
}
|
|
1668
|
+
codexAdapterList() {
|
|
1669
|
+
return [...this.codexAdapters.values()].map((a) => a.info);
|
|
1670
|
+
}
|
|
1671
|
+
broadcastCodexAdapters() {
|
|
1672
|
+
this.broadcastToDashboards({ type: "codex_adapters", adapters: this.codexAdapterList() });
|
|
1673
|
+
}
|
|
1443
1674
|
// --- Dashboard WebSocket ---
|
|
1444
1675
|
handleDashboardConnection(ws) {
|
|
1445
1676
|
this.dashboardSockets.add(ws);
|
|
@@ -1458,6 +1689,7 @@ var HubServer = class {
|
|
|
1458
1689
|
requests: [...this.choiceRequests.values()].map((c) => c.request)
|
|
1459
1690
|
};
|
|
1460
1691
|
ws.send(JSON.stringify(pendingMsg));
|
|
1692
|
+
ws.send(JSON.stringify({ type: "codex_adapters", adapters: this.codexAdapterList() }));
|
|
1461
1693
|
ws.on("message", (data) => {
|
|
1462
1694
|
try {
|
|
1463
1695
|
const msg = JSON.parse(data.toString());
|
|
@@ -1467,7 +1699,7 @@ var HubServer = class {
|
|
|
1467
1699
|
channelWs.send(JSON.stringify({ ...msg, source: "dashboard" }));
|
|
1468
1700
|
}
|
|
1469
1701
|
} else if (msg.type === "image_upload") {
|
|
1470
|
-
this.handleImageUpload(msg);
|
|
1702
|
+
this.handleImageUpload(ws, msg);
|
|
1471
1703
|
} else if (msg.type === "permission_response") {
|
|
1472
1704
|
if (this.forwardPermissionResponse(msg)) {
|
|
1473
1705
|
const verdict = msg.choiceId !== void 0 ? `choice ${msg.choiceId}` : msg.behavior;
|
|
@@ -1518,20 +1750,31 @@ var HubServer = class {
|
|
|
1518
1750
|
}
|
|
1519
1751
|
}
|
|
1520
1752
|
}
|
|
1521
|
-
handleImageUpload(msg) {
|
|
1753
|
+
handleImageUpload(ws, msg) {
|
|
1522
1754
|
const { sessionId, imageData, mimeType, originalName, content } = msg;
|
|
1755
|
+
const reject = (reason) => {
|
|
1756
|
+
logger.warn(`Image upload rejected for ${sessionId}: ${reason}`);
|
|
1757
|
+
const rejected = { type: "upload_rejected", sessionId, reason, withText: typeof content === "string" && content.trim().length > 0 };
|
|
1758
|
+
if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(rejected));
|
|
1759
|
+
};
|
|
1760
|
+
const channelWs = this.channelSockets.get(sessionId);
|
|
1761
|
+
if (!channelWs || channelWs.readyState !== WebSocket.OPEN) {
|
|
1762
|
+
reject("the session is not connected");
|
|
1763
|
+
return;
|
|
1764
|
+
}
|
|
1523
1765
|
if (!this.localChannels.has(sessionId)) {
|
|
1524
|
-
|
|
1766
|
+
reject("this session is on another PC; images can only go to sessions on the hub's PC");
|
|
1525
1767
|
return;
|
|
1526
1768
|
}
|
|
1527
|
-
const channelWs = this.channelSockets.get(sessionId);
|
|
1528
|
-
if (!channelWs || channelWs.readyState !== WebSocket.OPEN) return;
|
|
1529
1769
|
const allowedTypes = ["image/png", "image/jpeg", "image/gif", "image/webp"];
|
|
1530
|
-
if (!allowedTypes.includes(mimeType))
|
|
1770
|
+
if (!allowedTypes.includes(mimeType)) {
|
|
1771
|
+
reject("only PNG, JPEG, GIF and WebP images are supported");
|
|
1772
|
+
return;
|
|
1773
|
+
}
|
|
1531
1774
|
const base64Data = imageData.replace(/^data:image\/\w+;base64,/, "");
|
|
1532
1775
|
const buffer = Buffer.from(base64Data, "base64");
|
|
1533
1776
|
if (buffer.length > 10 * 1024 * 1024) {
|
|
1534
|
-
|
|
1777
|
+
reject("the image is larger than 10 MB");
|
|
1535
1778
|
return;
|
|
1536
1779
|
}
|
|
1537
1780
|
fs5.mkdirSync(UPLOADS_DIR, { recursive: true });
|
|
@@ -1545,7 +1788,8 @@ var HubServer = class {
|
|
|
1545
1788
|
imagePath: filePath,
|
|
1546
1789
|
mimeType,
|
|
1547
1790
|
originalName,
|
|
1548
|
-
content
|
|
1791
|
+
content,
|
|
1792
|
+
source: "dashboard"
|
|
1549
1793
|
};
|
|
1550
1794
|
channelWs.send(JSON.stringify(forwardMsg));
|
|
1551
1795
|
logger.info(`Image saved and forwarded: ${filename} (${buffer.length} bytes)`);
|
|
@@ -1554,7 +1798,7 @@ var HubServer = class {
|
|
|
1554
1798
|
fs5.unlinkSync(filePath);
|
|
1555
1799
|
} catch {
|
|
1556
1800
|
}
|
|
1557
|
-
}, 5 * 60 * 1e3);
|
|
1801
|
+
}, 5 * 60 * 1e3).unref();
|
|
1558
1802
|
}
|
|
1559
1803
|
async handleWebhookSave(req, res) {
|
|
1560
1804
|
const body = await this.readBody(req);
|
|
@@ -1587,7 +1831,7 @@ var HubServer = class {
|
|
|
1587
1831
|
this.telegramBot.onImageToSession = (sessionId, imagePath, mimeType, caption) => {
|
|
1588
1832
|
const channelWs = this.channelSockets.get(sessionId);
|
|
1589
1833
|
if (channelWs?.readyState === WebSocket.OPEN) {
|
|
1590
|
-
const msg = { type: "image_to_session", sessionId, imagePath, mimeType, content: caption };
|
|
1834
|
+
const msg = { type: "image_to_session", sessionId, imagePath, mimeType, content: caption, source: "telegram" };
|
|
1591
1835
|
channelWs.send(JSON.stringify(msg));
|
|
1592
1836
|
logger.info(`Telegram photo forwarded to session: ${sessionId}`);
|
|
1593
1837
|
}
|
|
@@ -1715,7 +1959,7 @@ var HubServer = class {
|
|
|
1715
1959
|
logger.warn("Codex adapter is enabled but codex/main.js was not found next to the hub");
|
|
1716
1960
|
return;
|
|
1717
1961
|
}
|
|
1718
|
-
this.codexSupervisor = new CodexSupervisor(script);
|
|
1962
|
+
this.codexSupervisor = new CodexSupervisor(script, { env: adapterEnv(process.env, { host: this.host, port: this.port }) });
|
|
1719
1963
|
this.codexSupervisor.start();
|
|
1720
1964
|
logger.info("Codex adapter started");
|
|
1721
1965
|
}
|
|
@@ -1730,6 +1974,14 @@ var HubServer = class {
|
|
|
1730
1974
|
this.channelAlive.set(sessionId, false);
|
|
1731
1975
|
ws.ping();
|
|
1732
1976
|
}
|
|
1977
|
+
for (const { ws } of this.codexAdapters.values()) {
|
|
1978
|
+
if (this.codexAlive.get(ws) === false) {
|
|
1979
|
+
ws.terminate();
|
|
1980
|
+
continue;
|
|
1981
|
+
}
|
|
1982
|
+
this.codexAlive.set(ws, false);
|
|
1983
|
+
ws.ping();
|
|
1984
|
+
}
|
|
1733
1985
|
}, 3e4);
|
|
1734
1986
|
}
|
|
1735
1987
|
cleanupUploads() {
|
|
@@ -1792,7 +2044,7 @@ Content-Length: 0\r
|
|
|
1792
2044
|
});
|
|
1793
2045
|
}
|
|
1794
2046
|
};
|
|
1795
|
-
if (process.argv[1]
|
|
2047
|
+
if (isEntryScript(process.argv[1], ["/hub/server.js", "/hub/server.ts"])) {
|
|
1796
2048
|
installCrashGuard("hub daemon");
|
|
1797
2049
|
logStartup("Hub daemon");
|
|
1798
2050
|
const config = loadConfig();
|
|
@@ -1942,6 +2194,7 @@ export {
|
|
|
1942
2194
|
SessionManager,
|
|
1943
2195
|
UPLOADS_DIR,
|
|
1944
2196
|
WS_PATH_CHANNEL,
|
|
2197
|
+
WS_PATH_CODEX,
|
|
1945
2198
|
WS_PATH_DASHBOARD,
|
|
1946
2199
|
loadConfig,
|
|
1947
2200
|
logger,
|