@delt/claude-alarm 1.0.1 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -4
- package/dist/cli.js +165 -13
- package/dist/cli.js.map +1 -1
- package/dist/codex/main.js +147 -12
- package/dist/codex/main.js.map +1 -1
- package/dist/dashboard/index.html +101 -7
- package/dist/hub/server.js +157 -12
- package/dist/hub/server.js.map +1 -1
- package/dist/index.d.ts +37 -2
- package/dist/index.js +157 -12
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/dashboard/index.html +101 -7
|
@@ -589,8 +589,23 @@
|
|
|
589
589
|
background: linear-gradient(180deg, rgba(245,158,11,0.08) 0%, transparent 100%);
|
|
590
590
|
padding: 0;
|
|
591
591
|
display: none;
|
|
592
|
+
flex-shrink: 0;
|
|
593
|
+
max-height: 45vh;
|
|
594
|
+
overflow-y: auto;
|
|
595
|
+
scrollbar-width: thin;
|
|
592
596
|
}
|
|
593
597
|
.permission-bar.active { display: block; }
|
|
598
|
+
.perm-count {
|
|
599
|
+
position: sticky;
|
|
600
|
+
top: 0;
|
|
601
|
+
z-index: 1;
|
|
602
|
+
padding: 6px 20px;
|
|
603
|
+
font-size: 12px;
|
|
604
|
+
font-weight: 600;
|
|
605
|
+
color: var(--yellow);
|
|
606
|
+
background: var(--surface);
|
|
607
|
+
border-bottom: 1px solid var(--border);
|
|
608
|
+
}
|
|
594
609
|
.perm-item {
|
|
595
610
|
display: flex;
|
|
596
611
|
align-items: center;
|
|
@@ -731,6 +746,18 @@
|
|
|
731
746
|
margin-left: 2px;
|
|
732
747
|
font-weight: 400;
|
|
733
748
|
}
|
|
749
|
+
.perm-actions.choices {
|
|
750
|
+
flex-direction: column;
|
|
751
|
+
gap: 6px;
|
|
752
|
+
}
|
|
753
|
+
.perm-actions .perm-choice {
|
|
754
|
+
padding: 7px 16px;
|
|
755
|
+
font-size: 13px;
|
|
756
|
+
background: var(--surface);
|
|
757
|
+
color: var(--text);
|
|
758
|
+
border: 1px solid var(--border);
|
|
759
|
+
}
|
|
760
|
+
.perm-actions .perm-choice:hover { border-color: var(--yellow); }
|
|
734
761
|
/* Permission in notification panel */
|
|
735
762
|
.notif-perm-actions {
|
|
736
763
|
display: flex;
|
|
@@ -1405,6 +1432,9 @@
|
|
|
1405
1432
|
timestamp: msg.timestamp,
|
|
1406
1433
|
resolved: false,
|
|
1407
1434
|
behavior: null,
|
|
1435
|
+
choices: msg.choices || null,
|
|
1436
|
+
sent: null,
|
|
1437
|
+
outcome: null,
|
|
1408
1438
|
});
|
|
1409
1439
|
// Auto-select session if none selected
|
|
1410
1440
|
if (!state.selectedSession && state.sessions[msg.sessionId]) selectSession(msg.sessionId);
|
|
@@ -1426,6 +1456,23 @@
|
|
|
1426
1456
|
renderNotifications();
|
|
1427
1457
|
}
|
|
1428
1458
|
break;
|
|
1459
|
+
case 'permission_pending':
|
|
1460
|
+
for (const m of applyPendingChoices(state.permissionRequests, msg.requests || [])) {
|
|
1461
|
+
handleMessage({ type: 'permission_request', ...m });
|
|
1462
|
+
}
|
|
1463
|
+
renderPermissionBar();
|
|
1464
|
+
renderNotifications();
|
|
1465
|
+
break;
|
|
1466
|
+
case 'permission_resolved': {
|
|
1467
|
+
const req = (state.permissionRequests[msg.sessionId] || []).find(r => r.requestId === msg.requestId);
|
|
1468
|
+
if (req && !req.resolved) {
|
|
1469
|
+
req.resolved = true;
|
|
1470
|
+
req.outcome = msg.state;
|
|
1471
|
+
renderPermissionBar();
|
|
1472
|
+
renderNotifications();
|
|
1473
|
+
}
|
|
1474
|
+
break;
|
|
1475
|
+
}
|
|
1429
1476
|
}
|
|
1430
1477
|
}
|
|
1431
1478
|
|
|
@@ -1664,7 +1711,10 @@
|
|
|
1664
1711
|
if (n.permRequestId) {
|
|
1665
1712
|
const reqs = state.permissionRequests[n.sessionId] || [];
|
|
1666
1713
|
const req = reqs.find(r => r.requestId === n.permRequestId);
|
|
1667
|
-
if (req &&
|
|
1714
|
+
if (req && req.choices) {
|
|
1715
|
+
if (req.resolved) permButtons = `<div class="perm-resolved-label">${req.outcome === 'expired' ? 'Expired' : 'Resolved'}</div>`;
|
|
1716
|
+
else if (req.sent != null) permButtons = '<div class="perm-resolved-label">Sent</div>';
|
|
1717
|
+
} else if (req && !req.resolved) {
|
|
1668
1718
|
permButtons = `<div class="notif-perm-actions">
|
|
1669
1719
|
<button class="notif-perm-allow" data-req-id="${esc(n.permRequestId)}" data-session-id="${esc(n.sessionId)}">Allow</button>
|
|
1670
1720
|
<button class="notif-perm-deny" data-req-id="${esc(n.permRequestId)}" data-session-id="${esc(n.sessionId)}">Deny</button>
|
|
@@ -1951,6 +2001,7 @@
|
|
|
1951
2001
|
let kind = 'code';
|
|
1952
2002
|
let lang = '';
|
|
1953
2003
|
switch (toolName) {
|
|
2004
|
+
case 'Command':
|
|
1954
2005
|
case 'Bash':
|
|
1955
2006
|
text = p.command ? '$ ' + p.command : raw; lang = 'bash'; break;
|
|
1956
2007
|
case 'Write':
|
|
@@ -2001,6 +2052,47 @@
|
|
|
2001
2052
|
renderNotifications();
|
|
2002
2053
|
}
|
|
2003
2054
|
|
|
2055
|
+
// After a reconnect the hub lists the choice requests still waiting; any other unresolved one died with the old connection.
|
|
2056
|
+
function applyPendingChoices(permissionRequests, pending) {
|
|
2057
|
+
const missing = pending.filter(p => !(permissionRequests[p.sessionId] || []).some(r => r.requestId === p.requestId));
|
|
2058
|
+
const live = new Set(pending.map(p => p.sessionId + '\n' + p.requestId));
|
|
2059
|
+
for (const [sid, reqs] of Object.entries(permissionRequests)) {
|
|
2060
|
+
for (const r of reqs) {
|
|
2061
|
+
if (!r.choices || r.resolved) continue;
|
|
2062
|
+
if (!live.has(sid + '\n' + r.requestId)) { r.resolved = true; r.outcome = 'expired'; }
|
|
2063
|
+
else r.sent = null;
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
return missing;
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
function sendPermissionChoice(sessionId, requestId, choiceId) {
|
|
2070
|
+
if (!state.ws || state.ws.readyState !== WebSocket.OPEN) return;
|
|
2071
|
+
state.ws.send(JSON.stringify({ type: 'permission_response', sessionId, requestId, choiceId }));
|
|
2072
|
+
const req = (state.permissionRequests[sessionId] || []).find(r => r.requestId === requestId);
|
|
2073
|
+
if (req) req.sent = choiceId;
|
|
2074
|
+
renderPermissionBar();
|
|
2075
|
+
renderNotifications();
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
function permissionActions(r, sid) {
|
|
2079
|
+
const ids = `data-req-id="${esc(r.requestId)}" data-session-id="${esc(sid)}"`;
|
|
2080
|
+
if (!r.choices) {
|
|
2081
|
+
return `<button class="perm-allow" ${ids}>Allow <span class="perm-kbd">(Enter)</span></button>
|
|
2082
|
+
<button class="perm-deny" ${ids}>Deny <span class="perm-kbd">(Esc)</span></button>`;
|
|
2083
|
+
}
|
|
2084
|
+
if (r.sent != null) {
|
|
2085
|
+
const chosen = r.choices.find(c => c.id === r.sent);
|
|
2086
|
+
return `<div class="perm-resolved-label">Sent: ${esc(chosen ? chosen.label : r.sent)}</div>`;
|
|
2087
|
+
}
|
|
2088
|
+
return r.choices.map(c => `<button class="perm-choice" ${ids} data-choice-id="${esc(c.id)}">${esc(c.label)}</button>`).join('');
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
// Codex choices have no safe default, so Enter/Esc stay with the message box for them.
|
|
2092
|
+
function shortcutRequest(reqs) {
|
|
2093
|
+
return reqs.find(r => !r.resolved && !r.choices);
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2004
2096
|
function renderPermissionBar() {
|
|
2005
2097
|
const bar = $('#permissionBar');
|
|
2006
2098
|
const sid = state.selectedSession;
|
|
@@ -2008,7 +2100,8 @@
|
|
|
2008
2100
|
const reqs = (state.permissionRequests[sid] || []).filter(r => !r.resolved);
|
|
2009
2101
|
if (!reqs.length) { bar.classList.remove('active'); bar.innerHTML = ''; return; }
|
|
2010
2102
|
bar.classList.add('active');
|
|
2011
|
-
|
|
2103
|
+
const count = reqs.length > 1 ? `<div class="perm-count">${reqs.length} permission requests waiting</div>` : '';
|
|
2104
|
+
bar.innerHTML = count + reqs.map(r => {
|
|
2012
2105
|
const { text: previewText, kind: previewKind, lang: previewLang } = formatPermPreview(r.toolName, r.inputPreview);
|
|
2013
2106
|
const displayName = formatToolName(r.toolName, r.inputPreview);
|
|
2014
2107
|
return `<div class="perm-item">
|
|
@@ -2017,12 +2110,10 @@
|
|
|
2017
2110
|
<div class="perm-header">
|
|
2018
2111
|
<span class="perm-tool">${esc(displayName)}</span>
|
|
2019
2112
|
</div>
|
|
2113
|
+
${r.choices && r.description ? `<div class="perm-desc">${esc(r.description)}</div>` : ''}
|
|
2020
2114
|
${previewText ? renderPermPreview(previewText, previewKind, previewLang) : ''}
|
|
2021
2115
|
</div>
|
|
2022
|
-
<div class="perm-actions">
|
|
2023
|
-
<button class="perm-allow" data-req-id="${esc(r.requestId)}" data-session-id="${esc(sid)}">Allow <span class="perm-kbd">(Enter)</span></button>
|
|
2024
|
-
<button class="perm-deny" data-req-id="${esc(r.requestId)}" data-session-id="${esc(sid)}">Deny <span class="perm-kbd">(Esc)</span></button>
|
|
2025
|
-
</div>
|
|
2116
|
+
<div class="perm-actions${r.choices ? ' choices' : ''}">${permissionActions(r, sid)}</div>
|
|
2026
2117
|
</div>`;
|
|
2027
2118
|
}).join('');
|
|
2028
2119
|
bindPermissionButtons(bar);
|
|
@@ -2035,6 +2126,9 @@
|
|
|
2035
2126
|
(container || document).querySelectorAll('.perm-deny, .notif-perm-deny').forEach(btn => {
|
|
2036
2127
|
btn.addEventListener('click', (e) => { e.stopPropagation(); sendPermissionVerdict(btn.dataset.sessionId, btn.dataset.reqId, 'deny'); });
|
|
2037
2128
|
});
|
|
2129
|
+
(container || document).querySelectorAll('.perm-choice').forEach(btn => {
|
|
2130
|
+
btn.addEventListener('click', (e) => { e.stopPropagation(); sendPermissionChoice(btn.dataset.sessionId, btn.dataset.reqId, btn.dataset.choiceId); });
|
|
2131
|
+
});
|
|
2038
2132
|
}
|
|
2039
2133
|
|
|
2040
2134
|
// Keyboard shortcuts: Enter=allow, Esc=deny — works even in textarea when permission is pending
|
|
@@ -2044,7 +2138,7 @@
|
|
|
2044
2138
|
const sid = state.selectedSession;
|
|
2045
2139
|
if (!sid) return;
|
|
2046
2140
|
const reqs = state.permissionRequests[sid] || [];
|
|
2047
|
-
const pending = reqs
|
|
2141
|
+
const pending = shortcutRequest(reqs);
|
|
2048
2142
|
if (!pending) return;
|
|
2049
2143
|
// Permission pending: intercept Enter/Esc everywhere (even in textarea)
|
|
2050
2144
|
// Shift+Enter in textarea still inserts newline
|
package/dist/hub/server.js
CHANGED
|
@@ -234,8 +234,15 @@ function sessionLabel(session) {
|
|
|
234
234
|
return session.agentKind === "codex" ? `Codex \xB7 ${base}` : base;
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
+
// src/shared/permission-key.ts
|
|
238
|
+
function permissionKey(sessionId, requestId) {
|
|
239
|
+
return `${sessionId}
|
|
240
|
+
${requestId}`;
|
|
241
|
+
}
|
|
242
|
+
|
|
237
243
|
// src/hub/telegram.ts
|
|
238
244
|
var TELEGRAM_API = "https://api.telegram.org/bot";
|
|
245
|
+
var MAX_CHOICE_MESSAGES = 200;
|
|
239
246
|
var TelegramBot = class _TelegramBot {
|
|
240
247
|
config;
|
|
241
248
|
offset = 0;
|
|
@@ -249,6 +256,11 @@ var TelegramBot = class _TelegramBot {
|
|
|
249
256
|
onImageToSession;
|
|
250
257
|
// Callback: when a permission verdict arrives from Telegram
|
|
251
258
|
onPermissionVerdict;
|
|
259
|
+
// Callback: when a Codex approval choice arrives from Telegram
|
|
260
|
+
onChoiceVerdict;
|
|
261
|
+
// Telegram caps callback_data at 64 bytes, so buttons carry a short token instead of the ids.
|
|
262
|
+
choiceTokens = /* @__PURE__ */ new Map();
|
|
263
|
+
choiceMessages = /* @__PURE__ */ new Map();
|
|
252
264
|
// Callback: get current sessions list
|
|
253
265
|
getSessions;
|
|
254
266
|
// Pending messages for session selection
|
|
@@ -537,6 +549,71 @@ ${previewHtml}${truncNote}`;
|
|
|
537
549
|
};
|
|
538
550
|
await this.sendMessage(text, void 0, replyMarkup);
|
|
539
551
|
}
|
|
552
|
+
async sendChoiceRequest(sessionId, sessionLabel2, requestId, toolName, description, inputPreview, choices) {
|
|
553
|
+
let preview = inputPreview;
|
|
554
|
+
try {
|
|
555
|
+
const p = JSON.parse(inputPreview);
|
|
556
|
+
if (typeof p.command === "string") preview = `$ ${p.command}`;
|
|
557
|
+
else if (typeof p.content === "string") preview = p.content;
|
|
558
|
+
} catch {
|
|
559
|
+
}
|
|
560
|
+
const slice = preview.slice(0, 3e3);
|
|
561
|
+
const html = `\u26A0\uFE0F <b>Permission Request</b> \u2014 ${this.escHtml(sessionLabel2)}
|
|
562
|
+
|
|
563
|
+
\u{1F527} <b>${this.escHtml(toolName)}</b>` + (description ? `
|
|
564
|
+
${this.escHtml(description)}` : "") + (slice ? `
|
|
565
|
+
<pre>${this.escHtml(slice)}</pre>` : "") + (preview.length > slice.length ? "\n<i>...truncated</i>" : "");
|
|
566
|
+
const entry = { html };
|
|
567
|
+
this.choiceMessages.set(permissionKey(sessionId, requestId), entry);
|
|
568
|
+
const rows = choices.map((c) => {
|
|
569
|
+
const token = randomUUID().replace(/-/g, "").slice(0, 16);
|
|
570
|
+
this.choiceTokens.set(token, { sessionId, requestId, choiceId: c.id, label: c.label });
|
|
571
|
+
return [{ text: c.label, callback_data: `pc:${token}` }];
|
|
572
|
+
});
|
|
573
|
+
this.trimChoiceMessages();
|
|
574
|
+
const sent = await this.sendMessage(html, void 0, { inline_keyboard: rows });
|
|
575
|
+
if (!sent) return;
|
|
576
|
+
entry.messageId = sent.message_id;
|
|
577
|
+
if (entry.outcome) await this.editMessageText(this.config.chatId, sent.message_id, html + entry.outcome);
|
|
578
|
+
}
|
|
579
|
+
async resolveChoiceRequest(sessionId, requestId, state) {
|
|
580
|
+
this.dropChoiceTokens(sessionId, requestId);
|
|
581
|
+
const key = permissionKey(sessionId, requestId);
|
|
582
|
+
const entry = this.choiceMessages.get(key);
|
|
583
|
+
if (!entry) return;
|
|
584
|
+
this.choiceMessages.delete(key);
|
|
585
|
+
entry.outcome = state === "resolved" ? "\n\n\u2705 <b>Resolved</b>" : "\n\n\u231B <b>Expired</b>";
|
|
586
|
+
if (entry.messageId !== void 0) await this.editMessageText(this.config.chatId, entry.messageId, entry.html + entry.outcome);
|
|
587
|
+
}
|
|
588
|
+
async handleChoiceCallback(query) {
|
|
589
|
+
const choice = this.choiceTokens.get(query.data.slice("pc:".length));
|
|
590
|
+
if (!choice) {
|
|
591
|
+
await this.answerCallbackQuery(query.id, "Expired");
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
this.dropChoiceTokens(choice.sessionId, choice.requestId);
|
|
595
|
+
this.onChoiceVerdict?.(choice.sessionId, choice.requestId, choice.choiceId);
|
|
596
|
+
await this.answerCallbackQuery(query.id, `Sent: ${choice.label}`);
|
|
597
|
+
const entry = this.choiceMessages.get(permissionKey(choice.sessionId, choice.requestId));
|
|
598
|
+
if (entry?.messageId !== void 0 && !entry.outcome) {
|
|
599
|
+
await this.editMessageText(this.config.chatId, entry.messageId, `${entry.html}
|
|
600
|
+
|
|
601
|
+
\u23F3 <b>Sent: ${this.escHtml(choice.label)}</b>`);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
dropChoiceTokens(sessionId, requestId) {
|
|
605
|
+
for (const [token, c] of this.choiceTokens) {
|
|
606
|
+
if (c.sessionId === sessionId && c.requestId === requestId) this.choiceTokens.delete(token);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
trimChoiceMessages() {
|
|
610
|
+
while (this.choiceMessages.size > MAX_CHOICE_MESSAGES) {
|
|
611
|
+
const oldest = this.choiceMessages.keys().next().value;
|
|
612
|
+
this.choiceMessages.delete(oldest);
|
|
613
|
+
const [sessionId, requestId] = oldest.split("\n");
|
|
614
|
+
this.dropChoiceTokens(sessionId, requestId);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
540
617
|
async handleCallbackQuery(query) {
|
|
541
618
|
if (!query.data) return;
|
|
542
619
|
if (String(query.message?.chat.id) !== String(this.config.chatId)) return;
|
|
@@ -544,6 +621,10 @@ ${previewHtml}${truncNote}`;
|
|
|
544
621
|
await this.handleSessionSelectCallback(query);
|
|
545
622
|
return;
|
|
546
623
|
}
|
|
624
|
+
if (query.data.startsWith("pc:")) {
|
|
625
|
+
await this.handleChoiceCallback(query);
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
547
628
|
if (!query.data.startsWith("perm:")) return;
|
|
548
629
|
const parts = query.data.split(":");
|
|
549
630
|
if (parts.length < 4) return;
|
|
@@ -899,6 +980,11 @@ function isSecureRequest(req) {
|
|
|
899
980
|
// src/hub/server.ts
|
|
900
981
|
var __dirname = path5.dirname(fileURLToPath(import.meta.url));
|
|
901
982
|
var MAX_WS_PAYLOAD = 16 * 1024 * 1024;
|
|
983
|
+
function validChoices(choices) {
|
|
984
|
+
if (!Array.isArray(choices)) return void 0;
|
|
985
|
+
const valid = choices.filter((c) => typeof c?.id === "string" && typeof c?.label === "string");
|
|
986
|
+
return valid.length ? valid : void 0;
|
|
987
|
+
}
|
|
902
988
|
var HubServer = class {
|
|
903
989
|
httpServer;
|
|
904
990
|
wssChannel;
|
|
@@ -917,6 +1003,8 @@ var HubServer = class {
|
|
|
917
1003
|
channelAlive = /* @__PURE__ */ new Map();
|
|
918
1004
|
// sessionId -> alive flag
|
|
919
1005
|
socketOwners = /* @__PURE__ */ new WeakMap();
|
|
1006
|
+
// Codex approvals carry their own choices; a response is forwarded only in the mode of the request it answers.
|
|
1007
|
+
choiceRequests = /* @__PURE__ */ new Map();
|
|
920
1008
|
host;
|
|
921
1009
|
port;
|
|
922
1010
|
token;
|
|
@@ -1054,6 +1142,7 @@ var HubServer = class {
|
|
|
1054
1142
|
this.channelSockets.delete(sessionId);
|
|
1055
1143
|
this.localChannels.delete(sessionId);
|
|
1056
1144
|
this.channelAlive.delete(sessionId);
|
|
1145
|
+
this.expireChoices(sessionId);
|
|
1057
1146
|
if (session) {
|
|
1058
1147
|
this.broadcastToDashboards({ type: "session_disconnected", sessionId });
|
|
1059
1148
|
this.jsonResponse(res, 200, { ok: true });
|
|
@@ -1197,6 +1286,7 @@ var HubServer = class {
|
|
|
1197
1286
|
this.channelSockets.delete(sessionId);
|
|
1198
1287
|
this.localChannels.delete(sessionId);
|
|
1199
1288
|
this.channelAlive.delete(sessionId);
|
|
1289
|
+
this.expireChoices(sessionId);
|
|
1200
1290
|
logger.info(`Channel disconnected: ${sessionId}`);
|
|
1201
1291
|
this.broadcastToDashboards({
|
|
1202
1292
|
type: "session_disconnected",
|
|
@@ -1284,7 +1374,22 @@ var HubServer = class {
|
|
|
1284
1374
|
}
|
|
1285
1375
|
case "permission_request": {
|
|
1286
1376
|
this.sessions.updateActivity(msg.sessionId);
|
|
1377
|
+
const choices = validChoices(msg.choices);
|
|
1287
1378
|
logger.info(`Permission request [${msg.requestId}] from ${msg.sessionId}: ${msg.toolName}`);
|
|
1379
|
+
if (choices) {
|
|
1380
|
+
this.choiceRequests.set(permissionKey(msg.sessionId, msg.requestId), {
|
|
1381
|
+
request: {
|
|
1382
|
+
sessionId: msg.sessionId,
|
|
1383
|
+
requestId: msg.requestId,
|
|
1384
|
+
toolName: msg.toolName,
|
|
1385
|
+
description: msg.description,
|
|
1386
|
+
inputPreview: msg.inputPreview,
|
|
1387
|
+
timestamp: msg.timestamp,
|
|
1388
|
+
choices
|
|
1389
|
+
},
|
|
1390
|
+
choiceIds: new Set(choices.map((c) => c.id))
|
|
1391
|
+
});
|
|
1392
|
+
}
|
|
1288
1393
|
this.broadcastToDashboards({
|
|
1289
1394
|
type: "permission_request",
|
|
1290
1395
|
sessionId: msg.sessionId,
|
|
@@ -1292,15 +1397,23 @@ var HubServer = class {
|
|
|
1292
1397
|
toolName: msg.toolName,
|
|
1293
1398
|
description: msg.description,
|
|
1294
1399
|
inputPreview: msg.inputPreview,
|
|
1295
|
-
timestamp: msg.timestamp
|
|
1400
|
+
timestamp: msg.timestamp,
|
|
1401
|
+
...choices ? { choices } : {}
|
|
1296
1402
|
});
|
|
1297
1403
|
if (this.telegramBot) {
|
|
1298
|
-
const
|
|
1299
|
-
|
|
1300
|
-
|
|
1404
|
+
const label = this.getSessionLabel(this.sessions.get(msg.sessionId));
|
|
1405
|
+
if (choices) {
|
|
1406
|
+
void this.telegramBot.sendChoiceRequest(msg.sessionId, label, msg.requestId, msg.toolName, msg.description, msg.inputPreview, choices);
|
|
1407
|
+
} else {
|
|
1408
|
+
this.telegramBot.sendPermissionRequest(msg.sessionId, label, msg.requestId, msg.toolName, msg.description, msg.inputPreview);
|
|
1409
|
+
}
|
|
1301
1410
|
}
|
|
1302
1411
|
break;
|
|
1303
1412
|
}
|
|
1413
|
+
case "permission_resolved": {
|
|
1414
|
+
this.resolveChoice(msg.sessionId, msg.requestId, msg.state === "expired" ? "expired" : "resolved");
|
|
1415
|
+
break;
|
|
1416
|
+
}
|
|
1304
1417
|
}
|
|
1305
1418
|
}
|
|
1306
1419
|
// --- Dashboard WebSocket ---
|
|
@@ -1316,6 +1429,11 @@ var HubServer = class {
|
|
|
1316
1429
|
sessions: this.sessions.getAll()
|
|
1317
1430
|
};
|
|
1318
1431
|
ws.send(JSON.stringify(sessionsMsg));
|
|
1432
|
+
const pendingMsg = {
|
|
1433
|
+
type: "permission_pending",
|
|
1434
|
+
requests: [...this.choiceRequests.values()].map((c) => c.request)
|
|
1435
|
+
};
|
|
1436
|
+
ws.send(JSON.stringify(pendingMsg));
|
|
1319
1437
|
ws.on("message", (data) => {
|
|
1320
1438
|
try {
|
|
1321
1439
|
const msg = JSON.parse(data.toString());
|
|
@@ -1327,10 +1445,9 @@ var HubServer = class {
|
|
|
1327
1445
|
} else if (msg.type === "image_upload") {
|
|
1328
1446
|
this.handleImageUpload(msg);
|
|
1329
1447
|
} else if (msg.type === "permission_response") {
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
logger.info(`Permission verdict [${msg.requestId}]: ${msg.behavior} -> session ${msg.sessionId}`);
|
|
1448
|
+
if (this.forwardPermissionResponse(msg)) {
|
|
1449
|
+
const verdict = msg.choiceId !== void 0 ? `choice ${msg.choiceId}` : msg.behavior;
|
|
1450
|
+
logger.info(`Permission verdict [${msg.requestId}]: ${verdict} -> session ${msg.sessionId}`);
|
|
1334
1451
|
}
|
|
1335
1452
|
}
|
|
1336
1453
|
} catch {
|
|
@@ -1343,6 +1460,32 @@ var HubServer = class {
|
|
|
1343
1460
|
});
|
|
1344
1461
|
}
|
|
1345
1462
|
// --- Helpers ---
|
|
1463
|
+
forwardPermissionResponse(msg) {
|
|
1464
|
+
const pending = this.choiceRequests.get(permissionKey(msg.sessionId, msg.requestId));
|
|
1465
|
+
let out;
|
|
1466
|
+
if (pending) {
|
|
1467
|
+
if (typeof msg.choiceId !== "string" || !pending.choiceIds.has(msg.choiceId)) return false;
|
|
1468
|
+
out = { type: "permission_response", sessionId: msg.sessionId, requestId: msg.requestId, choiceId: msg.choiceId };
|
|
1469
|
+
} else {
|
|
1470
|
+
if (msg.behavior !== "allow" && msg.behavior !== "deny" || msg.choiceId !== void 0) return false;
|
|
1471
|
+
if (this.sessions.get(msg.sessionId)?.agentKind === "codex") return false;
|
|
1472
|
+
out = { type: "permission_response", sessionId: msg.sessionId, requestId: msg.requestId, behavior: msg.behavior };
|
|
1473
|
+
}
|
|
1474
|
+
const channelWs = this.channelSockets.get(msg.sessionId);
|
|
1475
|
+
if (channelWs?.readyState !== WebSocket.OPEN) return false;
|
|
1476
|
+
channelWs.send(JSON.stringify(out));
|
|
1477
|
+
return true;
|
|
1478
|
+
}
|
|
1479
|
+
resolveChoice(sessionId, requestId, state) {
|
|
1480
|
+
if (!this.choiceRequests.delete(permissionKey(sessionId, requestId))) return;
|
|
1481
|
+
this.broadcastToDashboards({ type: "permission_resolved", sessionId, requestId, state });
|
|
1482
|
+
void this.telegramBot?.resolveChoiceRequest(sessionId, requestId, state);
|
|
1483
|
+
}
|
|
1484
|
+
expireChoices(sessionId) {
|
|
1485
|
+
for (const pending of [...this.choiceRequests.values()]) {
|
|
1486
|
+
if (pending.request.sessionId === sessionId) this.resolveChoice(sessionId, pending.request.requestId, "expired");
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1346
1489
|
broadcastToDashboards(msg) {
|
|
1347
1490
|
const payload = JSON.stringify(msg);
|
|
1348
1491
|
for (const ws of this.dashboardSockets) {
|
|
@@ -1426,14 +1569,16 @@ var HubServer = class {
|
|
|
1426
1569
|
}
|
|
1427
1570
|
};
|
|
1428
1571
|
this.telegramBot.onPermissionVerdict = (sessionId, requestId, behavior) => {
|
|
1429
|
-
|
|
1430
|
-
if (channelWs?.readyState === WebSocket.OPEN) {
|
|
1431
|
-
const msg = { type: "permission_response", sessionId, requestId, behavior };
|
|
1432
|
-
channelWs.send(JSON.stringify(msg));
|
|
1572
|
+
if (this.forwardPermissionResponse({ sessionId, requestId, behavior })) {
|
|
1433
1573
|
logger.info(`Telegram permission verdict [${requestId}]: ${behavior} -> session ${sessionId}`);
|
|
1434
1574
|
}
|
|
1435
1575
|
this.broadcastToDashboards({ type: "permission_response", sessionId, requestId, behavior });
|
|
1436
1576
|
};
|
|
1577
|
+
this.telegramBot.onChoiceVerdict = (sessionId, requestId, choiceId) => {
|
|
1578
|
+
if (this.forwardPermissionResponse({ sessionId, requestId, choiceId })) {
|
|
1579
|
+
logger.info(`Telegram choice [${requestId}]: ${choiceId} -> session ${sessionId}`);
|
|
1580
|
+
}
|
|
1581
|
+
};
|
|
1437
1582
|
this.notifier.configure({ telegramBot: this.telegramBot });
|
|
1438
1583
|
this.telegramBot.startPolling();
|
|
1439
1584
|
logger.info("Telegram bot initialized");
|