@delt/claude-alarm 1.1.0 → 1.2.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 +17 -10
- package/dist/channel/server.js.map +1 -1
- package/dist/cli.js +906 -67
- package/dist/cli.js.map +1 -1
- package/dist/codex/main.js +380 -63
- package/dist/codex/main.js.map +1 -1
- package/dist/dashboard/index.html +179 -2
- package/dist/hub/server.js +165 -8
- package/dist/hub/server.js.map +1 -1
- package/dist/index.d.ts +60 -2
- package/dist/index.js +166 -8
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/dashboard/index.html +179 -2
|
@@ -143,6 +143,19 @@
|
|
|
143
143
|
color: var(--text-dim);
|
|
144
144
|
margin-bottom: 8px;
|
|
145
145
|
}
|
|
146
|
+
[hidden] { display: none !important; }
|
|
147
|
+
.codex-section { margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--border); display: grid; gap: 8px; }
|
|
148
|
+
.codex-section .cmd-popup-title { margin-bottom: 0; color: var(--text); font-weight: 600; }
|
|
149
|
+
#codexAdapter, #codexFolder { box-sizing: border-box; width: 100%; min-width: 0; min-height: 36px; padding: 8px; border: 1px solid var(--border); border-radius: 6px; background: var(--surface); color: var(--text); font: inherit; font-size: 12px; }
|
|
150
|
+
#codexFolder::placeholder { color: var(--text-dim); }
|
|
151
|
+
#codexAdapter:focus, #codexFolder:focus { outline: 1px solid var(--accent); border-color: var(--accent); }
|
|
152
|
+
.codex-warning { color: var(--text-dim); font-size: 11px; line-height: 1.5; }
|
|
153
|
+
#codexCreate { min-height: 36px; padding: 8px 12px; border: 1px solid var(--accent); border-radius: 6px; background: var(--surface); color: var(--text); font: inherit; font-size: 12px; font-weight: 600; cursor: pointer; }
|
|
154
|
+
#codexCreate:disabled { opacity: 0.6; cursor: wait; }
|
|
155
|
+
.codex-error { color: var(--red); font-size: 12px; line-height: 1.5; overflow-wrap: anywhere; }
|
|
156
|
+
.session-close { flex-shrink: 0; align-self: flex-end; min-width: 32px; min-height: 32px; padding: 4px 6px; border: 1px solid var(--border); border-radius: 6px; background: var(--surface); color: var(--text-dim); font-size: 16px; cursor: pointer; }
|
|
157
|
+
.session-close:hover, .session-close:focus-visible { border-color: var(--red); color: var(--red); }
|
|
158
|
+
.session-close.armed { border-color: var(--red); color: var(--red); font-size: 11px; font-weight: 600; }
|
|
146
159
|
.session-card {
|
|
147
160
|
background: var(--surface);
|
|
148
161
|
border: 1px solid var(--border);
|
|
@@ -589,8 +602,23 @@
|
|
|
589
602
|
background: linear-gradient(180deg, rgba(245,158,11,0.08) 0%, transparent 100%);
|
|
590
603
|
padding: 0;
|
|
591
604
|
display: none;
|
|
605
|
+
flex-shrink: 0;
|
|
606
|
+
max-height: 45vh;
|
|
607
|
+
overflow-y: auto;
|
|
608
|
+
scrollbar-width: thin;
|
|
592
609
|
}
|
|
593
610
|
.permission-bar.active { display: block; }
|
|
611
|
+
.perm-count {
|
|
612
|
+
position: sticky;
|
|
613
|
+
top: 0;
|
|
614
|
+
z-index: 1;
|
|
615
|
+
padding: 6px 20px;
|
|
616
|
+
font-size: 12px;
|
|
617
|
+
font-weight: 600;
|
|
618
|
+
color: var(--yellow);
|
|
619
|
+
background: var(--surface);
|
|
620
|
+
border-bottom: 1px solid var(--border);
|
|
621
|
+
}
|
|
594
622
|
.perm-item {
|
|
595
623
|
display: flex;
|
|
596
624
|
align-items: center;
|
|
@@ -848,6 +876,9 @@
|
|
|
848
876
|
|
|
849
877
|
/* Mobile responsive */
|
|
850
878
|
@media (max-width: 768px) {
|
|
879
|
+
.codex-section { gap: 10px; }
|
|
880
|
+
#codexAdapter, #codexFolder, #codexCreate { min-height: 40px; font-size: 13px; }
|
|
881
|
+
.session-close { min-width: 36px; min-height: 36px; }
|
|
851
882
|
.container {
|
|
852
883
|
grid-template-columns: 1fr;
|
|
853
884
|
height: calc(100vh - 97px);
|
|
@@ -1000,6 +1031,15 @@
|
|
|
1000
1031
|
<span class="cmd-text">claude --dangerously-load-development-channels server:claude-alarm --dangerously-skip-permissions</span>
|
|
1001
1032
|
<span class="cmd-icon">📋</span>
|
|
1002
1033
|
</div>
|
|
1034
|
+
<div class="codex-section" id="codexSection" hidden>
|
|
1035
|
+
<div class="cmd-popup-title">New Codex conversation:</div>
|
|
1036
|
+
<select id="codexAdapter" hidden></select>
|
|
1037
|
+
<input id="codexFolder" list="codexFolders" placeholder="Folder on that PC, e.g. C:\work\project" autocomplete="off" spellcheck="false">
|
|
1038
|
+
<datalist id="codexFolders"></datalist>
|
|
1039
|
+
<div class="codex-warning">Runs with full access: no approval prompts.</div>
|
|
1040
|
+
<button id="codexCreate" type="button">Create</button>
|
|
1041
|
+
<div class="codex-error" id="codexError" hidden></div>
|
|
1042
|
+
</div>
|
|
1003
1043
|
</div>
|
|
1004
1044
|
<div id="sessionsList"></div>
|
|
1005
1045
|
</div>
|
|
@@ -1051,6 +1091,9 @@
|
|
|
1051
1091
|
waitingReply: {},
|
|
1052
1092
|
permissionRequests: {}, // sessionId -> [{ requestId, toolName, description, inputPreview, timestamp, resolved, behavior }]
|
|
1053
1093
|
sessionMeta: { names: {}, order: [] }, // custom names + order, keyed by volatile sessionId
|
|
1094
|
+
codexAdapters: [],
|
|
1095
|
+
pendingSelect: null,
|
|
1096
|
+
closeArmed: null,
|
|
1054
1097
|
};
|
|
1055
1098
|
|
|
1056
1099
|
const $ = (sel) => document.querySelector(sel);
|
|
@@ -1375,11 +1418,13 @@
|
|
|
1375
1418
|
msg.sessions.forEach(s => { state.sessions[s.id] = s; });
|
|
1376
1419
|
pruneSessionData();
|
|
1377
1420
|
renderSessions();
|
|
1421
|
+
if (state.pendingSelect && state.sessions[state.pendingSelect]) selectSession(state.pendingSelect);
|
|
1378
1422
|
break;
|
|
1379
1423
|
case 'session_connected':
|
|
1380
1424
|
state.sessions[msg.session.id] = msg.session;
|
|
1381
1425
|
if (!state.messages[msg.session.id]) state.messages[msg.session.id] = [];
|
|
1382
1426
|
renderSessions();
|
|
1427
|
+
if (state.pendingSelect === msg.session.id) selectSession(msg.session.id);
|
|
1383
1428
|
break;
|
|
1384
1429
|
case 'session_disconnected':
|
|
1385
1430
|
delete state.sessions[msg.sessionId];
|
|
@@ -1448,6 +1493,15 @@
|
|
|
1448
1493
|
renderPermissionBar();
|
|
1449
1494
|
renderNotifications();
|
|
1450
1495
|
break;
|
|
1496
|
+
case 'codex_adapters': {
|
|
1497
|
+
state.codexAdapters = msg.adapters || [];
|
|
1498
|
+
if ($('#cmdPopup').classList.contains('active')) {
|
|
1499
|
+
const before = $('#codexAdapter').value;
|
|
1500
|
+
renderCodexForm();
|
|
1501
|
+
if ($('#codexAdapter').value !== before) loadCodexFolders();
|
|
1502
|
+
}
|
|
1503
|
+
break;
|
|
1504
|
+
}
|
|
1451
1505
|
case 'permission_resolved': {
|
|
1452
1506
|
const req = (state.permissionRequests[msg.sessionId] || []).find(r => r.requestId === msg.requestId);
|
|
1453
1507
|
if (req && !req.resolved) {
|
|
@@ -1476,6 +1530,10 @@
|
|
|
1476
1530
|
const unread = state.unread[id] || 0;
|
|
1477
1531
|
const dispName = sessionDisplayName(s);
|
|
1478
1532
|
const connTime = new Date(s.connectedAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
1533
|
+
const armed = state.closeArmed && state.closeArmed.id === id && closeStep(state.closeArmed.at, Date.now()) !== 'arm';
|
|
1534
|
+
const closeBtn = s.closable
|
|
1535
|
+
? `<button class="session-close${armed ? ' armed' : ''}" type="button" title="Close — stays in Codex history">${armed ? 'Close?' : '×'}</button>`
|
|
1536
|
+
: '';
|
|
1479
1537
|
return `<div class="session-card${active}" data-id="${id}">
|
|
1480
1538
|
<span class="drag-handle" title="Drag to reorder">⠿</span>
|
|
1481
1539
|
<div class="session-body">
|
|
@@ -1483,6 +1541,7 @@
|
|
|
1483
1541
|
${cwdDisplay ? `<div class="session-cwd" title="${esc(s.cwd)}">${esc(cwdDisplay)} · ${connTime}</div>` : `<div class="session-cwd">${connTime}</div>`}
|
|
1484
1542
|
<span class="session-status ${s.status}">${s.status.replace('_', ' ')}</span>
|
|
1485
1543
|
</div>
|
|
1544
|
+
${closeBtn}
|
|
1486
1545
|
</div>`;
|
|
1487
1546
|
}).join('');
|
|
1488
1547
|
bindSessionCards(el);
|
|
@@ -1493,10 +1552,13 @@
|
|
|
1493
1552
|
el.querySelectorAll('.session-card').forEach(card => {
|
|
1494
1553
|
const id = card.dataset.id;
|
|
1495
1554
|
card.addEventListener('click', (e) => {
|
|
1555
|
+
if (e.target.closest('.session-close')) return;
|
|
1496
1556
|
if (e.target.closest('.drag-handle')) return;
|
|
1497
1557
|
if (e.target.classList.contains('name-edit-input')) return;
|
|
1498
1558
|
selectSession(id);
|
|
1499
1559
|
});
|
|
1560
|
+
const closeBtn = card.querySelector('.session-close');
|
|
1561
|
+
if (closeBtn) closeBtn.addEventListener('click', (e) => { e.stopPropagation(); onCloseClick(id); });
|
|
1500
1562
|
const nameEl = card.querySelector('.session-name');
|
|
1501
1563
|
nameEl.addEventListener('dblclick', (e) => {
|
|
1502
1564
|
e.stopPropagation();
|
|
@@ -1606,6 +1668,7 @@
|
|
|
1606
1668
|
}
|
|
1607
1669
|
|
|
1608
1670
|
function selectSession(id) {
|
|
1671
|
+
state.pendingSelect = null;
|
|
1609
1672
|
state.selectedSession = id;
|
|
1610
1673
|
state.unread[id] = 0;
|
|
1611
1674
|
if (!state.messages[id]) state.messages[id] = [];
|
|
@@ -1626,7 +1689,7 @@
|
|
|
1626
1689
|
|
|
1627
1690
|
function updateImageUI() {
|
|
1628
1691
|
const s = state.selectedSession ? state.sessions[state.selectedSession] : null;
|
|
1629
|
-
const canImage = s && s.isLocal
|
|
1692
|
+
const canImage = s && s.isLocal;
|
|
1630
1693
|
$('#attachBtn').disabled = !canImage;
|
|
1631
1694
|
}
|
|
1632
1695
|
|
|
@@ -2085,7 +2148,8 @@
|
|
|
2085
2148
|
const reqs = (state.permissionRequests[sid] || []).filter(r => !r.resolved);
|
|
2086
2149
|
if (!reqs.length) { bar.classList.remove('active'); bar.innerHTML = ''; return; }
|
|
2087
2150
|
bar.classList.add('active');
|
|
2088
|
-
|
|
2151
|
+
const count = reqs.length > 1 ? `<div class="perm-count">${reqs.length} permission requests waiting</div>` : '';
|
|
2152
|
+
bar.innerHTML = count + reqs.map(r => {
|
|
2089
2153
|
const { text: previewText, kind: previewKind, lang: previewLang } = formatPermPreview(r.toolName, r.inputPreview);
|
|
2090
2154
|
const displayName = formatToolName(r.toolName, r.inputPreview);
|
|
2091
2155
|
return `<div class="perm-item">
|
|
@@ -2169,10 +2233,32 @@
|
|
|
2169
2233
|
msgList.scrollTo({ top: msgList.scrollHeight, behavior: 'smooth' });
|
|
2170
2234
|
});
|
|
2171
2235
|
|
|
2236
|
+
// --- Codex helpers ---
|
|
2237
|
+
function usableAdapters(adapters) {
|
|
2238
|
+
return adapters.filter(a => a.ready).sort((a, b) => (b.isLocal ? 1 : 0) - (a.isLocal ? 1 : 0));
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
function adapterLabels(adapters) {
|
|
2242
|
+
const counts = {};
|
|
2243
|
+
adapters.forEach(a => { counts[a.host] = (counts[a.host] || 0) + 1; });
|
|
2244
|
+
return adapters.map(a => counts[a.host] > 1 ? `${a.host} (${a.id.slice(0, 4)})` : a.host);
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
// A second tap within 0.4 s is a phone double-tap, not a confirmation.
|
|
2248
|
+
function closeStep(armedAt, now) {
|
|
2249
|
+
if (armedAt === null || now - armedAt >= 3000) return 'arm';
|
|
2250
|
+
return now - armedAt < 400 ? 'wait' : 'close';
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2172
2253
|
// --- Add session popup ---
|
|
2173
2254
|
$('#addSessionBtn').addEventListener('click', (e) => {
|
|
2174
2255
|
e.stopPropagation();
|
|
2256
|
+
const opening = !$('#cmdPopup').classList.contains('active');
|
|
2175
2257
|
$('#cmdPopup').classList.toggle('active');
|
|
2258
|
+
if (opening) {
|
|
2259
|
+
renderCodexForm();
|
|
2260
|
+
loadCodexFolders();
|
|
2261
|
+
}
|
|
2176
2262
|
});
|
|
2177
2263
|
document.addEventListener('click', (e) => {
|
|
2178
2264
|
if (!e.target.closest('#cmdPopup') && !e.target.closest('#addSessionBtn')) {
|
|
@@ -2193,6 +2279,97 @@
|
|
|
2193
2279
|
});
|
|
2194
2280
|
});
|
|
2195
2281
|
|
|
2282
|
+
function renderCodexForm() {
|
|
2283
|
+
const usable = usableAdapters(state.codexAdapters);
|
|
2284
|
+
$('#codexSection').hidden = !usable.length;
|
|
2285
|
+
if (!usable.length) return;
|
|
2286
|
+
const select = $('#codexAdapter');
|
|
2287
|
+
const current = select.value;
|
|
2288
|
+
const labels = adapterLabels(usable);
|
|
2289
|
+
select.replaceChildren(...usable.map((a, i) => new Option(labels[i], a.id)));
|
|
2290
|
+
if (usable.some(a => a.id === current)) select.value = current;
|
|
2291
|
+
select.hidden = usable.length < 2;
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
async function loadCodexFolders() {
|
|
2295
|
+
const list = $('#codexFolders');
|
|
2296
|
+
list.innerHTML = '';
|
|
2297
|
+
const adapterId = $('#codexAdapter').value;
|
|
2298
|
+
if (!adapterId) return;
|
|
2299
|
+
try {
|
|
2300
|
+
const res = await fetch(`/api/codex/folders?adapterId=${encodeURIComponent(adapterId)}`);
|
|
2301
|
+
if (!res.ok || $('#codexAdapter').value !== adapterId) return;
|
|
2302
|
+
const { folders } = await res.json();
|
|
2303
|
+
list.replaceChildren(...folders.map(f => new Option('', f)));
|
|
2304
|
+
} catch {}
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
async function createCodexConversation() {
|
|
2308
|
+
const btn = $('#codexCreate');
|
|
2309
|
+
const folder = $('#codexFolder').value.trim();
|
|
2310
|
+
const adapterId = $('#codexAdapter').value;
|
|
2311
|
+
if (!folder || !adapterId || btn.disabled) return;
|
|
2312
|
+
const error = $('#codexError');
|
|
2313
|
+
error.hidden = true;
|
|
2314
|
+
btn.disabled = true;
|
|
2315
|
+
btn.textContent = 'Creating…';
|
|
2316
|
+
try {
|
|
2317
|
+
const res = await fetch('/api/codex/threads', {
|
|
2318
|
+
method: 'POST',
|
|
2319
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2320
|
+
body: JSON.stringify({ adapterId, cwd: folder }),
|
|
2321
|
+
});
|
|
2322
|
+
const body = await res.json().catch(() => ({}));
|
|
2323
|
+
if (!res.ok) throw new Error(body.error || `Request failed (${res.status})`);
|
|
2324
|
+
$('#cmdPopup').classList.remove('active');
|
|
2325
|
+
$('#codexFolder').value = '';
|
|
2326
|
+
// The session can register before or after this answer arrives.
|
|
2327
|
+
if (state.sessions[body.sessionId]) selectSession(body.sessionId);
|
|
2328
|
+
else state.pendingSelect = body.sessionId;
|
|
2329
|
+
} catch (err) {
|
|
2330
|
+
error.textContent = err.message;
|
|
2331
|
+
error.hidden = false;
|
|
2332
|
+
} finally {
|
|
2333
|
+
btn.disabled = false;
|
|
2334
|
+
btn.textContent = 'Create';
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2338
|
+
async function onCloseClick(id) {
|
|
2339
|
+
const armedAt = state.closeArmed && state.closeArmed.id === id ? state.closeArmed.at : null;
|
|
2340
|
+
const step = closeStep(armedAt, Date.now());
|
|
2341
|
+
if (step === 'wait') return;
|
|
2342
|
+
if (step === 'arm') {
|
|
2343
|
+
const at = Date.now();
|
|
2344
|
+
state.closeArmed = { id, at };
|
|
2345
|
+
renderSessions();
|
|
2346
|
+
setTimeout(() => {
|
|
2347
|
+
if (state.closeArmed && state.closeArmed.at === at) {
|
|
2348
|
+
state.closeArmed = null;
|
|
2349
|
+
renderSessions();
|
|
2350
|
+
}
|
|
2351
|
+
}, 3000);
|
|
2352
|
+
return;
|
|
2353
|
+
}
|
|
2354
|
+
state.closeArmed = null;
|
|
2355
|
+
renderSessions();
|
|
2356
|
+
try {
|
|
2357
|
+
await fetch('/api/codex/threads/close', {
|
|
2358
|
+
method: 'POST',
|
|
2359
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2360
|
+
body: JSON.stringify({ sessionId: id }),
|
|
2361
|
+
});
|
|
2362
|
+
} catch {}
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
$('#codexAdapter').addEventListener('change', loadCodexFolders);
|
|
2366
|
+
$('#codexCreate').addEventListener('click', createCodexConversation);
|
|
2367
|
+
$('#codexFolder').addEventListener('keydown', (e) => {
|
|
2368
|
+
if (e.key === 'Enter' && !e.isComposing) {
|
|
2369
|
+
e.preventDefault();
|
|
2370
|
+
createCodexConversation();
|
|
2371
|
+
}
|
|
2372
|
+
});
|
|
2196
2373
|
// --- Settings modal (Webhook + Telegram) ---
|
|
2197
2374
|
let webhookData = [];
|
|
2198
2375
|
const closeSettings = () => $('#settingsOverlay').classList.add('hidden');
|
package/dist/hub/server.js
CHANGED
|
@@ -42,6 +42,7 @@ var CODEX_PID_FILE = path.join(CONFIG_DIR, "codex.pid");
|
|
|
42
42
|
var CODEX_LOG_FILE = path.join(CONFIG_DIR, "codex.log");
|
|
43
43
|
var WS_PATH_CHANNEL = "/ws/channel";
|
|
44
44
|
var WS_PATH_DASHBOARD = "/ws/dashboard";
|
|
45
|
+
var WS_PATH_CODEX = "/ws/codex";
|
|
45
46
|
|
|
46
47
|
// src/shared/peer-name.ts
|
|
47
48
|
var SAFE_PEER_NAME = /^[^\u0000-\u001f\u007f"\\[\]]{1,128}$/;
|
|
@@ -175,6 +176,7 @@ var Notifier = class {
|
|
|
175
176
|
execFile(
|
|
176
177
|
"powershell",
|
|
177
178
|
["-Command", '(Get-ItemProperty -Path "HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\PushNotifications" -Name ToastEnabled -ErrorAction SilentlyContinue).ToastEnabled'],
|
|
179
|
+
{ windowsHide: true },
|
|
178
180
|
(err, stdout) => {
|
|
179
181
|
if (err) {
|
|
180
182
|
resolve(true);
|
|
@@ -192,7 +194,7 @@ var Notifier = class {
|
|
|
192
194
|
logger.warn("Windows notifications are disabled. Opening notification settings...");
|
|
193
195
|
logger.warn("Please enable notifications for this app, then try again.");
|
|
194
196
|
if (process.platform === "win32") {
|
|
195
|
-
execFile("powershell", ["-Command", "Start-Process ms-settings:notifications"]);
|
|
197
|
+
execFile("powershell", ["-Command", "Start-Process ms-settings:notifications"], { windowsHide: true });
|
|
196
198
|
}
|
|
197
199
|
setTimeout(() => {
|
|
198
200
|
this.notificationSettingsOpened = false;
|
|
@@ -920,6 +922,21 @@ function logStartup(context) {
|
|
|
920
922
|
`);
|
|
921
923
|
}
|
|
922
924
|
|
|
925
|
+
// src/shared/entry.ts
|
|
926
|
+
function isEntryScript(argv1, suffixes) {
|
|
927
|
+
if (!argv1) return false;
|
|
928
|
+
const normalized = argv1.replace(/\\/g, "/");
|
|
929
|
+
return suffixes.some((suffix) => normalized.endsWith(suffix));
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// src/shared/hub-url.ts
|
|
933
|
+
import net from "net";
|
|
934
|
+
function hubUrlHost(host) {
|
|
935
|
+
if (host === "" || host === "0.0.0.0") return "127.0.0.1";
|
|
936
|
+
if (host === "::") return "[::1]";
|
|
937
|
+
return net.isIPv6(host) ? `[${host}]` : host;
|
|
938
|
+
}
|
|
939
|
+
|
|
923
940
|
// src/hub/auth.ts
|
|
924
941
|
import crypto from "crypto";
|
|
925
942
|
var SESSION_COOKIE = "ca_session";
|
|
@@ -985,6 +1002,7 @@ function validChoices(choices) {
|
|
|
985
1002
|
const valid = choices.filter((c) => typeof c?.id === "string" && typeof c?.label === "string");
|
|
986
1003
|
return valid.length ? valid : void 0;
|
|
987
1004
|
}
|
|
1005
|
+
var nonEmpty = (v) => typeof v === "string" && v.trim() !== "";
|
|
988
1006
|
var HubServer = class {
|
|
989
1007
|
httpServer;
|
|
990
1008
|
wssChannel;
|
|
@@ -1010,7 +1028,13 @@ var HubServer = class {
|
|
|
1010
1028
|
token;
|
|
1011
1029
|
codexEnabled;
|
|
1012
1030
|
codexSupervisor;
|
|
1013
|
-
|
|
1031
|
+
wssCodex;
|
|
1032
|
+
codexAdapters = /* @__PURE__ */ new Map();
|
|
1033
|
+
codexAlive = /* @__PURE__ */ new WeakMap();
|
|
1034
|
+
codexCallTimeoutMs;
|
|
1035
|
+
codexCalls = /* @__PURE__ */ new Map();
|
|
1036
|
+
constructor(config, options = {}) {
|
|
1037
|
+
this.codexCallTimeoutMs = options.codexCallTimeoutMs ?? 6e4;
|
|
1014
1038
|
this.host = config?.hub?.host ?? DEFAULT_HUB_HOST;
|
|
1015
1039
|
this.port = config?.hub?.port ?? DEFAULT_HUB_PORT;
|
|
1016
1040
|
this.token = config?.hub?.token;
|
|
@@ -1023,7 +1047,7 @@ var HubServer = class {
|
|
|
1023
1047
|
if (config?.webhooks) {
|
|
1024
1048
|
this.notifier.configure({ webhooks: config.webhooks });
|
|
1025
1049
|
}
|
|
1026
|
-
const displayHost = this.host
|
|
1050
|
+
const displayHost = hubUrlHost(this.host);
|
|
1027
1051
|
this.notifier.configure({ dashboardUrl: `http://${displayHost}:${this.port}` });
|
|
1028
1052
|
const fullConfig = loadConfig();
|
|
1029
1053
|
if (fullConfig.telegram?.enabled && fullConfig.telegram.botToken && fullConfig.telegram.chatId) {
|
|
@@ -1036,6 +1060,9 @@ var HubServer = class {
|
|
|
1036
1060
|
this.wssDashboard = new WebSocketServer({ noServer: true, maxPayload: MAX_WS_PAYLOAD });
|
|
1037
1061
|
this.wssDashboard.on("connection", (ws) => this.handleDashboardConnection(ws));
|
|
1038
1062
|
this.wssDashboard.on("error", (err) => logger.warn(`Dashboard WebSocket server error: ${err.message}`));
|
|
1063
|
+
this.wssCodex = new WebSocketServer({ noServer: true, maxPayload: MAX_WS_PAYLOAD });
|
|
1064
|
+
this.wssCodex.on("connection", (ws, req) => this.handleCodexConnection(ws, req));
|
|
1065
|
+
this.wssCodex.on("error", (err) => logger.warn(`Codex WebSocket server error: ${err.message}`));
|
|
1039
1066
|
this.httpServer.on("upgrade", (req, socket, head) => {
|
|
1040
1067
|
socket.on("error", (err) => logger.debug(`Upgrade socket error: ${err.message}`));
|
|
1041
1068
|
const url = new URL(req.url ?? "/", "http://hub");
|
|
@@ -1056,6 +1083,10 @@ var HubServer = class {
|
|
|
1056
1083
|
this.wssDashboard.handleUpgrade(req, socket, head, (ws) => {
|
|
1057
1084
|
this.wssDashboard.emit("connection", ws, req);
|
|
1058
1085
|
});
|
|
1086
|
+
} else if (pathname === WS_PATH_CODEX) {
|
|
1087
|
+
this.wssCodex.handleUpgrade(req, socket, head, (ws) => {
|
|
1088
|
+
this.wssCodex.emit("connection", ws, req);
|
|
1089
|
+
});
|
|
1059
1090
|
} else {
|
|
1060
1091
|
socket.destroy();
|
|
1061
1092
|
}
|
|
@@ -1070,7 +1101,7 @@ var HubServer = class {
|
|
|
1070
1101
|
this.httpServer.listen(this.port, this.host, () => {
|
|
1071
1102
|
this.httpServer.removeListener("error", onStartupError);
|
|
1072
1103
|
this.httpServer.on("error", (err) => logger.error(`HTTP server error: ${err.message}`));
|
|
1073
|
-
const displayHost = this.host
|
|
1104
|
+
const displayHost = hubUrlHost(this.host);
|
|
1074
1105
|
logger.info(`Hub server listening on http://${displayHost}:${this.port}`);
|
|
1075
1106
|
this.startCodexAdapter();
|
|
1076
1107
|
resolve();
|
|
@@ -1085,10 +1116,12 @@ var HubServer = class {
|
|
|
1085
1116
|
if (this.telegramBot) this.telegramBot.stopPolling();
|
|
1086
1117
|
for (const ws of this.channelSockets.values()) ws.terminate();
|
|
1087
1118
|
for (const ws of this.dashboardSockets) ws.terminate();
|
|
1119
|
+
for (const { ws } of this.codexAdapters.values()) ws.terminate();
|
|
1088
1120
|
this.channelSockets.clear();
|
|
1089
1121
|
this.dashboardSockets.clear();
|
|
1090
1122
|
this.wssChannel.close();
|
|
1091
1123
|
this.wssDashboard.close();
|
|
1124
|
+
this.wssCodex.close();
|
|
1092
1125
|
this.httpServer.close(() => {
|
|
1093
1126
|
logger.info("Hub server stopped");
|
|
1094
1127
|
resolve();
|
|
@@ -1153,6 +1186,12 @@ var HubServer = class {
|
|
|
1153
1186
|
this.handleApiSend(req, res);
|
|
1154
1187
|
} else if (url.pathname === "/api/notify" && req.method === "POST") {
|
|
1155
1188
|
this.handleApiNotify(req, res);
|
|
1189
|
+
} else if (url.pathname === "/api/codex/folders" && req.method === "GET") {
|
|
1190
|
+
this.handleCodexFolders(url, res);
|
|
1191
|
+
} else if (url.pathname === "/api/codex/threads" && req.method === "POST") {
|
|
1192
|
+
this.handleCodexCreate(req, res);
|
|
1193
|
+
} else if (url.pathname === "/api/codex/threads/close" && req.method === "POST") {
|
|
1194
|
+
this.handleCodexClose(req, res);
|
|
1156
1195
|
} else if (url.pathname === "/api/webhooks" && req.method === "GET") {
|
|
1157
1196
|
const config = loadConfig();
|
|
1158
1197
|
this.jsonResponse(res, 200, { webhooks: config.webhooks || [] });
|
|
@@ -1416,6 +1455,114 @@ var HubServer = class {
|
|
|
1416
1455
|
}
|
|
1417
1456
|
}
|
|
1418
1457
|
}
|
|
1458
|
+
handleCodexConnection(ws, req) {
|
|
1459
|
+
const isLocal = this.isLocalRequest(req);
|
|
1460
|
+
let adapterId;
|
|
1461
|
+
ws.on("error", (err) => {
|
|
1462
|
+
logger.warn(`Codex adapter WebSocket error: ${err.message}`);
|
|
1463
|
+
ws.terminate();
|
|
1464
|
+
});
|
|
1465
|
+
ws.on("pong", () => this.codexAlive.set(ws, true));
|
|
1466
|
+
ws.on("message", (data) => {
|
|
1467
|
+
let msg;
|
|
1468
|
+
try {
|
|
1469
|
+
msg = JSON.parse(data.toString());
|
|
1470
|
+
if (msg === null || typeof msg !== "object") {
|
|
1471
|
+
logger.warn("Invalid message from Codex adapter");
|
|
1472
|
+
return;
|
|
1473
|
+
}
|
|
1474
|
+
} catch {
|
|
1475
|
+
logger.warn("Invalid message from Codex adapter");
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1478
|
+
if (msg.type === "adapter_hello") {
|
|
1479
|
+
const a = msg.adapter;
|
|
1480
|
+
if (typeof a?.id !== "string" || !a.id || typeof a.host !== "string") return;
|
|
1481
|
+
if (adapterId && adapterId !== a.id) return;
|
|
1482
|
+
const holder = this.codexAdapters.get(a.id);
|
|
1483
|
+
if (holder && holder.ws !== ws) holder.ws.terminate();
|
|
1484
|
+
if (!adapterId) logger.info(`Codex adapter connected: ${a.host} (${a.id}, local: ${isLocal})`);
|
|
1485
|
+
adapterId = a.id;
|
|
1486
|
+
this.codexAdapters.set(a.id, { ws, info: { id: a.id, host: a.host, ready: a.ready === true, isLocal } });
|
|
1487
|
+
this.broadcastCodexAdapters();
|
|
1488
|
+
} else if (msg.type === "adapter_result" && adapterId) {
|
|
1489
|
+
const call = this.codexCalls.get(msg.requestId);
|
|
1490
|
+
if (!call || call.adapterId !== adapterId) return;
|
|
1491
|
+
call.settle(msg.ok ? { status: 200, body: msg.data } : { status: 422, body: { error: String(msg.error) } });
|
|
1492
|
+
}
|
|
1493
|
+
});
|
|
1494
|
+
ws.on("close", () => {
|
|
1495
|
+
if (!adapterId || this.codexAdapters.get(adapterId)?.ws !== ws) return;
|
|
1496
|
+
this.codexAdapters.delete(adapterId);
|
|
1497
|
+
for (const call of [...this.codexCalls.values()]) {
|
|
1498
|
+
if (call.adapterId === adapterId) call.settle({ status: 502, body: { error: "Codex adapter disconnected" } });
|
|
1499
|
+
}
|
|
1500
|
+
logger.info(`Codex adapter disconnected: ${adapterId}`);
|
|
1501
|
+
this.broadcastCodexAdapters();
|
|
1502
|
+
});
|
|
1503
|
+
}
|
|
1504
|
+
callCodexAdapter(adapterId, call) {
|
|
1505
|
+
const adapter = this.codexAdapters.get(adapterId);
|
|
1506
|
+
if (!adapter || adapter.ws.readyState !== WebSocket.OPEN) {
|
|
1507
|
+
return Promise.resolve({ status: 404, body: { error: "Codex adapter is not connected" } });
|
|
1508
|
+
}
|
|
1509
|
+
const requestId = randomUUID3();
|
|
1510
|
+
return new Promise((resolve) => {
|
|
1511
|
+
const timer = setTimeout(
|
|
1512
|
+
() => settle({ status: 504, body: { error: "Codex did not respond in time. The conversation may still appear." } }),
|
|
1513
|
+
this.codexCallTimeoutMs
|
|
1514
|
+
);
|
|
1515
|
+
const settle = (outcome) => {
|
|
1516
|
+
clearTimeout(timer);
|
|
1517
|
+
this.codexCalls.delete(requestId);
|
|
1518
|
+
resolve(outcome);
|
|
1519
|
+
};
|
|
1520
|
+
this.codexCalls.set(requestId, { adapterId, settle });
|
|
1521
|
+
adapter.ws.send(JSON.stringify({ type: "adapter_call", requestId, call }));
|
|
1522
|
+
});
|
|
1523
|
+
}
|
|
1524
|
+
async handleCodexFolders(url, res) {
|
|
1525
|
+
const adapterId = url.searchParams.get("adapterId");
|
|
1526
|
+
if (!nonEmpty(adapterId)) {
|
|
1527
|
+
this.jsonResponse(res, 400, { error: "adapterId is required" });
|
|
1528
|
+
return;
|
|
1529
|
+
}
|
|
1530
|
+
const out = await this.callCodexAdapter(adapterId, { kind: "folders" });
|
|
1531
|
+
this.jsonResponse(res, out.status, out.body);
|
|
1532
|
+
}
|
|
1533
|
+
async handleCodexCreate(req, res) {
|
|
1534
|
+
const body = await this.readBody(req);
|
|
1535
|
+
const adapterId = body?.adapterId;
|
|
1536
|
+
const cwd = body?.cwd;
|
|
1537
|
+
if (!nonEmpty(adapterId) || !nonEmpty(cwd)) {
|
|
1538
|
+
this.jsonResponse(res, 400, { error: "adapterId and cwd are required" });
|
|
1539
|
+
return;
|
|
1540
|
+
}
|
|
1541
|
+
const out = await this.callCodexAdapter(adapterId, { kind: "create", cwd });
|
|
1542
|
+
this.jsonResponse(res, out.status, out.body);
|
|
1543
|
+
}
|
|
1544
|
+
async handleCodexClose(req, res) {
|
|
1545
|
+
const body = await this.readBody(req);
|
|
1546
|
+
const sessionId = body?.sessionId;
|
|
1547
|
+
if (!nonEmpty(sessionId)) {
|
|
1548
|
+
this.jsonResponse(res, 400, { error: "sessionId is required" });
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1551
|
+
const session = this.sessions.get(sessionId);
|
|
1552
|
+
const ws = this.channelSockets.get(sessionId);
|
|
1553
|
+
if (session?.agentKind !== "codex" || !session.closable || ws?.readyState !== WebSocket.OPEN) {
|
|
1554
|
+
this.jsonResponse(res, 404, { error: "No closable Codex conversation" });
|
|
1555
|
+
return;
|
|
1556
|
+
}
|
|
1557
|
+
ws.send(JSON.stringify({ type: "codex_close", sessionId }));
|
|
1558
|
+
this.jsonResponse(res, 200, { ok: true });
|
|
1559
|
+
}
|
|
1560
|
+
codexAdapterList() {
|
|
1561
|
+
return [...this.codexAdapters.values()].map((a) => a.info);
|
|
1562
|
+
}
|
|
1563
|
+
broadcastCodexAdapters() {
|
|
1564
|
+
this.broadcastToDashboards({ type: "codex_adapters", adapters: this.codexAdapterList() });
|
|
1565
|
+
}
|
|
1419
1566
|
// --- Dashboard WebSocket ---
|
|
1420
1567
|
handleDashboardConnection(ws) {
|
|
1421
1568
|
this.dashboardSockets.add(ws);
|
|
@@ -1434,6 +1581,7 @@ var HubServer = class {
|
|
|
1434
1581
|
requests: [...this.choiceRequests.values()].map((c) => c.request)
|
|
1435
1582
|
};
|
|
1436
1583
|
ws.send(JSON.stringify(pendingMsg));
|
|
1584
|
+
ws.send(JSON.stringify({ type: "codex_adapters", adapters: this.codexAdapterList() }));
|
|
1437
1585
|
ws.on("message", (data) => {
|
|
1438
1586
|
try {
|
|
1439
1587
|
const msg = JSON.parse(data.toString());
|
|
@@ -1521,7 +1669,8 @@ var HubServer = class {
|
|
|
1521
1669
|
imagePath: filePath,
|
|
1522
1670
|
mimeType,
|
|
1523
1671
|
originalName,
|
|
1524
|
-
content
|
|
1672
|
+
content,
|
|
1673
|
+
source: "dashboard"
|
|
1525
1674
|
};
|
|
1526
1675
|
channelWs.send(JSON.stringify(forwardMsg));
|
|
1527
1676
|
logger.info(`Image saved and forwarded: ${filename} (${buffer.length} bytes)`);
|
|
@@ -1530,7 +1679,7 @@ var HubServer = class {
|
|
|
1530
1679
|
fs5.unlinkSync(filePath);
|
|
1531
1680
|
} catch {
|
|
1532
1681
|
}
|
|
1533
|
-
}, 5 * 60 * 1e3);
|
|
1682
|
+
}, 5 * 60 * 1e3).unref();
|
|
1534
1683
|
}
|
|
1535
1684
|
async handleWebhookSave(req, res) {
|
|
1536
1685
|
const body = await this.readBody(req);
|
|
@@ -1563,7 +1712,7 @@ var HubServer = class {
|
|
|
1563
1712
|
this.telegramBot.onImageToSession = (sessionId, imagePath, mimeType, caption) => {
|
|
1564
1713
|
const channelWs = this.channelSockets.get(sessionId);
|
|
1565
1714
|
if (channelWs?.readyState === WebSocket.OPEN) {
|
|
1566
|
-
const msg = { type: "image_to_session", sessionId, imagePath, mimeType, content: caption };
|
|
1715
|
+
const msg = { type: "image_to_session", sessionId, imagePath, mimeType, content: caption, source: "telegram" };
|
|
1567
1716
|
channelWs.send(JSON.stringify(msg));
|
|
1568
1717
|
logger.info(`Telegram photo forwarded to session: ${sessionId}`);
|
|
1569
1718
|
}
|
|
@@ -1706,6 +1855,14 @@ var HubServer = class {
|
|
|
1706
1855
|
this.channelAlive.set(sessionId, false);
|
|
1707
1856
|
ws.ping();
|
|
1708
1857
|
}
|
|
1858
|
+
for (const { ws } of this.codexAdapters.values()) {
|
|
1859
|
+
if (this.codexAlive.get(ws) === false) {
|
|
1860
|
+
ws.terminate();
|
|
1861
|
+
continue;
|
|
1862
|
+
}
|
|
1863
|
+
this.codexAlive.set(ws, false);
|
|
1864
|
+
ws.ping();
|
|
1865
|
+
}
|
|
1709
1866
|
}, 3e4);
|
|
1710
1867
|
}
|
|
1711
1868
|
cleanupUploads() {
|
|
@@ -1768,7 +1925,7 @@ Content-Length: 0\r
|
|
|
1768
1925
|
});
|
|
1769
1926
|
}
|
|
1770
1927
|
};
|
|
1771
|
-
if (process.argv[1]
|
|
1928
|
+
if (isEntryScript(process.argv[1], ["/hub/server.js", "/hub/server.ts"])) {
|
|
1772
1929
|
installCrashGuard("hub daemon");
|
|
1773
1930
|
logStartup("Hub daemon");
|
|
1774
1931
|
const config = loadConfig();
|