@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@delt/claude-alarm",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Monitor and get notifications from multiple Claude Code sessions via MCP Channels",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -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">&#128203;</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?' : '&times;'}</button>`
1536
+ : '';
1479
1537
  return `<div class="session-card${active}" data-id="${id}">
1480
1538
  <span class="drag-handle" title="Drag to reorder">&#10303;</span>
1481
1539
  <div class="session-body">
@@ -1483,6 +1541,7 @@
1483
1541
  ${cwdDisplay ? `<div class="session-cwd" title="${esc(s.cwd)}">${esc(cwdDisplay)} &middot; ${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 && s.agentKind !== 'codex';
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
- bar.innerHTML = reqs.map(r => {
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');