@delt/claude-alarm 1.2.0 → 1.4.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 +574 -206
- package/dist/cli.js.map +1 -1
- package/dist/codex/main.js +459 -93
- package/dist/codex/main.js.map +1 -1
- package/dist/dashboard/index.html +86 -41
- package/dist/hub/server.js +189 -69
- package/dist/hub/server.js.map +1 -1
- package/dist/index.d.ts +19 -3
- package/dist/index.js +189 -69
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/dashboard/index.html +86 -41
|
@@ -1285,6 +1285,24 @@
|
|
|
1285
1285
|
el.classList.toggle('show', !!msg);
|
|
1286
1286
|
}
|
|
1287
1287
|
|
|
1288
|
+
function showUploadRejected(msg) {
|
|
1289
|
+
showNotDelivered(msg.sessionId, msg.withText ? 'Image and message not delivered' : 'Image not delivered', msg.reason);
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
function showMessageRejected(msg) {
|
|
1293
|
+
showNotDelivered(msg.sessionId, 'Message not delivered', msg.reason);
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
function showNotDelivered(sessionId, label, reason) {
|
|
1297
|
+
state.waitingReply[sessionId] = false;
|
|
1298
|
+
if (state.selectedSession === sessionId) {
|
|
1299
|
+
renderMessages();
|
|
1300
|
+
showMentionError(`${label}: ${reason}`);
|
|
1301
|
+
}
|
|
1302
|
+
state.notifications.unshift({ sessionId, title: label, message: reason, level: 'warning', time: Date.now() });
|
|
1303
|
+
renderNotifications();
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1288
1306
|
let mentionState = { open: false, items: [], index: 0, start: -1 };
|
|
1289
1307
|
|
|
1290
1308
|
function currentMentionQuery(input) {
|
|
@@ -1448,32 +1466,18 @@
|
|
|
1448
1466
|
state.notifications.unshift({ sessionId: msg.sessionId, title: 'Reply', message: msg.content.slice(0, 100), level: 'info', time: msg.timestamp });
|
|
1449
1467
|
renderNotifications();
|
|
1450
1468
|
break;
|
|
1469
|
+
case 'upload_rejected':
|
|
1470
|
+
showUploadRejected(msg);
|
|
1471
|
+
break;
|
|
1472
|
+
case 'message_rejected':
|
|
1473
|
+
showMessageRejected(msg);
|
|
1474
|
+
break;
|
|
1451
1475
|
case 'notification':
|
|
1452
1476
|
state.notifications.unshift({ sessionId: msg.sessionId, title: msg.title, message: msg.message, level: msg.level || 'info', time: msg.timestamp });
|
|
1453
1477
|
renderNotifications();
|
|
1454
1478
|
break;
|
|
1455
1479
|
case 'permission_request':
|
|
1456
|
-
|
|
1457
|
-
state.permissionRequests[msg.sessionId].unshift({
|
|
1458
|
-
requestId: msg.requestId,
|
|
1459
|
-
toolName: msg.toolName,
|
|
1460
|
-
description: msg.description,
|
|
1461
|
-
inputPreview: msg.inputPreview,
|
|
1462
|
-
timestamp: msg.timestamp,
|
|
1463
|
-
resolved: false,
|
|
1464
|
-
behavior: null,
|
|
1465
|
-
choices: msg.choices || null,
|
|
1466
|
-
sent: null,
|
|
1467
|
-
outcome: null,
|
|
1468
|
-
});
|
|
1469
|
-
// Auto-select session if none selected
|
|
1470
|
-
if (!state.selectedSession && state.sessions[msg.sessionId]) selectSession(msg.sessionId);
|
|
1471
|
-
if (state.selectedSession === msg.sessionId) { renderMessages(); renderPermissionBar(); }
|
|
1472
|
-
// Also show in notifications with permission info
|
|
1473
|
-
state.notifications.unshift({ sessionId: msg.sessionId, title: 'Permission Request', message: `${msg.toolName}: ${msg.description}`, level: 'warning', time: msg.timestamp, permRequestId: msg.requestId });
|
|
1474
|
-
renderNotifications();
|
|
1475
|
-
// Play notification sound effect via title flash
|
|
1476
|
-
flashTitle('Permission Request');
|
|
1480
|
+
showPermissionRequest(msg);
|
|
1477
1481
|
break;
|
|
1478
1482
|
case 'permission_response':
|
|
1479
1483
|
// Another source (Telegram, other dashboard) resolved a permission request
|
|
@@ -1487,11 +1491,7 @@
|
|
|
1487
1491
|
}
|
|
1488
1492
|
break;
|
|
1489
1493
|
case 'permission_pending':
|
|
1490
|
-
|
|
1491
|
-
handleMessage({ type: 'permission_request', ...m });
|
|
1492
|
-
}
|
|
1493
|
-
renderPermissionBar();
|
|
1494
|
-
renderNotifications();
|
|
1494
|
+
restorePendingRequests(msg.requests || []);
|
|
1495
1495
|
break;
|
|
1496
1496
|
case 'codex_adapters': {
|
|
1497
1497
|
state.codexAdapters = msg.adapters || [];
|
|
@@ -2114,6 +2114,46 @@
|
|
|
2114
2114
|
return missing;
|
|
2115
2115
|
}
|
|
2116
2116
|
|
|
2117
|
+
function addPermissionRequest(msg) {
|
|
2118
|
+
if (!state.permissionRequests[msg.sessionId]) state.permissionRequests[msg.sessionId] = [];
|
|
2119
|
+
state.permissionRequests[msg.sessionId].unshift({
|
|
2120
|
+
requestId: msg.requestId,
|
|
2121
|
+
toolName: msg.toolName,
|
|
2122
|
+
description: msg.description,
|
|
2123
|
+
inputPreview: msg.inputPreview,
|
|
2124
|
+
timestamp: msg.timestamp,
|
|
2125
|
+
resolved: false,
|
|
2126
|
+
behavior: null,
|
|
2127
|
+
choices: msg.choices || null,
|
|
2128
|
+
sent: null,
|
|
2129
|
+
outcome: null,
|
|
2130
|
+
});
|
|
2131
|
+
if (!state.selectedSession && state.sessions[msg.sessionId]) selectSession(msg.sessionId);
|
|
2132
|
+
if (state.selectedSession === msg.sessionId) { renderMessages(); renderPermissionBar(); }
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
function showPermissionRequest(msg) {
|
|
2136
|
+
addPermissionRequest(msg);
|
|
2137
|
+
state.notifications.unshift({ sessionId: msg.sessionId, title: 'Permission Request', message: `${msg.toolName}: ${msg.description}`, level: 'warning', time: msg.timestamp, permRequestId: msg.requestId });
|
|
2138
|
+
renderNotifications();
|
|
2139
|
+
flashTitle('Permission Request');
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
// A reconnect can bring back many requests at once; one row and one flash keep them from burying the notifications.
|
|
2143
|
+
function restorePendingRequests(requests) {
|
|
2144
|
+
const restored = applyPendingChoices(state.permissionRequests, requests);
|
|
2145
|
+
for (const m of restored) addPermissionRequest(m);
|
|
2146
|
+
if (restored.length) {
|
|
2147
|
+
const label = `${restored.length} approval request(s) waiting`;
|
|
2148
|
+
const listed = restored.slice(0, 3).map(m => `${m.toolName}: ${m.description}`).join(' · ');
|
|
2149
|
+
const message = restored.length > 3 ? `${listed} …and ${restored.length - 3} more` : listed;
|
|
2150
|
+
state.notifications.unshift({ sessionId: restored[0].sessionId, title: label, message, level: 'warning', time: Date.now() });
|
|
2151
|
+
flashTitle(label);
|
|
2152
|
+
}
|
|
2153
|
+
renderPermissionBar();
|
|
2154
|
+
renderNotifications();
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2117
2157
|
function sendPermissionChoice(sessionId, requestId, choiceId) {
|
|
2118
2158
|
if (!state.ws || state.ws.readyState !== WebSocket.OPEN) return;
|
|
2119
2159
|
state.ws.send(JSON.stringify({ type: 'permission_response', sessionId, requestId, choiceId }));
|
|
@@ -2197,23 +2237,28 @@
|
|
|
2197
2237
|
}, true);
|
|
2198
2238
|
|
|
2199
2239
|
// Flash title for attention
|
|
2200
|
-
let
|
|
2240
|
+
let flash = null;
|
|
2201
2241
|
function flashTitle(msg) {
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
document.title = original;
|
|
2213
|
-
|
|
2242
|
+
// While flashing, document.title may hold the flash text, so only a flash that starts from rest reads the real title.
|
|
2243
|
+
if (flash) {
|
|
2244
|
+
flash.msg = msg;
|
|
2245
|
+
return;
|
|
2246
|
+
}
|
|
2247
|
+
const current = { msg, original: document.title, on: true, interval: null, timeout: null };
|
|
2248
|
+
const stop = () => {
|
|
2249
|
+
clearInterval(current.interval);
|
|
2250
|
+
clearTimeout(current.timeout);
|
|
2251
|
+
window.removeEventListener('focus', stop);
|
|
2252
|
+
document.title = current.original;
|
|
2253
|
+
flash = null;
|
|
2214
2254
|
};
|
|
2215
|
-
|
|
2216
|
-
|
|
2255
|
+
current.interval = setInterval(() => {
|
|
2256
|
+
document.title = current.on ? `** ${current.msg} **` : current.original;
|
|
2257
|
+
current.on = !current.on;
|
|
2258
|
+
}, 500);
|
|
2259
|
+
current.timeout = setTimeout(stop, 30000);
|
|
2260
|
+
window.addEventListener('focus', stop);
|
|
2261
|
+
flash = current;
|
|
2217
2262
|
}
|
|
2218
2263
|
|
|
2219
2264
|
// Clear all notifications
|
package/dist/hub/server.js
CHANGED
|
@@ -245,6 +245,14 @@ ${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
|
+
var NO_LONGER_CONNECTED = "the session is no longer connected";
|
|
253
|
+
function visibleLength(html) {
|
|
254
|
+
return html.replace(/<[^>]*>/g, "").replace(/&(?:amp|lt|gt);/g, "&").length;
|
|
255
|
+
}
|
|
248
256
|
var TelegramBot = class _TelegramBot {
|
|
249
257
|
config;
|
|
250
258
|
offset = 0;
|
|
@@ -265,9 +273,9 @@ var TelegramBot = class _TelegramBot {
|
|
|
265
273
|
choiceMessages = /* @__PURE__ */ new Map();
|
|
266
274
|
// Callback: get current sessions list
|
|
267
275
|
getSessions;
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
276
|
+
latestSelection;
|
|
277
|
+
// Keyed by prompt, not by chat: buttons from an older prompt must still send that prompt's message.
|
|
278
|
+
selections = /* @__PURE__ */ new Map();
|
|
271
279
|
constructor(config) {
|
|
272
280
|
this.config = config;
|
|
273
281
|
}
|
|
@@ -276,8 +284,7 @@ var TelegramBot = class _TelegramBot {
|
|
|
276
284
|
}
|
|
277
285
|
/** Send a notification message to Telegram */
|
|
278
286
|
async sendNotification(sessionId, _sessionLabel, title, message) {
|
|
279
|
-
const text =
|
|
280
|
-
${this.mdToHtml(message)}`;
|
|
287
|
+
const text = this.fitNotification(title, message);
|
|
281
288
|
const result = await this.sendMessage(text);
|
|
282
289
|
if (result?.message_id) {
|
|
283
290
|
this.messageSessionMap.set(result.message_id, sessionId);
|
|
@@ -289,6 +296,25 @@ ${this.mdToHtml(message)}`;
|
|
|
289
296
|
}
|
|
290
297
|
}
|
|
291
298
|
}
|
|
299
|
+
fitNotification(title, message) {
|
|
300
|
+
const render = (body) => `<b>${this.escHtml(title)}</b>
|
|
301
|
+
${this.mdToHtml(body)}`;
|
|
302
|
+
const full = render(message);
|
|
303
|
+
if (visibleLength(full) <= MAX_VISIBLE_CHARS) return full;
|
|
304
|
+
const cut = (n) => {
|
|
305
|
+
const end = n > 0 && /[\uD800-\uDBFF]/.test(message[n - 1]) ? n - 1 : n;
|
|
306
|
+
return render(`${message.slice(0, end)}
|
|
307
|
+
${TRUNCATED}`);
|
|
308
|
+
};
|
|
309
|
+
let lo = 0;
|
|
310
|
+
let hi = message.length;
|
|
311
|
+
while (lo < hi) {
|
|
312
|
+
const mid = Math.ceil((lo + hi) / 2);
|
|
313
|
+
if (visibleLength(cut(mid)) <= MAX_VISIBLE_CHARS) lo = mid;
|
|
314
|
+
else hi = mid - 1;
|
|
315
|
+
}
|
|
316
|
+
return cut(lo);
|
|
317
|
+
}
|
|
292
318
|
/** Send a text message to the configured chat */
|
|
293
319
|
async sendMessage(text, replyToMessageId, replyMarkup) {
|
|
294
320
|
try {
|
|
@@ -370,6 +396,13 @@ ${this.mdToHtml(message)}`;
|
|
|
370
396
|
const hasPhoto = msg.photo && msg.photo.length > 0;
|
|
371
397
|
const text = (msg.text || msg.caption || "").trim();
|
|
372
398
|
if (!text && !hasPhoto) return;
|
|
399
|
+
if (hasPhoto) {
|
|
400
|
+
const size = msg.photo[msg.photo.length - 1].file_size;
|
|
401
|
+
if (size !== void 0 && size > MAX_PHOTO_BYTES) {
|
|
402
|
+
this.photoNotDelivered("it is larger than 10 MB");
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
373
406
|
if (msg.reply_to_message) {
|
|
374
407
|
const sessionId = this.messageSessionMap.get(msg.reply_to_message.message_id);
|
|
375
408
|
if (sessionId) {
|
|
@@ -383,18 +416,14 @@ ${this.mdToHtml(message)}`;
|
|
|
383
416
|
}
|
|
384
417
|
if (text) {
|
|
385
418
|
const selectMatch = text.match(/^\/s_(\d+)$/);
|
|
386
|
-
|
|
387
|
-
|
|
419
|
+
const latest = this.latestSelection;
|
|
420
|
+
if (selectMatch && latest !== void 0) {
|
|
421
|
+
const pending = this.selections.get(latest);
|
|
388
422
|
if (pending) {
|
|
389
|
-
this.
|
|
423
|
+
this.selections.delete(latest);
|
|
390
424
|
const session = this.pendingSession(pending, parseInt(selectMatch[1], 10) - 1);
|
|
391
425
|
if (session) {
|
|
392
|
-
if (pending
|
|
393
|
-
await this.deliverPhotoToSessionByFileId(session.id, pending.photoFileId, pending.caption);
|
|
394
|
-
} else if (pending.text) {
|
|
395
|
-
this.deliverToSession(session.id, pending.text);
|
|
396
|
-
}
|
|
397
|
-
this.sendMessage(`Sent to [${this.getLabel(session)}]`);
|
|
426
|
+
if (await this.deliverPending(session.id, pending)) this.sendMessage(`Sent to [${this.getLabel(session)}]`);
|
|
398
427
|
} else {
|
|
399
428
|
this.sendMessage("Invalid session number.");
|
|
400
429
|
}
|
|
@@ -415,16 +444,19 @@ ${this.mdToHtml(message)}`;
|
|
|
415
444
|
}
|
|
416
445
|
return;
|
|
417
446
|
}
|
|
447
|
+
const selectionId = randomUUID().replace(/-/g, "").slice(0, 8);
|
|
448
|
+
this.latestSelection = selectionId;
|
|
418
449
|
const sessionIds = sessions.map((s) => s.id);
|
|
419
450
|
if (hasPhoto) {
|
|
420
451
|
const largest = msg.photo[msg.photo.length - 1];
|
|
421
|
-
this.
|
|
452
|
+
this.selections.set(selectionId, { photoFileId: largest.file_id, caption: text, sessionIds });
|
|
422
453
|
} else {
|
|
423
|
-
this.
|
|
454
|
+
this.selections.set(selectionId, { text, sessionIds });
|
|
424
455
|
}
|
|
456
|
+
while (this.selections.size > MAX_SELECTIONS) this.selections.delete(this.selections.keys().next().value);
|
|
425
457
|
const buttons = sessions.map((s, i) => ({
|
|
426
458
|
text: this.getLabel(s),
|
|
427
|
-
callback_data: `
|
|
459
|
+
callback_data: `sel:${selectionId}:${i}`
|
|
428
460
|
}));
|
|
429
461
|
const rows = [];
|
|
430
462
|
for (let i = 0; i < buttons.length; i += 2) {
|
|
@@ -433,30 +465,37 @@ ${this.mdToHtml(message)}`;
|
|
|
433
465
|
this.sendMessage("Multiple sessions active. Select one:", void 0, { inline_keyboard: rows });
|
|
434
466
|
}
|
|
435
467
|
deliverToSession(sessionId, content) {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
468
|
+
const delivered = this.onMessageToSession?.(sessionId, content) === true;
|
|
469
|
+
if (!delivered) void this.sendMessage(`Not delivered: ${NO_LONGER_CONNECTED}`);
|
|
470
|
+
return delivered;
|
|
471
|
+
}
|
|
472
|
+
async deliverPending(sessionId, pending) {
|
|
473
|
+
if (pending.photoFileId) return this.deliverPhotoToSessionByFileId(sessionId, pending.photoFileId, pending.caption);
|
|
474
|
+
return pending.text ? this.deliverToSession(sessionId, pending.text) : false;
|
|
439
475
|
}
|
|
440
476
|
async deliverPhotoToSession(sessionId, photos, caption) {
|
|
441
477
|
const largest = photos[photos.length - 1];
|
|
442
|
-
|
|
478
|
+
return this.deliverPhotoToSessionByFileId(sessionId, largest.file_id, caption);
|
|
443
479
|
}
|
|
444
480
|
async deliverPhotoToSessionByFileId(sessionId, fileId, caption) {
|
|
445
481
|
try {
|
|
446
482
|
const fileRes = await fetch(`${this.apiUrl}/getFile?file_id=${fileId}`);
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
return;
|
|
483
|
+
const fileData = fileRes.ok ? await fileRes.json() : void 0;
|
|
484
|
+
if (!fileData?.ok || !fileData.result) {
|
|
485
|
+
this.photoNotDelivered("Telegram did not return the file");
|
|
486
|
+
return false;
|
|
450
487
|
}
|
|
451
|
-
const fileData = await fileRes.json();
|
|
452
|
-
if (!fileData.ok) return;
|
|
453
488
|
const downloadUrl = `https://api.telegram.org/file/bot${this.config.botToken}/${fileData.result.file_path}`;
|
|
454
489
|
const imgRes = await fetch(downloadUrl);
|
|
455
490
|
if (!imgRes.ok) {
|
|
456
|
-
|
|
457
|
-
return;
|
|
491
|
+
this.photoNotDelivered("the download failed");
|
|
492
|
+
return false;
|
|
458
493
|
}
|
|
459
494
|
const buffer = Buffer.from(await imgRes.arrayBuffer());
|
|
495
|
+
if (buffer.length > MAX_PHOTO_BYTES) {
|
|
496
|
+
this.photoNotDelivered("it is larger than 10 MB");
|
|
497
|
+
return false;
|
|
498
|
+
}
|
|
460
499
|
const ext = fileData.result.file_path.split(".").pop() || "jpg";
|
|
461
500
|
const mimeType = ext === "png" ? "image/png" : ext === "gif" ? "image/gif" : ext === "webp" ? "image/webp" : "image/jpeg";
|
|
462
501
|
fs.mkdirSync(UPLOADS_DIR, { recursive: true });
|
|
@@ -464,19 +503,37 @@ ${this.mdToHtml(message)}`;
|
|
|
464
503
|
const filePath = path2.join(UPLOADS_DIR, filename);
|
|
465
504
|
fs.writeFileSync(filePath, buffer);
|
|
466
505
|
logger.info(`Telegram photo saved: ${filename} (${buffer.length} bytes)`);
|
|
467
|
-
|
|
468
|
-
|
|
506
|
+
let accepted = false;
|
|
507
|
+
try {
|
|
508
|
+
accepted = this.onImageToSession?.(sessionId, filePath, mimeType, caption) === true;
|
|
509
|
+
} catch (err) {
|
|
510
|
+
logger.warn(`Telegram photo hand-off failed: ${err.message}`);
|
|
511
|
+
}
|
|
512
|
+
if (!accepted) {
|
|
513
|
+
try {
|
|
514
|
+
fs.unlinkSync(filePath);
|
|
515
|
+
} catch {
|
|
516
|
+
}
|
|
517
|
+
this.photoNotDelivered(NO_LONGER_CONNECTED);
|
|
518
|
+
return false;
|
|
469
519
|
}
|
|
470
520
|
setTimeout(() => {
|
|
471
521
|
try {
|
|
472
522
|
fs.unlinkSync(filePath);
|
|
473
523
|
} catch {
|
|
474
524
|
}
|
|
475
|
-
}, 5 * 60 * 1e3);
|
|
525
|
+
}, 5 * 60 * 1e3).unref();
|
|
526
|
+
return true;
|
|
476
527
|
} catch (err) {
|
|
477
528
|
logger.warn(`Telegram photo download failed: ${err.message}`);
|
|
529
|
+
this.photoNotDelivered("the download failed");
|
|
530
|
+
return false;
|
|
478
531
|
}
|
|
479
532
|
}
|
|
533
|
+
photoNotDelivered(reason) {
|
|
534
|
+
logger.warn(`Telegram photo not delivered: ${reason}`);
|
|
535
|
+
void this.sendMessage(`Photo not delivered: ${reason}`);
|
|
536
|
+
}
|
|
480
537
|
// Buttons are numbered against the list shown when they were sent; Codex sessions come and go, so the live list may have shifted.
|
|
481
538
|
pendingSession(pending, idx) {
|
|
482
539
|
const id = pending.sessionIds[idx];
|
|
@@ -590,7 +647,7 @@ ${this.escHtml(description)}` : "") + (slice ? `
|
|
|
590
647
|
async handleChoiceCallback(query) {
|
|
591
648
|
const choice = this.choiceTokens.get(query.data.slice("pc:".length));
|
|
592
649
|
if (!choice) {
|
|
593
|
-
await this.
|
|
650
|
+
await this.expire(query);
|
|
594
651
|
return;
|
|
595
652
|
}
|
|
596
653
|
this.dropChoiceTokens(choice.sessionId, choice.requestId);
|
|
@@ -619,10 +676,14 @@ ${this.escHtml(description)}` : "") + (slice ? `
|
|
|
619
676
|
async handleCallbackQuery(query) {
|
|
620
677
|
if (!query.data) return;
|
|
621
678
|
if (String(query.message?.chat.id) !== String(this.config.chatId)) return;
|
|
622
|
-
if (query.data.startsWith("
|
|
679
|
+
if (query.data.startsWith("sel:")) {
|
|
623
680
|
await this.handleSessionSelectCallback(query);
|
|
624
681
|
return;
|
|
625
682
|
}
|
|
683
|
+
if (query.data.startsWith("sess:")) {
|
|
684
|
+
await this.expire(query);
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
626
687
|
if (query.data.startsWith("pc:")) {
|
|
627
688
|
await this.handleChoiceCallback(query);
|
|
628
689
|
return;
|
|
@@ -645,21 +706,22 @@ ${label}`);
|
|
|
645
706
|
}
|
|
646
707
|
}
|
|
647
708
|
async handleSessionSelectCallback(query) {
|
|
648
|
-
const
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
709
|
+
const [, selectionId, idxStr] = query.data.split(":");
|
|
710
|
+
const pending = this.selections.get(selectionId);
|
|
711
|
+
if (!pending) {
|
|
712
|
+
await this.expire(query);
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
const session = this.pendingSession(pending, parseInt(idxStr, 10));
|
|
716
|
+
if (!session) {
|
|
655
717
|
await this.answerCallbackQuery(query.id, "Session not found");
|
|
656
718
|
return;
|
|
657
719
|
}
|
|
658
|
-
this.
|
|
659
|
-
if (pending
|
|
660
|
-
await this.
|
|
661
|
-
|
|
662
|
-
|
|
720
|
+
this.selections.delete(selectionId);
|
|
721
|
+
if (!await this.deliverPending(session.id, pending)) {
|
|
722
|
+
await this.answerCallbackQuery(query.id, pending.photoFileId ? "Photo not delivered" : "Not delivered");
|
|
723
|
+
if (query.message) await this.removeButtons(query.message.chat.id, query.message.message_id);
|
|
724
|
+
return;
|
|
663
725
|
}
|
|
664
726
|
await this.answerCallbackQuery(query.id, `Sent to ${this.getLabel(session)}`);
|
|
665
727
|
if (query.message) {
|
|
@@ -677,6 +739,21 @@ ${label}`);
|
|
|
677
739
|
logger.warn(`Telegram answerCallbackQuery error: ${err.message}`);
|
|
678
740
|
}
|
|
679
741
|
}
|
|
742
|
+
async expire(query) {
|
|
743
|
+
await this.answerCallbackQuery(query.id, "Expired");
|
|
744
|
+
if (query.message) await this.removeButtons(query.message.chat.id, query.message.message_id);
|
|
745
|
+
}
|
|
746
|
+
async removeButtons(chatId, messageId) {
|
|
747
|
+
try {
|
|
748
|
+
await fetch(`${this.apiUrl}/editMessageReplyMarkup`, {
|
|
749
|
+
method: "POST",
|
|
750
|
+
headers: { "Content-Type": "application/json" },
|
|
751
|
+
body: JSON.stringify({ chat_id: chatId, message_id: messageId, reply_markup: { inline_keyboard: [] } })
|
|
752
|
+
});
|
|
753
|
+
} catch (err) {
|
|
754
|
+
logger.warn(`Telegram editMessageReplyMarkup error: ${err.message}`);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
680
757
|
async editMessageText(chatId, messageId, text) {
|
|
681
758
|
try {
|
|
682
759
|
await fetch(`${this.apiUrl}/editMessageText`, {
|
|
@@ -773,19 +850,30 @@ var HEALTHY_RUN_MS = 6e4;
|
|
|
773
850
|
function resolveAdapterScript(baseDir) {
|
|
774
851
|
return [path3.join(baseDir, "..", "codex", "main.js"), path3.join(baseDir, "codex", "main.js")].find((p) => fs2.existsSync(p));
|
|
775
852
|
}
|
|
853
|
+
function adapterEnv(base, hub) {
|
|
854
|
+
const env = { ...base, CLAUDE_ALARM_HUB_HOST: hub.host, CLAUDE_ALARM_HUB_PORT: String(hub.port) };
|
|
855
|
+
delete env.CLAUDE_ALARM_HUB_TOKEN;
|
|
856
|
+
return env;
|
|
857
|
+
}
|
|
776
858
|
var CodexSupervisor = class {
|
|
777
|
-
constructor(script,
|
|
859
|
+
constructor(script, opts = {}) {
|
|
778
860
|
this.script = script;
|
|
779
|
-
this.spawnFn = spawnFn;
|
|
780
|
-
this.minDelayMs = minDelayMs;
|
|
781
|
-
this.maxDelayMs = maxDelayMs;
|
|
782
|
-
this.stopGraceMs = stopGraceMs;
|
|
783
|
-
this.
|
|
861
|
+
this.spawnFn = opts.spawnFn ?? spawn;
|
|
862
|
+
this.minDelayMs = opts.minDelayMs ?? 2e3;
|
|
863
|
+
this.maxDelayMs = opts.maxDelayMs ?? 6e4;
|
|
864
|
+
this.stopGraceMs = opts.stopGraceMs ?? 3e3;
|
|
865
|
+
this.env = opts.env;
|
|
866
|
+
this.delay = this.minDelayMs;
|
|
784
867
|
}
|
|
785
868
|
child;
|
|
786
869
|
timer;
|
|
787
870
|
stopped = true;
|
|
788
871
|
delay;
|
|
872
|
+
spawnFn;
|
|
873
|
+
minDelayMs;
|
|
874
|
+
maxDelayMs;
|
|
875
|
+
stopGraceMs;
|
|
876
|
+
env;
|
|
789
877
|
start() {
|
|
790
878
|
this.stopped = false;
|
|
791
879
|
this.launch();
|
|
@@ -807,7 +895,8 @@ var CodexSupervisor = class {
|
|
|
807
895
|
const child = this.spawnFn(process.execPath, [this.script, "--watch-stdin"], {
|
|
808
896
|
detached: true,
|
|
809
897
|
stdio: ["pipe", "pipe", "pipe"],
|
|
810
|
-
windowsHide: true
|
|
898
|
+
windowsHide: true,
|
|
899
|
+
...this.env ? { env: this.env } : {}
|
|
811
900
|
});
|
|
812
901
|
this.child = child;
|
|
813
902
|
child.stdout?.on("data", (d) => process.stdout.write(d));
|
|
@@ -1387,7 +1476,16 @@ var HubServer = class {
|
|
|
1387
1476
|
this.sessions.updateActivity(msg.sessionId);
|
|
1388
1477
|
const notifySession = this.sessions.get(msg.sessionId);
|
|
1389
1478
|
const notifyLabel = this.getSessionLabel(notifySession);
|
|
1390
|
-
|
|
1479
|
+
const notifyTitle = `[${notifyLabel}] ${msg.title}`;
|
|
1480
|
+
if (msg.to === "api") {
|
|
1481
|
+
logger.info(`${notifyTitle}: ${msg.message}`);
|
|
1482
|
+
break;
|
|
1483
|
+
}
|
|
1484
|
+
if (msg.to === "telegram") {
|
|
1485
|
+
void this.telegramBot?.sendNotification(msg.sessionId, notifyLabel, notifyTitle, msg.message);
|
|
1486
|
+
break;
|
|
1487
|
+
}
|
|
1488
|
+
if (msg.to !== "dashboard") this.notifier.notifyWithSession(msg.sessionId, notifyLabel, notifyTitle, msg.message, msg.level ?? "info");
|
|
1391
1489
|
this.broadcastToDashboards({
|
|
1392
1490
|
type: "notification",
|
|
1393
1491
|
sessionId: msg.sessionId,
|
|
@@ -1589,9 +1687,14 @@ var HubServer = class {
|
|
|
1589
1687
|
const channelWs = this.channelSockets.get(msg.sessionId);
|
|
1590
1688
|
if (channelWs?.readyState === WebSocket.OPEN) {
|
|
1591
1689
|
channelWs.send(JSON.stringify({ ...msg, source: "dashboard" }));
|
|
1690
|
+
} else {
|
|
1691
|
+
const reason = "the session is not connected";
|
|
1692
|
+
logger.warn(`Message rejected for ${msg.sessionId}: ${reason}`);
|
|
1693
|
+
const rejected = { type: "message_rejected", sessionId: msg.sessionId, reason };
|
|
1694
|
+
if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(rejected));
|
|
1592
1695
|
}
|
|
1593
1696
|
} else if (msg.type === "image_upload") {
|
|
1594
|
-
this.handleImageUpload(msg);
|
|
1697
|
+
this.handleImageUpload(ws, msg);
|
|
1595
1698
|
} else if (msg.type === "permission_response") {
|
|
1596
1699
|
if (this.forwardPermissionResponse(msg)) {
|
|
1597
1700
|
const verdict = msg.choiceId !== void 0 ? `choice ${msg.choiceId}` : msg.behavior;
|
|
@@ -1642,20 +1745,31 @@ var HubServer = class {
|
|
|
1642
1745
|
}
|
|
1643
1746
|
}
|
|
1644
1747
|
}
|
|
1645
|
-
handleImageUpload(msg) {
|
|
1748
|
+
handleImageUpload(ws, msg) {
|
|
1646
1749
|
const { sessionId, imageData, mimeType, originalName, content } = msg;
|
|
1750
|
+
const reject = (reason) => {
|
|
1751
|
+
logger.warn(`Image upload rejected for ${sessionId}: ${reason}`);
|
|
1752
|
+
const rejected = { type: "upload_rejected", sessionId, reason, withText: typeof content === "string" && content.trim().length > 0 };
|
|
1753
|
+
if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(rejected));
|
|
1754
|
+
};
|
|
1755
|
+
const channelWs = this.channelSockets.get(sessionId);
|
|
1756
|
+
if (!channelWs || channelWs.readyState !== WebSocket.OPEN) {
|
|
1757
|
+
reject("the session is not connected");
|
|
1758
|
+
return;
|
|
1759
|
+
}
|
|
1647
1760
|
if (!this.localChannels.has(sessionId)) {
|
|
1648
|
-
|
|
1761
|
+
reject("this session is on another PC; images can only go to sessions on the hub's PC");
|
|
1649
1762
|
return;
|
|
1650
1763
|
}
|
|
1651
|
-
const channelWs = this.channelSockets.get(sessionId);
|
|
1652
|
-
if (!channelWs || channelWs.readyState !== WebSocket.OPEN) return;
|
|
1653
1764
|
const allowedTypes = ["image/png", "image/jpeg", "image/gif", "image/webp"];
|
|
1654
|
-
if (!allowedTypes.includes(mimeType))
|
|
1765
|
+
if (!allowedTypes.includes(mimeType)) {
|
|
1766
|
+
reject("only PNG, JPEG, GIF and WebP images are supported");
|
|
1767
|
+
return;
|
|
1768
|
+
}
|
|
1655
1769
|
const base64Data = imageData.replace(/^data:image\/\w+;base64,/, "");
|
|
1656
1770
|
const buffer = Buffer.from(base64Data, "base64");
|
|
1657
1771
|
if (buffer.length > 10 * 1024 * 1024) {
|
|
1658
|
-
|
|
1772
|
+
reject("the image is larger than 10 MB");
|
|
1659
1773
|
return;
|
|
1660
1774
|
}
|
|
1661
1775
|
fs5.mkdirSync(UPLOADS_DIR, { recursive: true });
|
|
@@ -1703,19 +1817,25 @@ var HubServer = class {
|
|
|
1703
1817
|
this.telegramBot.getSessions = () => this.sessions.getAll();
|
|
1704
1818
|
this.telegramBot.onMessageToSession = (sessionId, content) => {
|
|
1705
1819
|
const channelWs = this.channelSockets.get(sessionId);
|
|
1706
|
-
if (channelWs?.readyState
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
logger.info(`Telegram message forwarded to session: ${sessionId}`);
|
|
1820
|
+
if (channelWs?.readyState !== WebSocket.OPEN) {
|
|
1821
|
+
logger.warn(`Telegram message not delivered to ${sessionId}: the session is not connected`);
|
|
1822
|
+
return false;
|
|
1710
1823
|
}
|
|
1824
|
+
const msg = { type: "message_to_session", sessionId, content, source: "telegram" };
|
|
1825
|
+
channelWs.send(JSON.stringify(msg));
|
|
1826
|
+
logger.info(`Telegram message forwarded to session: ${sessionId}`);
|
|
1827
|
+
return true;
|
|
1711
1828
|
};
|
|
1712
1829
|
this.telegramBot.onImageToSession = (sessionId, imagePath, mimeType, caption) => {
|
|
1713
1830
|
const channelWs = this.channelSockets.get(sessionId);
|
|
1714
|
-
if (channelWs?.readyState
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
logger.info(`Telegram photo forwarded to session: ${sessionId}`);
|
|
1831
|
+
if (channelWs?.readyState !== WebSocket.OPEN) {
|
|
1832
|
+
logger.warn(`Telegram photo not delivered to ${sessionId}: the session is not connected`);
|
|
1833
|
+
return false;
|
|
1718
1834
|
}
|
|
1835
|
+
const msg = { type: "image_to_session", sessionId, imagePath, mimeType, content: caption, source: "telegram" };
|
|
1836
|
+
channelWs.send(JSON.stringify(msg));
|
|
1837
|
+
logger.info(`Telegram photo forwarded to session: ${sessionId}`);
|
|
1838
|
+
return true;
|
|
1719
1839
|
};
|
|
1720
1840
|
this.telegramBot.onPermissionVerdict = (sessionId, requestId, behavior) => {
|
|
1721
1841
|
if (this.forwardPermissionResponse({ sessionId, requestId, behavior })) {
|
|
@@ -1840,7 +1960,7 @@ var HubServer = class {
|
|
|
1840
1960
|
logger.warn("Codex adapter is enabled but codex/main.js was not found next to the hub");
|
|
1841
1961
|
return;
|
|
1842
1962
|
}
|
|
1843
|
-
this.codexSupervisor = new CodexSupervisor(script);
|
|
1963
|
+
this.codexSupervisor = new CodexSupervisor(script, { env: adapterEnv(process.env, { host: this.host, port: this.port }) });
|
|
1844
1964
|
this.codexSupervisor.start();
|
|
1845
1965
|
logger.info("Codex adapter started");
|
|
1846
1966
|
}
|