@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/hub/server.js
CHANGED
|
@@ -42,6 +42,7 @@ var CODEX_PID_FILE = path.join(CONFIG_DIR, "codex.pid");
|
|
|
42
42
|
var CODEX_LOG_FILE = path.join(CONFIG_DIR, "codex.log");
|
|
43
43
|
var WS_PATH_CHANNEL = "/ws/channel";
|
|
44
44
|
var WS_PATH_DASHBOARD = "/ws/dashboard";
|
|
45
|
+
var WS_PATH_CODEX = "/ws/codex";
|
|
45
46
|
|
|
46
47
|
// src/shared/peer-name.ts
|
|
47
48
|
var SAFE_PEER_NAME = /^[^\u0000-\u001f\u007f"\\[\]]{1,128}$/;
|
|
@@ -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));
|
|
@@ -920,6 +1006,21 @@ function logStartup(context) {
|
|
|
920
1006
|
`);
|
|
921
1007
|
}
|
|
922
1008
|
|
|
1009
|
+
// src/shared/entry.ts
|
|
1010
|
+
function isEntryScript(argv1, suffixes) {
|
|
1011
|
+
if (!argv1) return false;
|
|
1012
|
+
const normalized = argv1.replace(/\\/g, "/");
|
|
1013
|
+
return suffixes.some((suffix) => normalized.endsWith(suffix));
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// src/shared/hub-url.ts
|
|
1017
|
+
import net from "net";
|
|
1018
|
+
function hubUrlHost(host) {
|
|
1019
|
+
if (host === "" || host === "0.0.0.0") return "127.0.0.1";
|
|
1020
|
+
if (host === "::") return "[::1]";
|
|
1021
|
+
return net.isIPv6(host) ? `[${host}]` : host;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
923
1024
|
// src/hub/auth.ts
|
|
924
1025
|
import crypto from "crypto";
|
|
925
1026
|
var SESSION_COOKIE = "ca_session";
|
|
@@ -985,6 +1086,7 @@ function validChoices(choices) {
|
|
|
985
1086
|
const valid = choices.filter((c) => typeof c?.id === "string" && typeof c?.label === "string");
|
|
986
1087
|
return valid.length ? valid : void 0;
|
|
987
1088
|
}
|
|
1089
|
+
var nonEmpty = (v) => typeof v === "string" && v.trim() !== "";
|
|
988
1090
|
var HubServer = class {
|
|
989
1091
|
httpServer;
|
|
990
1092
|
wssChannel;
|
|
@@ -1010,7 +1112,13 @@ var HubServer = class {
|
|
|
1010
1112
|
token;
|
|
1011
1113
|
codexEnabled;
|
|
1012
1114
|
codexSupervisor;
|
|
1013
|
-
|
|
1115
|
+
wssCodex;
|
|
1116
|
+
codexAdapters = /* @__PURE__ */ new Map();
|
|
1117
|
+
codexAlive = /* @__PURE__ */ new WeakMap();
|
|
1118
|
+
codexCallTimeoutMs;
|
|
1119
|
+
codexCalls = /* @__PURE__ */ new Map();
|
|
1120
|
+
constructor(config, options = {}) {
|
|
1121
|
+
this.codexCallTimeoutMs = options.codexCallTimeoutMs ?? 6e4;
|
|
1014
1122
|
this.host = config?.hub?.host ?? DEFAULT_HUB_HOST;
|
|
1015
1123
|
this.port = config?.hub?.port ?? DEFAULT_HUB_PORT;
|
|
1016
1124
|
this.token = config?.hub?.token;
|
|
@@ -1023,7 +1131,7 @@ var HubServer = class {
|
|
|
1023
1131
|
if (config?.webhooks) {
|
|
1024
1132
|
this.notifier.configure({ webhooks: config.webhooks });
|
|
1025
1133
|
}
|
|
1026
|
-
const displayHost = this.host
|
|
1134
|
+
const displayHost = hubUrlHost(this.host);
|
|
1027
1135
|
this.notifier.configure({ dashboardUrl: `http://${displayHost}:${this.port}` });
|
|
1028
1136
|
const fullConfig = loadConfig();
|
|
1029
1137
|
if (fullConfig.telegram?.enabled && fullConfig.telegram.botToken && fullConfig.telegram.chatId) {
|
|
@@ -1036,6 +1144,9 @@ var HubServer = class {
|
|
|
1036
1144
|
this.wssDashboard = new WebSocketServer({ noServer: true, maxPayload: MAX_WS_PAYLOAD });
|
|
1037
1145
|
this.wssDashboard.on("connection", (ws) => this.handleDashboardConnection(ws));
|
|
1038
1146
|
this.wssDashboard.on("error", (err) => logger.warn(`Dashboard WebSocket server error: ${err.message}`));
|
|
1147
|
+
this.wssCodex = new WebSocketServer({ noServer: true, maxPayload: MAX_WS_PAYLOAD });
|
|
1148
|
+
this.wssCodex.on("connection", (ws, req) => this.handleCodexConnection(ws, req));
|
|
1149
|
+
this.wssCodex.on("error", (err) => logger.warn(`Codex WebSocket server error: ${err.message}`));
|
|
1039
1150
|
this.httpServer.on("upgrade", (req, socket, head) => {
|
|
1040
1151
|
socket.on("error", (err) => logger.debug(`Upgrade socket error: ${err.message}`));
|
|
1041
1152
|
const url = new URL(req.url ?? "/", "http://hub");
|
|
@@ -1056,6 +1167,10 @@ var HubServer = class {
|
|
|
1056
1167
|
this.wssDashboard.handleUpgrade(req, socket, head, (ws) => {
|
|
1057
1168
|
this.wssDashboard.emit("connection", ws, req);
|
|
1058
1169
|
});
|
|
1170
|
+
} else if (pathname === WS_PATH_CODEX) {
|
|
1171
|
+
this.wssCodex.handleUpgrade(req, socket, head, (ws) => {
|
|
1172
|
+
this.wssCodex.emit("connection", ws, req);
|
|
1173
|
+
});
|
|
1059
1174
|
} else {
|
|
1060
1175
|
socket.destroy();
|
|
1061
1176
|
}
|
|
@@ -1070,7 +1185,7 @@ var HubServer = class {
|
|
|
1070
1185
|
this.httpServer.listen(this.port, this.host, () => {
|
|
1071
1186
|
this.httpServer.removeListener("error", onStartupError);
|
|
1072
1187
|
this.httpServer.on("error", (err) => logger.error(`HTTP server error: ${err.message}`));
|
|
1073
|
-
const displayHost = this.host
|
|
1188
|
+
const displayHost = hubUrlHost(this.host);
|
|
1074
1189
|
logger.info(`Hub server listening on http://${displayHost}:${this.port}`);
|
|
1075
1190
|
this.startCodexAdapter();
|
|
1076
1191
|
resolve();
|
|
@@ -1085,10 +1200,12 @@ var HubServer = class {
|
|
|
1085
1200
|
if (this.telegramBot) this.telegramBot.stopPolling();
|
|
1086
1201
|
for (const ws of this.channelSockets.values()) ws.terminate();
|
|
1087
1202
|
for (const ws of this.dashboardSockets) ws.terminate();
|
|
1203
|
+
for (const { ws } of this.codexAdapters.values()) ws.terminate();
|
|
1088
1204
|
this.channelSockets.clear();
|
|
1089
1205
|
this.dashboardSockets.clear();
|
|
1090
1206
|
this.wssChannel.close();
|
|
1091
1207
|
this.wssDashboard.close();
|
|
1208
|
+
this.wssCodex.close();
|
|
1092
1209
|
this.httpServer.close(() => {
|
|
1093
1210
|
logger.info("Hub server stopped");
|
|
1094
1211
|
resolve();
|
|
@@ -1153,6 +1270,12 @@ var HubServer = class {
|
|
|
1153
1270
|
this.handleApiSend(req, res);
|
|
1154
1271
|
} else if (url.pathname === "/api/notify" && req.method === "POST") {
|
|
1155
1272
|
this.handleApiNotify(req, res);
|
|
1273
|
+
} else if (url.pathname === "/api/codex/folders" && req.method === "GET") {
|
|
1274
|
+
this.handleCodexFolders(url, res);
|
|
1275
|
+
} else if (url.pathname === "/api/codex/threads" && req.method === "POST") {
|
|
1276
|
+
this.handleCodexCreate(req, res);
|
|
1277
|
+
} else if (url.pathname === "/api/codex/threads/close" && req.method === "POST") {
|
|
1278
|
+
this.handleCodexClose(req, res);
|
|
1156
1279
|
} else if (url.pathname === "/api/webhooks" && req.method === "GET") {
|
|
1157
1280
|
const config = loadConfig();
|
|
1158
1281
|
this.jsonResponse(res, 200, { webhooks: config.webhooks || [] });
|
|
@@ -1416,6 +1539,114 @@ var HubServer = class {
|
|
|
1416
1539
|
}
|
|
1417
1540
|
}
|
|
1418
1541
|
}
|
|
1542
|
+
handleCodexConnection(ws, req) {
|
|
1543
|
+
const isLocal = this.isLocalRequest(req);
|
|
1544
|
+
let adapterId;
|
|
1545
|
+
ws.on("error", (err) => {
|
|
1546
|
+
logger.warn(`Codex adapter WebSocket error: ${err.message}`);
|
|
1547
|
+
ws.terminate();
|
|
1548
|
+
});
|
|
1549
|
+
ws.on("pong", () => this.codexAlive.set(ws, true));
|
|
1550
|
+
ws.on("message", (data) => {
|
|
1551
|
+
let msg;
|
|
1552
|
+
try {
|
|
1553
|
+
msg = JSON.parse(data.toString());
|
|
1554
|
+
if (msg === null || typeof msg !== "object") {
|
|
1555
|
+
logger.warn("Invalid message from Codex adapter");
|
|
1556
|
+
return;
|
|
1557
|
+
}
|
|
1558
|
+
} catch {
|
|
1559
|
+
logger.warn("Invalid message from Codex adapter");
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
if (msg.type === "adapter_hello") {
|
|
1563
|
+
const a = msg.adapter;
|
|
1564
|
+
if (typeof a?.id !== "string" || !a.id || typeof a.host !== "string") return;
|
|
1565
|
+
if (adapterId && adapterId !== a.id) return;
|
|
1566
|
+
const holder = this.codexAdapters.get(a.id);
|
|
1567
|
+
if (holder && holder.ws !== ws) holder.ws.terminate();
|
|
1568
|
+
if (!adapterId) logger.info(`Codex adapter connected: ${a.host} (${a.id}, local: ${isLocal})`);
|
|
1569
|
+
adapterId = a.id;
|
|
1570
|
+
this.codexAdapters.set(a.id, { ws, info: { id: a.id, host: a.host, ready: a.ready === true, isLocal } });
|
|
1571
|
+
this.broadcastCodexAdapters();
|
|
1572
|
+
} else if (msg.type === "adapter_result" && adapterId) {
|
|
1573
|
+
const call = this.codexCalls.get(msg.requestId);
|
|
1574
|
+
if (!call || call.adapterId !== adapterId) return;
|
|
1575
|
+
call.settle(msg.ok ? { status: 200, body: msg.data } : { status: 422, body: { error: String(msg.error) } });
|
|
1576
|
+
}
|
|
1577
|
+
});
|
|
1578
|
+
ws.on("close", () => {
|
|
1579
|
+
if (!adapterId || this.codexAdapters.get(adapterId)?.ws !== ws) return;
|
|
1580
|
+
this.codexAdapters.delete(adapterId);
|
|
1581
|
+
for (const call of [...this.codexCalls.values()]) {
|
|
1582
|
+
if (call.adapterId === adapterId) call.settle({ status: 502, body: { error: "Codex adapter disconnected" } });
|
|
1583
|
+
}
|
|
1584
|
+
logger.info(`Codex adapter disconnected: ${adapterId}`);
|
|
1585
|
+
this.broadcastCodexAdapters();
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
1588
|
+
callCodexAdapter(adapterId, call) {
|
|
1589
|
+
const adapter = this.codexAdapters.get(adapterId);
|
|
1590
|
+
if (!adapter || adapter.ws.readyState !== WebSocket.OPEN) {
|
|
1591
|
+
return Promise.resolve({ status: 404, body: { error: "Codex adapter is not connected" } });
|
|
1592
|
+
}
|
|
1593
|
+
const requestId = randomUUID3();
|
|
1594
|
+
return new Promise((resolve) => {
|
|
1595
|
+
const timer = setTimeout(
|
|
1596
|
+
() => settle({ status: 504, body: { error: "Codex did not respond in time. The conversation may still appear." } }),
|
|
1597
|
+
this.codexCallTimeoutMs
|
|
1598
|
+
);
|
|
1599
|
+
const settle = (outcome) => {
|
|
1600
|
+
clearTimeout(timer);
|
|
1601
|
+
this.codexCalls.delete(requestId);
|
|
1602
|
+
resolve(outcome);
|
|
1603
|
+
};
|
|
1604
|
+
this.codexCalls.set(requestId, { adapterId, settle });
|
|
1605
|
+
adapter.ws.send(JSON.stringify({ type: "adapter_call", requestId, call }));
|
|
1606
|
+
});
|
|
1607
|
+
}
|
|
1608
|
+
async handleCodexFolders(url, res) {
|
|
1609
|
+
const adapterId = url.searchParams.get("adapterId");
|
|
1610
|
+
if (!nonEmpty(adapterId)) {
|
|
1611
|
+
this.jsonResponse(res, 400, { error: "adapterId is required" });
|
|
1612
|
+
return;
|
|
1613
|
+
}
|
|
1614
|
+
const out = await this.callCodexAdapter(adapterId, { kind: "folders" });
|
|
1615
|
+
this.jsonResponse(res, out.status, out.body);
|
|
1616
|
+
}
|
|
1617
|
+
async handleCodexCreate(req, res) {
|
|
1618
|
+
const body = await this.readBody(req);
|
|
1619
|
+
const adapterId = body?.adapterId;
|
|
1620
|
+
const cwd = body?.cwd;
|
|
1621
|
+
if (!nonEmpty(adapterId) || !nonEmpty(cwd)) {
|
|
1622
|
+
this.jsonResponse(res, 400, { error: "adapterId and cwd are required" });
|
|
1623
|
+
return;
|
|
1624
|
+
}
|
|
1625
|
+
const out = await this.callCodexAdapter(adapterId, { kind: "create", cwd });
|
|
1626
|
+
this.jsonResponse(res, out.status, out.body);
|
|
1627
|
+
}
|
|
1628
|
+
async handleCodexClose(req, res) {
|
|
1629
|
+
const body = await this.readBody(req);
|
|
1630
|
+
const sessionId = body?.sessionId;
|
|
1631
|
+
if (!nonEmpty(sessionId)) {
|
|
1632
|
+
this.jsonResponse(res, 400, { error: "sessionId is required" });
|
|
1633
|
+
return;
|
|
1634
|
+
}
|
|
1635
|
+
const session = this.sessions.get(sessionId);
|
|
1636
|
+
const ws = this.channelSockets.get(sessionId);
|
|
1637
|
+
if (session?.agentKind !== "codex" || !session.closable || ws?.readyState !== WebSocket.OPEN) {
|
|
1638
|
+
this.jsonResponse(res, 404, { error: "No closable Codex conversation" });
|
|
1639
|
+
return;
|
|
1640
|
+
}
|
|
1641
|
+
ws.send(JSON.stringify({ type: "codex_close", sessionId }));
|
|
1642
|
+
this.jsonResponse(res, 200, { ok: true });
|
|
1643
|
+
}
|
|
1644
|
+
codexAdapterList() {
|
|
1645
|
+
return [...this.codexAdapters.values()].map((a) => a.info);
|
|
1646
|
+
}
|
|
1647
|
+
broadcastCodexAdapters() {
|
|
1648
|
+
this.broadcastToDashboards({ type: "codex_adapters", adapters: this.codexAdapterList() });
|
|
1649
|
+
}
|
|
1419
1650
|
// --- Dashboard WebSocket ---
|
|
1420
1651
|
handleDashboardConnection(ws) {
|
|
1421
1652
|
this.dashboardSockets.add(ws);
|
|
@@ -1434,6 +1665,7 @@ var HubServer = class {
|
|
|
1434
1665
|
requests: [...this.choiceRequests.values()].map((c) => c.request)
|
|
1435
1666
|
};
|
|
1436
1667
|
ws.send(JSON.stringify(pendingMsg));
|
|
1668
|
+
ws.send(JSON.stringify({ type: "codex_adapters", adapters: this.codexAdapterList() }));
|
|
1437
1669
|
ws.on("message", (data) => {
|
|
1438
1670
|
try {
|
|
1439
1671
|
const msg = JSON.parse(data.toString());
|
|
@@ -1443,7 +1675,7 @@ var HubServer = class {
|
|
|
1443
1675
|
channelWs.send(JSON.stringify({ ...msg, source: "dashboard" }));
|
|
1444
1676
|
}
|
|
1445
1677
|
} else if (msg.type === "image_upload") {
|
|
1446
|
-
this.handleImageUpload(msg);
|
|
1678
|
+
this.handleImageUpload(ws, msg);
|
|
1447
1679
|
} else if (msg.type === "permission_response") {
|
|
1448
1680
|
if (this.forwardPermissionResponse(msg)) {
|
|
1449
1681
|
const verdict = msg.choiceId !== void 0 ? `choice ${msg.choiceId}` : msg.behavior;
|
|
@@ -1494,20 +1726,31 @@ var HubServer = class {
|
|
|
1494
1726
|
}
|
|
1495
1727
|
}
|
|
1496
1728
|
}
|
|
1497
|
-
handleImageUpload(msg) {
|
|
1729
|
+
handleImageUpload(ws, msg) {
|
|
1498
1730
|
const { sessionId, imageData, mimeType, originalName, content } = msg;
|
|
1731
|
+
const reject = (reason) => {
|
|
1732
|
+
logger.warn(`Image upload rejected for ${sessionId}: ${reason}`);
|
|
1733
|
+
const rejected = { type: "upload_rejected", sessionId, reason, withText: typeof content === "string" && content.trim().length > 0 };
|
|
1734
|
+
if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(rejected));
|
|
1735
|
+
};
|
|
1736
|
+
const channelWs = this.channelSockets.get(sessionId);
|
|
1737
|
+
if (!channelWs || channelWs.readyState !== WebSocket.OPEN) {
|
|
1738
|
+
reject("the session is not connected");
|
|
1739
|
+
return;
|
|
1740
|
+
}
|
|
1499
1741
|
if (!this.localChannels.has(sessionId)) {
|
|
1500
|
-
|
|
1742
|
+
reject("this session is on another PC; images can only go to sessions on the hub's PC");
|
|
1501
1743
|
return;
|
|
1502
1744
|
}
|
|
1503
|
-
const channelWs = this.channelSockets.get(sessionId);
|
|
1504
|
-
if (!channelWs || channelWs.readyState !== WebSocket.OPEN) return;
|
|
1505
1745
|
const allowedTypes = ["image/png", "image/jpeg", "image/gif", "image/webp"];
|
|
1506
|
-
if (!allowedTypes.includes(mimeType))
|
|
1746
|
+
if (!allowedTypes.includes(mimeType)) {
|
|
1747
|
+
reject("only PNG, JPEG, GIF and WebP images are supported");
|
|
1748
|
+
return;
|
|
1749
|
+
}
|
|
1507
1750
|
const base64Data = imageData.replace(/^data:image\/\w+;base64,/, "");
|
|
1508
1751
|
const buffer = Buffer.from(base64Data, "base64");
|
|
1509
1752
|
if (buffer.length > 10 * 1024 * 1024) {
|
|
1510
|
-
|
|
1753
|
+
reject("the image is larger than 10 MB");
|
|
1511
1754
|
return;
|
|
1512
1755
|
}
|
|
1513
1756
|
fs5.mkdirSync(UPLOADS_DIR, { recursive: true });
|
|
@@ -1521,7 +1764,8 @@ var HubServer = class {
|
|
|
1521
1764
|
imagePath: filePath,
|
|
1522
1765
|
mimeType,
|
|
1523
1766
|
originalName,
|
|
1524
|
-
content
|
|
1767
|
+
content,
|
|
1768
|
+
source: "dashboard"
|
|
1525
1769
|
};
|
|
1526
1770
|
channelWs.send(JSON.stringify(forwardMsg));
|
|
1527
1771
|
logger.info(`Image saved and forwarded: ${filename} (${buffer.length} bytes)`);
|
|
@@ -1530,7 +1774,7 @@ var HubServer = class {
|
|
|
1530
1774
|
fs5.unlinkSync(filePath);
|
|
1531
1775
|
} catch {
|
|
1532
1776
|
}
|
|
1533
|
-
}, 5 * 60 * 1e3);
|
|
1777
|
+
}, 5 * 60 * 1e3).unref();
|
|
1534
1778
|
}
|
|
1535
1779
|
async handleWebhookSave(req, res) {
|
|
1536
1780
|
const body = await this.readBody(req);
|
|
@@ -1563,7 +1807,7 @@ var HubServer = class {
|
|
|
1563
1807
|
this.telegramBot.onImageToSession = (sessionId, imagePath, mimeType, caption) => {
|
|
1564
1808
|
const channelWs = this.channelSockets.get(sessionId);
|
|
1565
1809
|
if (channelWs?.readyState === WebSocket.OPEN) {
|
|
1566
|
-
const msg = { type: "image_to_session", sessionId, imagePath, mimeType, content: caption };
|
|
1810
|
+
const msg = { type: "image_to_session", sessionId, imagePath, mimeType, content: caption, source: "telegram" };
|
|
1567
1811
|
channelWs.send(JSON.stringify(msg));
|
|
1568
1812
|
logger.info(`Telegram photo forwarded to session: ${sessionId}`);
|
|
1569
1813
|
}
|
|
@@ -1691,7 +1935,7 @@ var HubServer = class {
|
|
|
1691
1935
|
logger.warn("Codex adapter is enabled but codex/main.js was not found next to the hub");
|
|
1692
1936
|
return;
|
|
1693
1937
|
}
|
|
1694
|
-
this.codexSupervisor = new CodexSupervisor(script);
|
|
1938
|
+
this.codexSupervisor = new CodexSupervisor(script, { env: adapterEnv(process.env, { host: this.host, port: this.port }) });
|
|
1695
1939
|
this.codexSupervisor.start();
|
|
1696
1940
|
logger.info("Codex adapter started");
|
|
1697
1941
|
}
|
|
@@ -1706,6 +1950,14 @@ var HubServer = class {
|
|
|
1706
1950
|
this.channelAlive.set(sessionId, false);
|
|
1707
1951
|
ws.ping();
|
|
1708
1952
|
}
|
|
1953
|
+
for (const { ws } of this.codexAdapters.values()) {
|
|
1954
|
+
if (this.codexAlive.get(ws) === false) {
|
|
1955
|
+
ws.terminate();
|
|
1956
|
+
continue;
|
|
1957
|
+
}
|
|
1958
|
+
this.codexAlive.set(ws, false);
|
|
1959
|
+
ws.ping();
|
|
1960
|
+
}
|
|
1709
1961
|
}, 3e4);
|
|
1710
1962
|
}
|
|
1711
1963
|
cleanupUploads() {
|
|
@@ -1768,7 +2020,7 @@ Content-Length: 0\r
|
|
|
1768
2020
|
});
|
|
1769
2021
|
}
|
|
1770
2022
|
};
|
|
1771
|
-
if (process.argv[1]
|
|
2023
|
+
if (isEntryScript(process.argv[1], ["/hub/server.js", "/hub/server.ts"])) {
|
|
1772
2024
|
installCrashGuard("hub daemon");
|
|
1773
2025
|
logStartup("Hub daemon");
|
|
1774
2026
|
const config = loadConfig();
|