@delt/claude-alarm 1.2.0 → 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 +2 -1
- package/dist/cli.js +408 -169
- package/dist/cli.js.map +1 -1
- package/dist/codex/main.js +302 -65
- package/dist/codex/main.js.map +1 -1
- package/dist/dashboard/index.html +14 -0
- package/dist/hub/server.js +143 -48
- package/dist/hub/server.js.map +1 -1
- package/dist/index.d.ts +11 -1
- package/dist/index.js +143 -48
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/dashboard/index.html +14 -0
package/dist/hub/server.js
CHANGED
|
@@ -245,6 +245,13 @@ ${requestId}`;
|
|
|
245
245
|
// src/hub/telegram.ts
|
|
246
246
|
var TELEGRAM_API = "https://api.telegram.org/bot";
|
|
247
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
|
+
}
|
|
248
255
|
var TelegramBot = class _TelegramBot {
|
|
249
256
|
config;
|
|
250
257
|
offset = 0;
|
|
@@ -265,9 +272,9 @@ var TelegramBot = class _TelegramBot {
|
|
|
265
272
|
choiceMessages = /* @__PURE__ */ new Map();
|
|
266
273
|
// Callback: get current sessions list
|
|
267
274
|
getSessions;
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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();
|
|
271
278
|
constructor(config) {
|
|
272
279
|
this.config = config;
|
|
273
280
|
}
|
|
@@ -276,8 +283,7 @@ var TelegramBot = class _TelegramBot {
|
|
|
276
283
|
}
|
|
277
284
|
/** Send a notification message to Telegram */
|
|
278
285
|
async sendNotification(sessionId, _sessionLabel, title, message) {
|
|
279
|
-
const text =
|
|
280
|
-
${this.mdToHtml(message)}`;
|
|
286
|
+
const text = this.fitNotification(title, message);
|
|
281
287
|
const result = await this.sendMessage(text);
|
|
282
288
|
if (result?.message_id) {
|
|
283
289
|
this.messageSessionMap.set(result.message_id, sessionId);
|
|
@@ -289,6 +295,25 @@ ${this.mdToHtml(message)}`;
|
|
|
289
295
|
}
|
|
290
296
|
}
|
|
291
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
|
+
}
|
|
292
317
|
/** Send a text message to the configured chat */
|
|
293
318
|
async sendMessage(text, replyToMessageId, replyMarkup) {
|
|
294
319
|
try {
|
|
@@ -370,6 +395,13 @@ ${this.mdToHtml(message)}`;
|
|
|
370
395
|
const hasPhoto = msg.photo && msg.photo.length > 0;
|
|
371
396
|
const text = (msg.text || msg.caption || "").trim();
|
|
372
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
|
+
}
|
|
373
405
|
if (msg.reply_to_message) {
|
|
374
406
|
const sessionId = this.messageSessionMap.get(msg.reply_to_message.message_id);
|
|
375
407
|
if (sessionId) {
|
|
@@ -383,18 +415,20 @@ ${this.mdToHtml(message)}`;
|
|
|
383
415
|
}
|
|
384
416
|
if (text) {
|
|
385
417
|
const selectMatch = text.match(/^\/s_(\d+)$/);
|
|
386
|
-
|
|
387
|
-
|
|
418
|
+
const latest = this.latestSelection;
|
|
419
|
+
if (selectMatch && latest !== void 0) {
|
|
420
|
+
const pending = this.selections.get(latest);
|
|
388
421
|
if (pending) {
|
|
389
|
-
this.
|
|
422
|
+
this.selections.delete(latest);
|
|
390
423
|
const session = this.pendingSession(pending, parseInt(selectMatch[1], 10) - 1);
|
|
391
424
|
if (session) {
|
|
425
|
+
let delivered = true;
|
|
392
426
|
if (pending.photoFileId) {
|
|
393
|
-
await this.deliverPhotoToSessionByFileId(session.id, pending.photoFileId, pending.caption);
|
|
427
|
+
delivered = await this.deliverPhotoToSessionByFileId(session.id, pending.photoFileId, pending.caption);
|
|
394
428
|
} else if (pending.text) {
|
|
395
429
|
this.deliverToSession(session.id, pending.text);
|
|
396
430
|
}
|
|
397
|
-
this.sendMessage(`Sent to [${this.getLabel(session)}]`);
|
|
431
|
+
if (delivered) this.sendMessage(`Sent to [${this.getLabel(session)}]`);
|
|
398
432
|
} else {
|
|
399
433
|
this.sendMessage("Invalid session number.");
|
|
400
434
|
}
|
|
@@ -415,16 +449,19 @@ ${this.mdToHtml(message)}`;
|
|
|
415
449
|
}
|
|
416
450
|
return;
|
|
417
451
|
}
|
|
452
|
+
const selectionId = randomUUID().replace(/-/g, "").slice(0, 8);
|
|
453
|
+
this.latestSelection = selectionId;
|
|
418
454
|
const sessionIds = sessions.map((s) => s.id);
|
|
419
455
|
if (hasPhoto) {
|
|
420
456
|
const largest = msg.photo[msg.photo.length - 1];
|
|
421
|
-
this.
|
|
457
|
+
this.selections.set(selectionId, { photoFileId: largest.file_id, caption: text, sessionIds });
|
|
422
458
|
} else {
|
|
423
|
-
this.
|
|
459
|
+
this.selections.set(selectionId, { text, sessionIds });
|
|
424
460
|
}
|
|
461
|
+
while (this.selections.size > MAX_SELECTIONS) this.selections.delete(this.selections.keys().next().value);
|
|
425
462
|
const buttons = sessions.map((s, i) => ({
|
|
426
463
|
text: this.getLabel(s),
|
|
427
|
-
callback_data: `
|
|
464
|
+
callback_data: `sel:${selectionId}:${i}`
|
|
428
465
|
}));
|
|
429
466
|
const rows = [];
|
|
430
467
|
for (let i = 0; i < buttons.length; i += 2) {
|
|
@@ -439,24 +476,27 @@ ${this.mdToHtml(message)}`;
|
|
|
439
476
|
}
|
|
440
477
|
async deliverPhotoToSession(sessionId, photos, caption) {
|
|
441
478
|
const largest = photos[photos.length - 1];
|
|
442
|
-
|
|
479
|
+
return this.deliverPhotoToSessionByFileId(sessionId, largest.file_id, caption);
|
|
443
480
|
}
|
|
444
481
|
async deliverPhotoToSessionByFileId(sessionId, fileId, caption) {
|
|
445
482
|
try {
|
|
446
483
|
const fileRes = await fetch(`${this.apiUrl}/getFile?file_id=${fileId}`);
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
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;
|
|
450
488
|
}
|
|
451
|
-
const fileData = await fileRes.json();
|
|
452
|
-
if (!fileData.ok) return;
|
|
453
489
|
const downloadUrl = `https://api.telegram.org/file/bot${this.config.botToken}/${fileData.result.file_path}`;
|
|
454
490
|
const imgRes = await fetch(downloadUrl);
|
|
455
491
|
if (!imgRes.ok) {
|
|
456
|
-
|
|
457
|
-
return;
|
|
492
|
+
this.photoNotDelivered("the download failed");
|
|
493
|
+
return false;
|
|
458
494
|
}
|
|
459
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
|
+
}
|
|
460
500
|
const ext = fileData.result.file_path.split(".").pop() || "jpg";
|
|
461
501
|
const mimeType = ext === "png" ? "image/png" : ext === "gif" ? "image/gif" : ext === "webp" ? "image/webp" : "image/jpeg";
|
|
462
502
|
fs.mkdirSync(UPLOADS_DIR, { recursive: true });
|
|
@@ -472,11 +512,18 @@ ${this.mdToHtml(message)}`;
|
|
|
472
512
|
fs.unlinkSync(filePath);
|
|
473
513
|
} catch {
|
|
474
514
|
}
|
|
475
|
-
}, 5 * 60 * 1e3);
|
|
515
|
+
}, 5 * 60 * 1e3).unref();
|
|
516
|
+
return this.onImageToSession !== void 0;
|
|
476
517
|
} catch (err) {
|
|
477
518
|
logger.warn(`Telegram photo download failed: ${err.message}`);
|
|
519
|
+
this.photoNotDelivered("the download failed");
|
|
520
|
+
return false;
|
|
478
521
|
}
|
|
479
522
|
}
|
|
523
|
+
photoNotDelivered(reason) {
|
|
524
|
+
logger.warn(`Telegram photo not delivered: ${reason}`);
|
|
525
|
+
void this.sendMessage(`Photo not delivered: ${reason}`);
|
|
526
|
+
}
|
|
480
527
|
// Buttons are numbered against the list shown when they were sent; Codex sessions come and go, so the live list may have shifted.
|
|
481
528
|
pendingSession(pending, idx) {
|
|
482
529
|
const id = pending.sessionIds[idx];
|
|
@@ -590,7 +637,7 @@ ${this.escHtml(description)}` : "") + (slice ? `
|
|
|
590
637
|
async handleChoiceCallback(query) {
|
|
591
638
|
const choice = this.choiceTokens.get(query.data.slice("pc:".length));
|
|
592
639
|
if (!choice) {
|
|
593
|
-
await this.
|
|
640
|
+
await this.expire(query);
|
|
594
641
|
return;
|
|
595
642
|
}
|
|
596
643
|
this.dropChoiceTokens(choice.sessionId, choice.requestId);
|
|
@@ -619,10 +666,14 @@ ${this.escHtml(description)}` : "") + (slice ? `
|
|
|
619
666
|
async handleCallbackQuery(query) {
|
|
620
667
|
if (!query.data) return;
|
|
621
668
|
if (String(query.message?.chat.id) !== String(this.config.chatId)) return;
|
|
622
|
-
if (query.data.startsWith("
|
|
669
|
+
if (query.data.startsWith("sel:")) {
|
|
623
670
|
await this.handleSessionSelectCallback(query);
|
|
624
671
|
return;
|
|
625
672
|
}
|
|
673
|
+
if (query.data.startsWith("sess:")) {
|
|
674
|
+
await this.expire(query);
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
626
677
|
if (query.data.startsWith("pc:")) {
|
|
627
678
|
await this.handleChoiceCallback(query);
|
|
628
679
|
return;
|
|
@@ -645,19 +696,25 @@ ${label}`);
|
|
|
645
696
|
}
|
|
646
697
|
}
|
|
647
698
|
async handleSessionSelectCallback(query) {
|
|
648
|
-
const
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
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) {
|
|
655
707
|
await this.answerCallbackQuery(query.id, "Session not found");
|
|
656
708
|
return;
|
|
657
709
|
}
|
|
658
|
-
this.
|
|
710
|
+
this.selections.delete(selectionId);
|
|
659
711
|
if (pending.photoFileId) {
|
|
660
|
-
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
|
+
}
|
|
661
718
|
} else if (pending.text) {
|
|
662
719
|
this.deliverToSession(session.id, pending.text);
|
|
663
720
|
}
|
|
@@ -677,6 +734,21 @@ ${label}`);
|
|
|
677
734
|
logger.warn(`Telegram answerCallbackQuery error: ${err.message}`);
|
|
678
735
|
}
|
|
679
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
|
+
}
|
|
680
752
|
async editMessageText(chatId, messageId, text) {
|
|
681
753
|
try {
|
|
682
754
|
await fetch(`${this.apiUrl}/editMessageText`, {
|
|
@@ -773,19 +845,30 @@ var HEALTHY_RUN_MS = 6e4;
|
|
|
773
845
|
function resolveAdapterScript(baseDir) {
|
|
774
846
|
return [path3.join(baseDir, "..", "codex", "main.js"), path3.join(baseDir, "codex", "main.js")].find((p) => fs2.existsSync(p));
|
|
775
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
|
+
}
|
|
776
853
|
var CodexSupervisor = class {
|
|
777
|
-
constructor(script,
|
|
854
|
+
constructor(script, opts = {}) {
|
|
778
855
|
this.script = script;
|
|
779
|
-
this.spawnFn = spawnFn;
|
|
780
|
-
this.minDelayMs = minDelayMs;
|
|
781
|
-
this.maxDelayMs = maxDelayMs;
|
|
782
|
-
this.stopGraceMs = stopGraceMs;
|
|
783
|
-
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;
|
|
784
862
|
}
|
|
785
863
|
child;
|
|
786
864
|
timer;
|
|
787
865
|
stopped = true;
|
|
788
866
|
delay;
|
|
867
|
+
spawnFn;
|
|
868
|
+
minDelayMs;
|
|
869
|
+
maxDelayMs;
|
|
870
|
+
stopGraceMs;
|
|
871
|
+
env;
|
|
789
872
|
start() {
|
|
790
873
|
this.stopped = false;
|
|
791
874
|
this.launch();
|
|
@@ -807,7 +890,8 @@ var CodexSupervisor = class {
|
|
|
807
890
|
const child = this.spawnFn(process.execPath, [this.script, "--watch-stdin"], {
|
|
808
891
|
detached: true,
|
|
809
892
|
stdio: ["pipe", "pipe", "pipe"],
|
|
810
|
-
windowsHide: true
|
|
893
|
+
windowsHide: true,
|
|
894
|
+
...this.env ? { env: this.env } : {}
|
|
811
895
|
});
|
|
812
896
|
this.child = child;
|
|
813
897
|
child.stdout?.on("data", (d) => process.stdout.write(d));
|
|
@@ -1591,7 +1675,7 @@ var HubServer = class {
|
|
|
1591
1675
|
channelWs.send(JSON.stringify({ ...msg, source: "dashboard" }));
|
|
1592
1676
|
}
|
|
1593
1677
|
} else if (msg.type === "image_upload") {
|
|
1594
|
-
this.handleImageUpload(msg);
|
|
1678
|
+
this.handleImageUpload(ws, msg);
|
|
1595
1679
|
} else if (msg.type === "permission_response") {
|
|
1596
1680
|
if (this.forwardPermissionResponse(msg)) {
|
|
1597
1681
|
const verdict = msg.choiceId !== void 0 ? `choice ${msg.choiceId}` : msg.behavior;
|
|
@@ -1642,20 +1726,31 @@ var HubServer = class {
|
|
|
1642
1726
|
}
|
|
1643
1727
|
}
|
|
1644
1728
|
}
|
|
1645
|
-
handleImageUpload(msg) {
|
|
1729
|
+
handleImageUpload(ws, msg) {
|
|
1646
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
|
+
}
|
|
1647
1741
|
if (!this.localChannels.has(sessionId)) {
|
|
1648
|
-
|
|
1742
|
+
reject("this session is on another PC; images can only go to sessions on the hub's PC");
|
|
1649
1743
|
return;
|
|
1650
1744
|
}
|
|
1651
|
-
const channelWs = this.channelSockets.get(sessionId);
|
|
1652
|
-
if (!channelWs || channelWs.readyState !== WebSocket.OPEN) return;
|
|
1653
1745
|
const allowedTypes = ["image/png", "image/jpeg", "image/gif", "image/webp"];
|
|
1654
|
-
if (!allowedTypes.includes(mimeType))
|
|
1746
|
+
if (!allowedTypes.includes(mimeType)) {
|
|
1747
|
+
reject("only PNG, JPEG, GIF and WebP images are supported");
|
|
1748
|
+
return;
|
|
1749
|
+
}
|
|
1655
1750
|
const base64Data = imageData.replace(/^data:image\/\w+;base64,/, "");
|
|
1656
1751
|
const buffer = Buffer.from(base64Data, "base64");
|
|
1657
1752
|
if (buffer.length > 10 * 1024 * 1024) {
|
|
1658
|
-
|
|
1753
|
+
reject("the image is larger than 10 MB");
|
|
1659
1754
|
return;
|
|
1660
1755
|
}
|
|
1661
1756
|
fs5.mkdirSync(UPLOADS_DIR, { recursive: true });
|
|
@@ -1840,7 +1935,7 @@ var HubServer = class {
|
|
|
1840
1935
|
logger.warn("Codex adapter is enabled but codex/main.js was not found next to the hub");
|
|
1841
1936
|
return;
|
|
1842
1937
|
}
|
|
1843
|
-
this.codexSupervisor = new CodexSupervisor(script);
|
|
1938
|
+
this.codexSupervisor = new CodexSupervisor(script, { env: adapterEnv(process.env, { host: this.host, port: this.port }) });
|
|
1844
1939
|
this.codexSupervisor.start();
|
|
1845
1940
|
logger.info("Codex adapter started");
|
|
1846
1941
|
}
|