@delt/claude-alarm 0.8.1 → 0.9.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 +14 -0
- package/dist/channel/server.js +69 -5
- package/dist/channel/server.js.map +1 -1
- package/dist/cli.js +176 -38
- package/dist/cli.js.map +1 -1
- package/dist/dashboard/index.html +279 -26
- package/dist/hub/server.js +77 -9
- package/dist/hub/server.js.map +1 -1
- package/dist/index.d.ts +10 -1
- package/dist/index.js +81 -11
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
- package/src/dashboard/index.html +279 -26
|
@@ -187,6 +187,7 @@
|
|
|
187
187
|
outline: none;
|
|
188
188
|
font-family: inherit;
|
|
189
189
|
}
|
|
190
|
+
.name-edit-input.invalid { border-color: var(--red); }
|
|
190
191
|
.session-name {
|
|
191
192
|
font-size: 14px;
|
|
192
193
|
font-weight: 500;
|
|
@@ -400,6 +401,39 @@
|
|
|
400
401
|
}
|
|
401
402
|
.drag-overlay.active { display: flex; }
|
|
402
403
|
|
|
404
|
+
.mention-error {
|
|
405
|
+
display: none;
|
|
406
|
+
padding: 6px 20px 0;
|
|
407
|
+
color: var(--red);
|
|
408
|
+
font-size: 12px;
|
|
409
|
+
}
|
|
410
|
+
.mention-error.show { display: block; }
|
|
411
|
+
.message-input-area { position: relative; }
|
|
412
|
+
.mention-popup {
|
|
413
|
+
display: none;
|
|
414
|
+
position: absolute;
|
|
415
|
+
bottom: calc(100% + 4px);
|
|
416
|
+
left: 20px;
|
|
417
|
+
min-width: 220px;
|
|
418
|
+
max-height: 200px;
|
|
419
|
+
overflow-y: auto;
|
|
420
|
+
background: var(--surface);
|
|
421
|
+
border: 1px solid var(--border);
|
|
422
|
+
border-radius: 6px;
|
|
423
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
|
|
424
|
+
z-index: 20;
|
|
425
|
+
}
|
|
426
|
+
.mention-popup.open { display: block; }
|
|
427
|
+
.mention-item {
|
|
428
|
+
display: flex;
|
|
429
|
+
justify-content: space-between;
|
|
430
|
+
gap: 12px;
|
|
431
|
+
padding: 6px 10px;
|
|
432
|
+
font-size: 13px;
|
|
433
|
+
cursor: pointer;
|
|
434
|
+
}
|
|
435
|
+
.mention-item.active, .mention-item:hover { background: var(--border); }
|
|
436
|
+
.mention-folder { color: var(--text-dim); font-size: 11px; }
|
|
403
437
|
.message-input-area {
|
|
404
438
|
border-top: 1px solid var(--border);
|
|
405
439
|
padding: 12px 20px;
|
|
@@ -617,12 +651,35 @@
|
|
|
617
651
|
border-radius: 5px;
|
|
618
652
|
padding: 6px 10px;
|
|
619
653
|
margin-top: 5px;
|
|
620
|
-
max-height:
|
|
621
|
-
overflow
|
|
654
|
+
max-height: 220px;
|
|
655
|
+
overflow: auto;
|
|
622
656
|
white-space: pre-wrap;
|
|
623
657
|
word-break: break-all;
|
|
624
658
|
line-height: 1.4;
|
|
625
659
|
}
|
|
660
|
+
.perm-preview pre {
|
|
661
|
+
margin: 0;
|
|
662
|
+
white-space: pre;
|
|
663
|
+
font-family: inherit;
|
|
664
|
+
font-size: inherit;
|
|
665
|
+
}
|
|
666
|
+
.perm-preview code { font-family: inherit; }
|
|
667
|
+
.perm-preview.prose {
|
|
668
|
+
font-family: inherit;
|
|
669
|
+
white-space: normal;
|
|
670
|
+
word-break: normal;
|
|
671
|
+
}
|
|
672
|
+
.perm-preview.prose pre,
|
|
673
|
+
.perm-preview.prose code { font-family: monospace; }
|
|
674
|
+
.perm-preview.prose ul { margin: 4px 0; padding-left: 20px; }
|
|
675
|
+
.perm-preview.prose li { margin: 2px 0; }
|
|
676
|
+
.perm-preview.prose strong { font-weight: 600; }
|
|
677
|
+
.perm-preview.prose h2,
|
|
678
|
+
.perm-preview.prose h3,
|
|
679
|
+
.perm-preview.prose h4 { font-size: 12px; font-weight: 600; margin: 4px 0; }
|
|
680
|
+
.perm-preview.prose table { border-collapse: collapse; margin: 4px 0; font-size: 11px; }
|
|
681
|
+
.perm-preview.prose th,
|
|
682
|
+
.perm-preview.prose td { border: 1px solid var(--border); padding: 3px 6px; text-align: left; }
|
|
626
683
|
.perm-actions {
|
|
627
684
|
display: flex;
|
|
628
685
|
gap: 10px;
|
|
@@ -940,7 +997,9 @@
|
|
|
940
997
|
<span class="preview-name" id="previewName"></span>
|
|
941
998
|
<button class="preview-cancel" id="previewCancel">×</button>
|
|
942
999
|
</div>
|
|
1000
|
+
<div class="mention-error" id="mentionError"></div>
|
|
943
1001
|
<div class="message-input-area">
|
|
1002
|
+
<div class="mention-popup" id="mentionPopup"></div>
|
|
944
1003
|
<button class="attach-btn" id="attachBtn" disabled title="Attach image">📎</button>
|
|
945
1004
|
<input type="file" id="fileInput" accept="image/*" style="display:none">
|
|
946
1005
|
<textarea id="msgInput" placeholder="Send a message to session... (Shift+Enter for new line)" disabled rows="1"></textarea>
|
|
@@ -1072,6 +1131,146 @@
|
|
|
1072
1131
|
return state.sessionMeta.names[s.id] || s.displayName || s.name;
|
|
1073
1132
|
}
|
|
1074
1133
|
|
|
1134
|
+
function normName(s) {
|
|
1135
|
+
return String(s).normalize('NFC').trim().toLowerCase();
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
function folderName(s) {
|
|
1139
|
+
return s.displayName || s.name;
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
function renameError(val, id) {
|
|
1143
|
+
if (/[\[\]@]/.test(val)) return 'Name cannot contain [, ] or @';
|
|
1144
|
+
const key = normName(val);
|
|
1145
|
+
const taken = Object.values(state.sessions).some(o => o.id !== id &&
|
|
1146
|
+
(normName(sessionDisplayName(o)) === key || normName(folderName(o)) === key));
|
|
1147
|
+
return taken ? 'Name already used by another session' : null;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
function sessionNames(s) {
|
|
1151
|
+
return [state.sessionMeta.names[s.id], folderName(s), s.peerName].filter(Boolean);
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// Built-in SendMessage only reaches sessions on the hub's own machine.
|
|
1155
|
+
function mentionTargets() {
|
|
1156
|
+
const selected = state.sessions[state.selectedSession];
|
|
1157
|
+
if (!selected || !selected.isLocal) return [];
|
|
1158
|
+
return Object.values(state.sessions).filter(s => s.id !== selected.id && s.isLocal && s.peerName);
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
function resolveMention(name) {
|
|
1162
|
+
const key = normName(name);
|
|
1163
|
+
const matches = mentionTargets().filter(s => sessionNames(s).some(n => normName(n) === key));
|
|
1164
|
+
return matches.length === 1 ? matches[0] : null;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
function mentionLabel(s) {
|
|
1168
|
+
return [sessionDisplayName(s), folderName(s), s.peerName].find(n => resolveMention(n) === s) || s.peerName;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
// A token only counts as a mention when it names (or starts with) some
|
|
1172
|
+
// session, so pasted code like @Override or npm @types/x is left alone.
|
|
1173
|
+
function looksLikeSessionMention(name) {
|
|
1174
|
+
const key = normName(name);
|
|
1175
|
+
return Object.values(state.sessions).some(s => sessionNames(s).some(n => key.startsWith(normName(n))));
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
function extractMentions(text) {
|
|
1179
|
+
const prose = text.replace(/```[\s\S]*?(```|$)/g, ' ').replace(/`[^`\n]*`/g, ' ');
|
|
1180
|
+
const re = /(^|\s)@(?:\[([^\]\n]+)\]|([^\s\[\]]+))/g;
|
|
1181
|
+
const found = [];
|
|
1182
|
+
let m;
|
|
1183
|
+
while ((m = re.exec(prose))) {
|
|
1184
|
+
const bracketed = m[2] !== undefined;
|
|
1185
|
+
const name = bracketed ? m[2] : m[3].replace(/[.,!?;:)]+$/, '');
|
|
1186
|
+
found.push({ raw: m[0].trim(), name, bracketed });
|
|
1187
|
+
}
|
|
1188
|
+
return found;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// The routing line is the contract with the channel instructions in
|
|
1192
|
+
// src/channel/server.ts (ROUTING) — change both together.
|
|
1193
|
+
function buildRouting(text) {
|
|
1194
|
+
const mentions = extractMentions(text);
|
|
1195
|
+
if (!mentions.length) return { ok: true, content: text };
|
|
1196
|
+
const unknown = [];
|
|
1197
|
+
const lines = [];
|
|
1198
|
+
const seen = new Set();
|
|
1199
|
+
for (const m of mentions) {
|
|
1200
|
+
const s = resolveMention(m.name);
|
|
1201
|
+
if (!s) {
|
|
1202
|
+
if (m.bracketed || looksLikeSessionMention(m.name)) unknown.push(m.raw);
|
|
1203
|
+
continue;
|
|
1204
|
+
}
|
|
1205
|
+
if (seen.has(s.id)) continue;
|
|
1206
|
+
seen.add(s.id);
|
|
1207
|
+
lines.push(`[claude-alarm] @${m.name} = SendMessage to "${s.peerName}"`);
|
|
1208
|
+
}
|
|
1209
|
+
if (unknown.length) return { ok: false, unknown };
|
|
1210
|
+
if (!lines.length) return { ok: true, content: text };
|
|
1211
|
+
return { ok: true, content: `${text}\n\n${lines.join('\n')}` };
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
function showMentionError(msg) {
|
|
1215
|
+
const el = $('#mentionError');
|
|
1216
|
+
el.textContent = msg || '';
|
|
1217
|
+
el.classList.toggle('show', !!msg);
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
let mentionState = { open: false, items: [], index: 0, start: -1 };
|
|
1221
|
+
|
|
1222
|
+
function currentMentionQuery(input) {
|
|
1223
|
+
const upto = input.value.slice(0, input.selectionStart);
|
|
1224
|
+
const m = upto.match(/(^|\s)@(\[[^\]\n]*|[^\s\[\]]*)$/);
|
|
1225
|
+
if (!m) return null;
|
|
1226
|
+
return { start: upto.length - m[2].length - 1, query: m[2].replace(/^\[/, '') };
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
function closeMentionPopup() {
|
|
1230
|
+
mentionState = { open: false, items: [], index: 0, start: -1 };
|
|
1231
|
+
$('#mentionPopup').classList.remove('open');
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
function renderMentionPopup() {
|
|
1235
|
+
$('#mentionPopup').innerHTML = mentionState.items.map((s, i) =>
|
|
1236
|
+
`<div class="mention-item${i === mentionState.index ? ' active' : ''}" data-idx="${i}">` +
|
|
1237
|
+
`<span>${esc(sessionDisplayName(s))}</span><span class="mention-folder">${esc(folderName(s))}</span></div>`
|
|
1238
|
+
).join('');
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
function updateMentionPopup() {
|
|
1242
|
+
const q = currentMentionQuery($('#msgInput'));
|
|
1243
|
+
if (!q) return closeMentionPopup();
|
|
1244
|
+
const key = normName(q.query);
|
|
1245
|
+
const items = mentionTargets().filter(s =>
|
|
1246
|
+
normName(sessionDisplayName(s)).includes(key) || normName(folderName(s)).includes(key));
|
|
1247
|
+
if (!items.length) return closeMentionPopup();
|
|
1248
|
+
const index = mentionState.open ? Math.min(mentionState.index, items.length - 1) : 0;
|
|
1249
|
+
mentionState = { open: true, items, index, start: q.start };
|
|
1250
|
+
renderMentionPopup();
|
|
1251
|
+
$('#mentionPopup').classList.add('open');
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
function applyMention(s) {
|
|
1255
|
+
const input = $('#msgInput');
|
|
1256
|
+
const label = mentionLabel(s);
|
|
1257
|
+
const token = /\s/.test(label) ? `@[${label}] ` : `@${label} `;
|
|
1258
|
+
const before = input.value.slice(0, mentionState.start);
|
|
1259
|
+
const after = input.value.slice(input.selectionStart);
|
|
1260
|
+
input.value = before + token + after;
|
|
1261
|
+
const pos = before.length + token.length;
|
|
1262
|
+
input.setSelectionRange(pos, pos);
|
|
1263
|
+
closeMentionPopup();
|
|
1264
|
+
input.focus();
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
$('#mentionPopup').addEventListener('mousedown', (e) => {
|
|
1268
|
+
const item = e.target.closest('.mention-item');
|
|
1269
|
+
if (!item) return;
|
|
1270
|
+
e.preventDefault();
|
|
1271
|
+
applyMention(mentionState.items[Number(item.dataset.idx)]);
|
|
1272
|
+
});
|
|
1273
|
+
|
|
1075
1274
|
// --- Theme ---
|
|
1076
1275
|
function initTheme() {
|
|
1077
1276
|
const saved = localStorage.getItem('claude-alarm-theme') || 'dark';
|
|
@@ -1284,12 +1483,18 @@
|
|
|
1284
1483
|
input.select();
|
|
1285
1484
|
|
|
1286
1485
|
let done = false;
|
|
1287
|
-
const commit = (save) => {
|
|
1486
|
+
const commit = (save, fromBlur) => {
|
|
1288
1487
|
if (done) return;
|
|
1488
|
+
const val = input.value.trim();
|
|
1489
|
+
const baseName = s.displayName || s.name;
|
|
1490
|
+
const err = save && val && val !== baseName ? renameError(val, id) : null;
|
|
1491
|
+
if (err && !fromBlur) {
|
|
1492
|
+
input.classList.add('invalid');
|
|
1493
|
+
input.title = err;
|
|
1494
|
+
return;
|
|
1495
|
+
}
|
|
1289
1496
|
done = true;
|
|
1290
|
-
if (save) {
|
|
1291
|
-
const val = input.value.trim();
|
|
1292
|
-
const baseName = s.displayName || s.name;
|
|
1497
|
+
if (save && !err) {
|
|
1293
1498
|
if (!val || val === baseName) delete state.sessionMeta.names[id];
|
|
1294
1499
|
else state.sessionMeta.names[id] = val;
|
|
1295
1500
|
saveSessionMeta();
|
|
@@ -1298,10 +1503,11 @@
|
|
|
1298
1503
|
};
|
|
1299
1504
|
input.addEventListener('keydown', (e) => {
|
|
1300
1505
|
e.stopPropagation();
|
|
1301
|
-
if (e.key === 'Enter') { e.preventDefault(); commit(true); }
|
|
1302
|
-
else if (e.key === 'Escape') { e.preventDefault(); commit(false); }
|
|
1506
|
+
if (e.key === 'Enter') { e.preventDefault(); commit(true, false); }
|
|
1507
|
+
else if (e.key === 'Escape') { e.preventDefault(); commit(false, false); }
|
|
1303
1508
|
});
|
|
1304
|
-
input.addEventListener('
|
|
1509
|
+
input.addEventListener('input', () => { input.classList.remove('invalid'); input.title = ''; });
|
|
1510
|
+
input.addEventListener('blur', () => commit(true, true));
|
|
1305
1511
|
input.addEventListener('click', (e) => e.stopPropagation());
|
|
1306
1512
|
}
|
|
1307
1513
|
|
|
@@ -1554,6 +1760,13 @@
|
|
|
1554
1760
|
if (!content && !hasImage) return;
|
|
1555
1761
|
if (!state.selectedSession || !state.ws) return;
|
|
1556
1762
|
|
|
1763
|
+
const routing = buildRouting(content);
|
|
1764
|
+
if (!routing.ok) {
|
|
1765
|
+
showMentionError(`Unknown session: ${routing.unknown.join(', ')} — pick from the @ list, or put a space right after the name.`);
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
showMentionError(null);
|
|
1769
|
+
|
|
1557
1770
|
if (hasImage) {
|
|
1558
1771
|
// Send image + text together
|
|
1559
1772
|
state.ws.send(JSON.stringify({
|
|
@@ -1562,11 +1775,11 @@
|
|
|
1562
1775
|
imageData: state.pendingImage.data,
|
|
1563
1776
|
mimeType: state.pendingImage.mimeType,
|
|
1564
1777
|
originalName: state.pendingImage.name,
|
|
1565
|
-
content: content || '',
|
|
1778
|
+
content: routing.content || '',
|
|
1566
1779
|
}));
|
|
1567
1780
|
} else if (content) {
|
|
1568
1781
|
// Text only
|
|
1569
|
-
state.ws.send(JSON.stringify({ type: 'message_to_session', sessionId: state.selectedSession, content }));
|
|
1782
|
+
state.ws.send(JSON.stringify({ type: 'message_to_session', sessionId: state.selectedSession, content: routing.content }));
|
|
1570
1783
|
}
|
|
1571
1784
|
|
|
1572
1785
|
// Add to local messages
|
|
@@ -1581,6 +1794,7 @@
|
|
|
1581
1794
|
saveMessages();
|
|
1582
1795
|
|
|
1583
1796
|
input.value = '';
|
|
1797
|
+
closeMentionPopup();
|
|
1584
1798
|
input.style.height = 'auto';
|
|
1585
1799
|
clearPendingImage();
|
|
1586
1800
|
state.waitingReply[state.selectedSession] = true;
|
|
@@ -1589,11 +1803,24 @@
|
|
|
1589
1803
|
|
|
1590
1804
|
$('#sendBtn').addEventListener('click', sendMessage);
|
|
1591
1805
|
$('#msgInput').addEventListener('keydown', (e) => {
|
|
1806
|
+
if (mentionState.open) {
|
|
1807
|
+
// Enter while an IME (e.g. Korean) is composing only commits the syllable.
|
|
1808
|
+
if (e.isComposing) return;
|
|
1809
|
+
const n = mentionState.items.length;
|
|
1810
|
+
if (e.key === 'ArrowDown') { e.preventDefault(); mentionState.index = (mentionState.index + 1) % n; renderMentionPopup(); return; }
|
|
1811
|
+
if (e.key === 'ArrowUp') { e.preventDefault(); mentionState.index = (mentionState.index - 1 + n) % n; renderMentionPopup(); return; }
|
|
1812
|
+
if (e.key === 'Enter' || e.key === 'Tab') { e.preventDefault(); applyMention(mentionState.items[mentionState.index]); return; }
|
|
1813
|
+
if (e.key === 'Escape') { e.preventDefault(); closeMentionPopup(); return; }
|
|
1814
|
+
}
|
|
1592
1815
|
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); }
|
|
1593
1816
|
});
|
|
1817
|
+
$('#msgInput').addEventListener('blur', closeMentionPopup);
|
|
1818
|
+
$('#msgInput').addEventListener('click', updateMentionPopup);
|
|
1594
1819
|
$('#msgInput').addEventListener('input', function() {
|
|
1820
|
+
showMentionError(null);
|
|
1595
1821
|
this.style.height = 'auto';
|
|
1596
1822
|
this.style.height = Math.min(this.scrollHeight, 120) + 'px';
|
|
1823
|
+
updateMentionPopup();
|
|
1597
1824
|
});
|
|
1598
1825
|
|
|
1599
1826
|
function relativeTime(ts) {
|
|
@@ -1670,9 +1897,26 @@
|
|
|
1670
1897
|
return toolName;
|
|
1671
1898
|
}
|
|
1672
1899
|
|
|
1673
|
-
|
|
1900
|
+
const LANG_BY_EXT = {
|
|
1901
|
+
js: 'javascript', mjs: 'javascript', cjs: 'javascript', jsx: 'javascript',
|
|
1902
|
+
ts: 'typescript', tsx: 'typescript', py: 'python', rb: 'ruby', go: 'go',
|
|
1903
|
+
java: 'java', kt: 'kotlin', rs: 'rust', php: 'php', cs: 'csharp',
|
|
1904
|
+
sh: 'bash', bash: 'bash', zsh: 'bash', ps1: 'powershell',
|
|
1905
|
+
json: 'json', yml: 'yaml', yaml: 'yaml', xml: 'xml', sql: 'sql',
|
|
1906
|
+
md: 'markdown', html: 'html', vue: 'html', css: 'css', scss: 'scss',
|
|
1907
|
+
};
|
|
1908
|
+
function langOf(path) {
|
|
1909
|
+
const m = /\.([a-z0-9]+)$/i.exec(path || '');
|
|
1910
|
+
return m ? (LANG_BY_EXT[m[1].toLowerCase()] || '') : '';
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
// Returns { text, kind, lang } — dashboard shows full content, no truncation.
|
|
1914
|
+
// kind 'code' must stay byte-exact: markdown rendering would swallow the ** and `
|
|
1915
|
+
// characters that the approver needs to see to judge what actually gets executed.
|
|
1916
|
+
// Only notify-style messages (prose Claude wrote) are safe to render as markdown.
|
|
1674
1917
|
function formatPermPreview(toolName, raw) {
|
|
1675
|
-
if (!raw) return { text: '' };
|
|
1918
|
+
if (!raw) return { text: '', kind: 'code', lang: '' };
|
|
1919
|
+
const isNotify = toolName === 'notify' || toolName.endsWith('__notify');
|
|
1676
1920
|
let p = null;
|
|
1677
1921
|
try { p = JSON.parse(raw); } catch {
|
|
1678
1922
|
// Truncated JSON - extract known fields with regex (closing quote optional for truncated strings)
|
|
@@ -1682,43 +1926,52 @@
|
|
|
1682
1926
|
const contentMatch = raw.match(/"content"\s*:\s*"((?:[^"\\]|\\.)*)"?/);
|
|
1683
1927
|
const titleMatch = raw.match(/"title"\s*:\s*"((?:[^"\\]|\\.)*)"?/);
|
|
1684
1928
|
const msgMatch = raw.match(/"message"\s*:\s*"((?:[^"\\]|\\.)*)"?/);
|
|
1685
|
-
if (titleMatch && msgMatch) return { text: msgMatch[1] };
|
|
1686
|
-
if (cmdMatch) return { text: '$ ' + cmdMatch[1] };
|
|
1687
|
-
if (fileMatch) return { text: fileMatch[1] };
|
|
1688
|
-
if (patternMatch) return { text: patternMatch[1] };
|
|
1689
|
-
if (contentMatch) return { text: contentMatch[1] };
|
|
1690
|
-
return { text: raw };
|
|
1929
|
+
if (titleMatch && msgMatch) return { text: msgMatch[1], kind: 'prose', lang: '' };
|
|
1930
|
+
if (cmdMatch) return { text: '$ ' + cmdMatch[1], kind: 'code', lang: 'bash' };
|
|
1931
|
+
if (fileMatch) return { text: fileMatch[1], kind: 'code', lang: langOf(fileMatch[1]) };
|
|
1932
|
+
if (patternMatch) return { text: patternMatch[1], kind: 'code', lang: '' };
|
|
1933
|
+
if (contentMatch) return { text: contentMatch[1], kind: 'code', lang: '' };
|
|
1934
|
+
return { text: raw, kind: 'code', lang: '' };
|
|
1691
1935
|
}
|
|
1692
1936
|
let text;
|
|
1937
|
+
let kind = 'code';
|
|
1938
|
+
let lang = '';
|
|
1693
1939
|
switch (toolName) {
|
|
1694
1940
|
case 'Bash':
|
|
1695
|
-
text = p.command ? '$ ' + p.command : raw; break;
|
|
1941
|
+
text = p.command ? '$ ' + p.command : raw; lang = 'bash'; break;
|
|
1696
1942
|
case 'Write':
|
|
1697
1943
|
case 'Edit':
|
|
1698
1944
|
if (p.file_path) {
|
|
1699
1945
|
text = p.file_path;
|
|
1946
|
+
lang = langOf(p.file_path);
|
|
1700
1947
|
if (p.new_string) text += '\n+ ' + p.new_string;
|
|
1701
1948
|
else if (p.content) text += '\n' + p.content;
|
|
1702
1949
|
} else { text = raw; }
|
|
1703
1950
|
break;
|
|
1704
1951
|
case 'Read':
|
|
1705
|
-
text = p.file_path || raw; break;
|
|
1952
|
+
text = p.file_path || raw; lang = langOf(p.file_path); break;
|
|
1706
1953
|
case 'Glob':
|
|
1707
1954
|
text = p.pattern || raw; break;
|
|
1708
1955
|
case 'Grep':
|
|
1709
1956
|
text = (p.pattern || '') + (p.path ? ' in ' + p.path : ''); break;
|
|
1710
1957
|
default:
|
|
1711
1958
|
if (p.title && p.message) {
|
|
1712
|
-
text = p.message;
|
|
1959
|
+
text = p.message; kind = 'prose';
|
|
1713
1960
|
} else if (p.content && typeof p.content === 'string') {
|
|
1714
|
-
text = p.content;
|
|
1961
|
+
text = p.content; kind = isNotify ? 'prose' : 'code';
|
|
1715
1962
|
} else {
|
|
1716
1963
|
const entries = Object.entries(p);
|
|
1717
1964
|
if (entries.length <= 3) text = entries.map(([k, v]) => k + ': ' + String(v)).join('\n');
|
|
1718
1965
|
else text = JSON.stringify(p, null, 1);
|
|
1719
1966
|
}
|
|
1720
1967
|
}
|
|
1721
|
-
return { text: text || '' };
|
|
1968
|
+
return { text: text || '', kind, lang };
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
function renderPermPreview(text, kind, lang) {
|
|
1972
|
+
if (kind === 'prose') return `<div class="perm-preview prose">${renderMarkdown(text)}</div>`;
|
|
1973
|
+
const cls = lang ? ` class="lang-${esc(lang)}"` : '';
|
|
1974
|
+
return `<div class="perm-preview"><pre><code${cls}>${esc(text)}</code></pre></div>`;
|
|
1722
1975
|
}
|
|
1723
1976
|
|
|
1724
1977
|
function sendPermissionVerdict(sessionId, requestId, behavior) {
|
|
@@ -1742,7 +1995,7 @@
|
|
|
1742
1995
|
if (!reqs.length) { bar.classList.remove('active'); bar.innerHTML = ''; return; }
|
|
1743
1996
|
bar.classList.add('active');
|
|
1744
1997
|
bar.innerHTML = reqs.map(r => {
|
|
1745
|
-
const { text: previewText } = formatPermPreview(r.toolName, r.inputPreview);
|
|
1998
|
+
const { text: previewText, kind: previewKind, lang: previewLang } = formatPermPreview(r.toolName, r.inputPreview);
|
|
1746
1999
|
const displayName = formatToolName(r.toolName, r.inputPreview);
|
|
1747
2000
|
return `<div class="perm-item">
|
|
1748
2001
|
<div class="perm-icon">⚠</div>
|
|
@@ -1750,7 +2003,7 @@
|
|
|
1750
2003
|
<div class="perm-header">
|
|
1751
2004
|
<span class="perm-tool">${esc(displayName)}</span>
|
|
1752
2005
|
</div>
|
|
1753
|
-
${previewText ?
|
|
2006
|
+
${previewText ? renderPermPreview(previewText, previewKind, previewLang) : ''}
|
|
1754
2007
|
</div>
|
|
1755
2008
|
<div class="perm-actions">
|
|
1756
2009
|
<button class="perm-allow" data-req-id="${esc(r.requestId)}" data-session-id="${esc(sid)}">Allow <span class="perm-kbd">(Enter)</span></button>
|
package/dist/hub/server.js
CHANGED
|
@@ -41,6 +41,12 @@ var UPLOADS_DIR = path.join(CONFIG_DIR, "uploads");
|
|
|
41
41
|
var WS_PATH_CHANNEL = "/ws/channel";
|
|
42
42
|
var WS_PATH_DASHBOARD = "/ws/dashboard";
|
|
43
43
|
|
|
44
|
+
// src/shared/peer-name.ts
|
|
45
|
+
var SAFE_PEER_NAME = /^[^\u0000-\u001f\u007f"\\[\]]{1,128}$/;
|
|
46
|
+
function isValidPeerName(name) {
|
|
47
|
+
return typeof name === "string" && name.trim() !== "" && SAFE_PEER_NAME.test(name);
|
|
48
|
+
}
|
|
49
|
+
|
|
44
50
|
// src/hub/session-manager.ts
|
|
45
51
|
var SessionManager = class {
|
|
46
52
|
sessions = /* @__PURE__ */ new Map();
|
|
@@ -64,7 +70,7 @@ var SessionManager = class {
|
|
|
64
70
|
} else {
|
|
65
71
|
session.displayName = baseName;
|
|
66
72
|
}
|
|
67
|
-
this.sessions.set(session.id, { ...session });
|
|
73
|
+
this.sessions.set(session.id, { ...session, peerName: isValidPeerName(session.peerName) ? session.peerName : void 0 });
|
|
68
74
|
}
|
|
69
75
|
unregister(sessionId) {
|
|
70
76
|
const session = this.sessions.get(sessionId);
|
|
@@ -79,6 +85,13 @@ var SessionManager = class {
|
|
|
79
85
|
}
|
|
80
86
|
return session;
|
|
81
87
|
}
|
|
88
|
+
setPeerName(sessionId, peerName) {
|
|
89
|
+
const session = this.sessions.get(sessionId);
|
|
90
|
+
if (session) {
|
|
91
|
+
session.peerName = isValidPeerName(peerName) ? peerName : void 0;
|
|
92
|
+
}
|
|
93
|
+
return session;
|
|
94
|
+
}
|
|
82
95
|
updateActivity(sessionId) {
|
|
83
96
|
const session = this.sessions.get(sessionId);
|
|
84
97
|
if (session) {
|
|
@@ -210,7 +223,7 @@ import fs from "fs";
|
|
|
210
223
|
import path2 from "path";
|
|
211
224
|
import { randomUUID } from "crypto";
|
|
212
225
|
var TELEGRAM_API = "https://api.telegram.org/bot";
|
|
213
|
-
var TelegramBot = class {
|
|
226
|
+
var TelegramBot = class _TelegramBot {
|
|
214
227
|
config;
|
|
215
228
|
offset = 0;
|
|
216
229
|
polling = false;
|
|
@@ -442,33 +455,45 @@ ${this.mdToHtml(message)}`;
|
|
|
442
455
|
}
|
|
443
456
|
/** Send a permission request with inline buttons */
|
|
444
457
|
async sendPermissionRequest(sessionId, sessionLabel, requestId, toolName, description, inputPreview) {
|
|
458
|
+
const isNotify = toolName.endsWith("__notify") || toolName === "notify";
|
|
445
459
|
let preview = inputPreview;
|
|
460
|
+
let kind = "code";
|
|
461
|
+
let lang = "";
|
|
446
462
|
let truncated = false;
|
|
447
463
|
try {
|
|
448
464
|
const p = JSON.parse(inputPreview);
|
|
449
|
-
if (p.command)
|
|
450
|
-
|
|
451
|
-
|
|
465
|
+
if (p.command) {
|
|
466
|
+
preview = `$ ${p.command}`;
|
|
467
|
+
lang = "bash";
|
|
468
|
+
} else if (p.title && p.message) {
|
|
469
|
+
preview = p.message;
|
|
470
|
+
kind = "prose";
|
|
471
|
+
} else if (p.file_path) {
|
|
452
472
|
preview = p.file_path;
|
|
473
|
+
lang = this.langOf(p.file_path);
|
|
453
474
|
if (p.content) {
|
|
454
475
|
preview += "\n" + p.content.slice(0, 3e3);
|
|
455
476
|
if (p.content.length > 500) truncated = true;
|
|
456
477
|
}
|
|
457
478
|
} else if (p.content && typeof p.content === "string") {
|
|
458
479
|
preview = p.content.slice(0, 3e3);
|
|
480
|
+
if (isNotify) kind = "prose";
|
|
459
481
|
if (p.content.length > 500) truncated = true;
|
|
460
482
|
}
|
|
461
483
|
} catch {
|
|
462
484
|
truncated = true;
|
|
463
485
|
const cmdMatch = inputPreview.match(/"command"\s*:\s*"((?:[^"\\]|\\.)*)"/);
|
|
464
486
|
const contentMatch = inputPreview.match(/"content"\s*:\s*"((?:[^"\\]|\\.)*)"/);
|
|
465
|
-
if (cmdMatch)
|
|
466
|
-
|
|
487
|
+
if (cmdMatch) {
|
|
488
|
+
preview = `$ ${cmdMatch[1]}`;
|
|
489
|
+
lang = "bash";
|
|
490
|
+
} else if (contentMatch) preview = contentMatch[1].slice(0, 3e3);
|
|
467
491
|
}
|
|
468
492
|
const truncNote = truncated ? "\n\n<i>...truncated</i>" : "";
|
|
469
493
|
const previewSlice = preview.slice(0, 3e3);
|
|
470
494
|
const isShort = !previewSlice.includes("\n") && previewSlice.length < 100;
|
|
471
|
-
const
|
|
495
|
+
const langAttr = lang ? ` class="language-${lang}"` : "";
|
|
496
|
+
const previewHtml = kind === "prose" ? this.mdToHtml(previewSlice) : isShort ? `<code>${this.escHtml(previewSlice)}</code>` : `<pre><code${langAttr}>${this.escHtml(previewSlice)}</code></pre>`;
|
|
472
497
|
let displayTool = toolName;
|
|
473
498
|
if ((toolName.endsWith("__notify") || toolName === "notify") && inputPreview) {
|
|
474
499
|
try {
|
|
@@ -568,6 +593,40 @@ ${label}`);
|
|
|
568
593
|
escHtml(s) {
|
|
569
594
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
570
595
|
}
|
|
596
|
+
static LANG_BY_EXT = {
|
|
597
|
+
js: "javascript",
|
|
598
|
+
mjs: "javascript",
|
|
599
|
+
cjs: "javascript",
|
|
600
|
+
jsx: "javascript",
|
|
601
|
+
ts: "typescript",
|
|
602
|
+
tsx: "typescript",
|
|
603
|
+
py: "python",
|
|
604
|
+
rb: "ruby",
|
|
605
|
+
go: "go",
|
|
606
|
+
java: "java",
|
|
607
|
+
kt: "kotlin",
|
|
608
|
+
rs: "rust",
|
|
609
|
+
php: "php",
|
|
610
|
+
cs: "csharp",
|
|
611
|
+
sh: "bash",
|
|
612
|
+
bash: "bash",
|
|
613
|
+
zsh: "bash",
|
|
614
|
+
ps1: "powershell",
|
|
615
|
+
json: "json",
|
|
616
|
+
yml: "yaml",
|
|
617
|
+
yaml: "yaml",
|
|
618
|
+
xml: "xml",
|
|
619
|
+
sql: "sql",
|
|
620
|
+
md: "markdown",
|
|
621
|
+
html: "html",
|
|
622
|
+
vue: "html",
|
|
623
|
+
css: "css",
|
|
624
|
+
scss: "scss"
|
|
625
|
+
};
|
|
626
|
+
langOf(path5) {
|
|
627
|
+
const m = /\.([a-z0-9]+)$/i.exec(path5 || "");
|
|
628
|
+
return m ? _TelegramBot.LANG_BY_EXT[m[1].toLowerCase()] ?? "" : "";
|
|
629
|
+
}
|
|
571
630
|
/** Convert markdown to Telegram HTML (escape first, then apply formatting) */
|
|
572
631
|
mdToHtml(s) {
|
|
573
632
|
let text = s.replace(
|
|
@@ -995,7 +1054,7 @@ var HubServer = class {
|
|
|
995
1054
|
this.sessions.register(session);
|
|
996
1055
|
this.channelSockets.set(session.id, ws);
|
|
997
1056
|
if (isLocal) this.localChannels.add(session.id);
|
|
998
|
-
logger.info(`Session registered: ${session.id} (${session.name}, channel: ${session.channelEnabled ?? false})`);
|
|
1057
|
+
logger.info(`Session registered: ${session.id} (${session.name}, channel: ${session.channelEnabled ?? false}, peer: ${session.peerName ?? "-"})`);
|
|
999
1058
|
this.broadcastToDashboards({
|
|
1000
1059
|
type: isReregister ? "session_updated" : "session_connected",
|
|
1001
1060
|
session
|
|
@@ -1009,6 +1068,15 @@ var HubServer = class {
|
|
|
1009
1068
|
}
|
|
1010
1069
|
break;
|
|
1011
1070
|
}
|
|
1071
|
+
case "peer_name": {
|
|
1072
|
+
if (this.channelSockets.get(msg.sessionId) !== ws) break;
|
|
1073
|
+
const updated = this.sessions.setPeerName(msg.sessionId, msg.peerName);
|
|
1074
|
+
if (updated) {
|
|
1075
|
+
logger.info(`Peer name for ${msg.sessionId}: ${msg.peerName ?? "-"}`);
|
|
1076
|
+
this.broadcastToDashboards({ type: "session_updated", session: updated });
|
|
1077
|
+
}
|
|
1078
|
+
break;
|
|
1079
|
+
}
|
|
1012
1080
|
case "notify": {
|
|
1013
1081
|
this.sessions.updateActivity(msg.sessionId);
|
|
1014
1082
|
const notifySession = this.sessions.get(msg.sessionId);
|