@axiomatic-labs/claudeflow 2.13.28 → 2.13.29

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.
Files changed (2) hide show
  1. package/lib/panel.js +284 -0
  2. package/package.json +1 -1
package/lib/panel.js CHANGED
@@ -309,6 +309,91 @@ function getRemindersInfo(cwd) {
309
309
  };
310
310
  }
311
311
 
312
+ // ─── log events (Logs tab) ───────────────────────────────────────
313
+ //
314
+ // Sidecar files live in `.claude/tmp/log-events/<id>.json`. Each is one
315
+ // event (enforcement-silenced or reminder-injected) with metadata and,
316
+ // for reminders, the full content that was injected at fire time. The
317
+ // panel reads them via:
318
+ // GET /api/logs?limit=&type=&before= → metadata-only paged list
319
+ // GET /api/logs/:id → full record incl. content
320
+ function logEventsDir(cwd) {
321
+ return path.join(cwd, '.claude', 'tmp', 'log-events');
322
+ }
323
+
324
+ function isValidLogId(id) {
325
+ return typeof id === 'string' && /^[A-Za-z0-9_-]+$/.test(id) && id.length <= 64;
326
+ }
327
+
328
+ function listLogEvents(cwd, { limit = 200, before = null, type = 'all' } = {}) {
329
+ const dir = logEventsDir(cwd);
330
+ let files;
331
+ try {
332
+ files = fs.readdirSync(dir).filter((f) => f.endsWith('.json'));
333
+ } catch {
334
+ return { entries: [], total: 0 };
335
+ }
336
+ files.sort().reverse(); // filenames begin with sortable timestamp; newest first
337
+ const total = files.length;
338
+
339
+ let beforeIdx = 0;
340
+ if (before) {
341
+ const beforeFile = `${before}.json`;
342
+ const idx = files.indexOf(beforeFile);
343
+ if (idx >= 0) beforeIdx = idx + 1;
344
+ }
345
+
346
+ const entries = [];
347
+ for (let i = beforeIdx; i < files.length && entries.length < limit; i++) {
348
+ let r;
349
+ try { r = JSON.parse(fs.readFileSync(path.join(dir, files[i]), 'utf8')); }
350
+ catch { continue; }
351
+ if (type !== 'all') {
352
+ if (type === 'enforcement' && !r.type.startsWith('enforcement')) continue;
353
+ if (type === 'reminder' && !r.type.startsWith('reminder')) continue;
354
+ }
355
+ // Strip large `content` from list view; clients fetch it via /api/logs/:id
356
+ entries.push({
357
+ id: r.id,
358
+ timestamp: r.timestamp,
359
+ type: r.type,
360
+ event: r.event,
361
+ matcher: r.matcher || '',
362
+ handler: r.handler,
363
+ summary: r.summary,
364
+ contentBytes: r.contentBytes || 0,
365
+ activeReminderIds: r.activeReminderIds || null,
366
+ });
367
+ }
368
+ return { entries, total };
369
+ }
370
+
371
+ function readLogEvent(cwd, id) {
372
+ if (!isValidLogId(id)) return null;
373
+ try {
374
+ return JSON.parse(fs.readFileSync(path.join(logEventsDir(cwd), `${id}.json`), 'utf8'));
375
+ } catch {
376
+ return null;
377
+ }
378
+ }
379
+
380
+ function getLogsInfo(cwd) {
381
+ const dir = logEventsDir(cwd);
382
+ let total = 0;
383
+ let mostRecent = null;
384
+ try {
385
+ const files = fs.readdirSync(dir).filter((f) => f.endsWith('.json')).sort();
386
+ total = files.length;
387
+ if (total > 0) {
388
+ try {
389
+ const r = JSON.parse(fs.readFileSync(path.join(dir, files[files.length - 1]), 'utf8'));
390
+ mostRecent = r.timestamp;
391
+ } catch {}
392
+ }
393
+ } catch {}
394
+ return { available: true, total, mostRecent };
395
+ }
396
+
312
397
  function getDoctorInfo(cwd) {
313
398
  const checks = [checkCdpPortMismatch(cwd), checkStaleLockfiles(cwd)];
314
399
  const issues = checks.filter((c) => c.severity !== 'ok' && c.severity !== 'info');
@@ -331,6 +416,7 @@ function collectStatus(cwd) {
331
416
  activeRun: getActiveRunInfo(cwd),
332
417
  reminders: getRemindersInfo(cwd),
333
418
  enforcement: getEnforcementInfo(cwd),
419
+ logs: getLogsInfo(cwd),
334
420
  doctor: getDoctorInfo(cwd),
335
421
  };
336
422
  }
@@ -441,6 +527,26 @@ details[open] summary { padding-bottom: 8px; border-bottom: 1px solid var(--bord
441
527
  .reminder-row .reminder-desc { font-size: 12px; margin-left: 22px; margin-top: 2px; }
442
528
  .reminder-row .reminder-id { font-size: 11px; margin-left: 22px; margin-top: 2px; }
443
529
  .reminder-row code { background: var(--panel-2); padding: 1px 5px; border-radius: 3px; font-size: 11px; }
530
+ .logs-controls { display: flex; gap: 12px; align-items: center; margin-bottom: 14px; }
531
+ .logs-controls select, .logs-controls button { background: var(--panel); border: 1px solid var(--border); color: var(--fg); padding: 6px 10px; border-radius: 6px; font: inherit; }
532
+ .logs-controls button:hover { border-color: var(--accent); cursor: pointer; }
533
+ .logs-table { width: 100%; border-collapse: collapse; font-size: 13px; }
534
+ .logs-table th, .logs-table td { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--border); vertical-align: top; }
535
+ .logs-table th { font-weight: 500; color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px; }
536
+ .logs-table tr:hover td { background: rgba(124, 58, 237, 0.05); }
537
+ .logs-time { font-family: var(--mono); font-size: 12px; white-space: nowrap; }
538
+ .logs-handler { font-family: var(--mono); font-size: 12px; }
539
+ .logs-size { font-family: var(--mono); font-size: 12px; color: var(--muted); white-space: nowrap; }
540
+ .logs-view { background: var(--panel-2); border: 1px solid var(--border); color: var(--fg); padding: 4px 12px; border-radius: 5px; font: inherit; font-size: 12px; cursor: pointer; }
541
+ .logs-view:hover { border-color: var(--accent); }
542
+ #log-drawer { display: none; position: fixed; top: 0; right: 0; bottom: 0; width: 60%; max-width: 920px; background: var(--panel); border-left: 1px solid var(--border); box-shadow: -8px 0 24px rgba(0,0,0,0.4); z-index: 100; overflow-y: auto; }
543
+ #log-drawer .drawer-head { display: flex; justify-content: space-between; align-items: center; padding: 14px 22px; border-bottom: 1px solid var(--border); position: sticky; top: 0; background: var(--panel); }
544
+ #log-drawer .drawer-head .title { font-weight: 600; }
545
+ #log-drawer .drawer-head button { background: none; border: 1px solid var(--border); color: var(--fg); padding: 5px 10px; border-radius: 5px; cursor: pointer; }
546
+ #log-drawer .drawer-body { padding: 18px 22px; }
547
+ #log-drawer h3 { margin: 0 0 12px; font-size: 15px; }
548
+ #log-drawer h4 { margin: 0 0 6px; font-size: 13px; color: var(--muted); }
549
+ #log-drawer pre { max-height: none; }
444
550
  .toast { position: fixed; bottom: 18px; right: 18px; background: var(--panel); border: 1px solid var(--border); padding: 10px 14px; border-radius: 6px; font-size: 13px; box-shadow: 0 4px 12px rgba(0,0,0,0.4); }
445
551
  .toast.err { border-color: var(--err); }
446
552
  .toast.ok { border-color: var(--ok); }
@@ -466,12 +572,20 @@ details[open] summary { padding-bottom: 8px; border-bottom: 1px solid var(--bord
466
572
  <nav id="nav"></nav>
467
573
  <section id="content"><p class="muted">Loading…</p></section>
468
574
  </main>
575
+ <aside id="log-drawer">
576
+ <div class="drawer-head">
577
+ <span class="title">Log event details</span>
578
+ <button id="log-drawer-close">Close ✕</button>
579
+ </div>
580
+ <div class="drawer-body" id="log-drawer-body"></div>
581
+ </aside>
469
582
  <script>
470
583
  const SECTIONS = [
471
584
  { id: 'overview', label: 'Overview' },
472
585
  { id: 'claudeMd', label: 'CLAUDE.md' },
473
586
  { id: 'hooks', label: 'Hooks' },
474
587
  { id: 'reminders', label: 'Reminders' },
588
+ { id: 'logs', label: 'Logs' },
475
589
  { id: 'mcp', label: 'MCP & observer' },
476
590
  { id: 'setupContext', label: 'Setup context' },
477
591
  { id: 'activeRun', label: 'Active run' },
@@ -542,6 +656,8 @@ async function refresh() {
542
656
  restoreOpenDetails(open);
543
657
  // If preview was visible/open, ensure it stays loaded after re-render.
544
658
  if (open.has('claude-md-preview')) loadClaudeMdPreview();
659
+ // Refresh logs entries silently on the Logs tab so new events appear.
660
+ if (active === 'logs') loadLogsPage();
545
661
  }
546
662
 
547
663
  function renderHeader() {
@@ -571,6 +687,7 @@ function severityFor(id) {
571
687
  case 'claudeMd': return !s.claudeMd.exists ? 'err' : (s.claudeMd.optOut ? 'info' : 'ok');
572
688
  case 'hooks': return s.hooks.warnings > 0 ? 'warn' : 'ok';
573
689
  case 'reminders': return s.reminders && s.reminders.disabledCount > 0 ? 'info' : 'ok';
690
+ case 'logs': return s.logs && s.logs.total > 0 ? 'ok' : 'info';
574
691
  case 'mcp': return !s.mcp.configFound ? 'info' : (s.mcp.playwright.match && s.mcp.staleLockfiles.length === 0 ? 'ok' : 'warn');
575
692
  case 'setupContext': return !s.setupContext.exists ? 'err' : (s.setupContext.toolingComplete ? 'ok' : 'warn');
576
693
  case 'activeRun': return s.activeRun.active ? 'info' : 'info';
@@ -615,6 +732,7 @@ function renderContent() {
615
732
  claudeMd: renderClaudeMd,
616
733
  hooks: renderHooks,
617
734
  reminders: renderReminders,
735
+ logs: renderLogs,
618
736
  mcp: renderMcp,
619
737
  setupContext: renderSetup,
620
738
  activeRun: renderRun,
@@ -642,6 +760,9 @@ function renderOverview() {
642
760
  const ef = s.enforcement && s.enforcement.available
643
761
  ? (s.enforcement.on ? \`ON (\${s.enforcement.count} blocking hooks)\` : \`OFF — \${s.enforcement.count} blocking hooks silenced\`)
644
762
  : 'unavailable';
763
+ const lg = s.logs && s.logs.available
764
+ ? \`\${s.logs.total} captured\${s.logs.mostRecent ? ' · last ' + fmtRelativeTime(s.logs.mostRecent) : ''}\`
765
+ : 'no events';
645
766
  return \`<h2>Overview</h2>
646
767
  <p class="sub">Snapshot at \${new Date(s.timestamp).toLocaleTimeString()}</p>
647
768
  <div class="card">
@@ -653,6 +774,7 @@ function renderOverview() {
653
774
  \${row('Active run', ar, { kind: 'info', text: s.activeRun.active ? '·' : '·' })}
654
775
  \${row('Reminders', rm, { kind: s.reminders && s.reminders.available ? (s.reminders.disabledCount ? 'info' : 'ok') : 'err', text: s.reminders && s.reminders.disabledCount ? '·' : '✓' })}
655
776
  \${row('Enforcement', ef, { kind: s.enforcement && s.enforcement.available ? (s.enforcement.on ? 'ok' : 'err') : 'err', text: s.enforcement && s.enforcement.available ? (s.enforcement.on ? '✓' : '✗') : '·' })}
777
+ \${row('Logs', lg, { kind: s.logs && s.logs.total > 0 ? 'ok' : 'info', text: '·' })}
656
778
  \${row('Doctor', dc, { kind: s.doctor.issueCount === 0 ? 'ok' : 'warn', text: s.doctor.issueCount === 0 ? '✓' : '!' })}
657
779
  </div>\`;
658
780
  }
@@ -730,6 +852,132 @@ function renderReminders() {
730
852
  \${sections}\`;
731
853
  }
732
854
 
855
+ // Logs tab — paginated table backed by /api/logs. Lazy-loaded (we don't
856
+ // inline 500 rows into the initial render). The "View" button fetches
857
+ // the full sidecar from /api/logs/:id and renders it in the drawer.
858
+ let logsState = { entries: [], type: 'all', loading: false, allLoaded: false };
859
+
860
+ async function loadLogsPage({ append = false } = {}) {
861
+ const params = new URLSearchParams({ limit: '100', type: logsState.type });
862
+ if (append && logsState.entries.length > 0) {
863
+ params.set('before', logsState.entries[logsState.entries.length - 1].id);
864
+ }
865
+ logsState.loading = true;
866
+ try {
867
+ const r = await fetch('/api/logs?' + params.toString());
868
+ const j = await r.json();
869
+ if (append) logsState.entries = logsState.entries.concat(j.entries);
870
+ else logsState.entries = j.entries;
871
+ logsState.allLoaded = j.entries.length < 100;
872
+ } catch (e) {
873
+ showToast('Logs fetch failed: ' + e.message, 'err');
874
+ } finally {
875
+ logsState.loading = false;
876
+ if (active === 'logs') renderContent();
877
+ }
878
+ }
879
+
880
+ async function viewLogDetail(id) {
881
+ const drawer = document.getElementById('log-drawer');
882
+ const body = document.getElementById('log-drawer-body');
883
+ drawer.style.display = 'block';
884
+ body.innerHTML = '<p class="muted">Loading…</p>';
885
+ try {
886
+ const r = await fetch('/api/logs/' + encodeURIComponent(id));
887
+ if (!r.ok) { body.innerHTML = '<p class="error">Event not found.</p>'; return; }
888
+ const ev = await r.json();
889
+ const meta = [
890
+ ['ID', ev.id],
891
+ ['Timestamp (UTC)', ev.timestamp],
892
+ ['Local time', new Date(ev.timestamp).toLocaleString()],
893
+ ['Type', ev.type],
894
+ ['Event', ev.event],
895
+ ['Matcher', ev.matcher || '(any)'],
896
+ ['Handler', ev.handler],
897
+ ];
898
+ let html = '<h3>' + escapeHtml(ev.summary || ev.type) + '</h3>';
899
+ html += '<div class="card">' + meta.map(([k, v]) => row(k, v)).join('') + '</div>';
900
+ if (Array.isArray(ev.activeReminderIds)) {
901
+ html += '<h4 style="margin-top:14px">Active reminders at fire time</h4>';
902
+ html += '<p class="sub">' + ev.activeReminderIds.map(escapeHtml).join(', ') + '</p>';
903
+ }
904
+ if (typeof ev.contentBytes === 'number') {
905
+ html += '<h4 style="margin-top:14px">Injected content (' + fmtBytes(ev.contentBytes) + ')</h4>';
906
+ }
907
+ if (typeof ev.content === 'string' && ev.content.length > 0) {
908
+ html += '<pre>' + escapeHtml(ev.content) + '</pre>';
909
+ } else if (ev.type === 'enforcement-silenced') {
910
+ html += '<p class="muted">No content — the hook was silenced before running.</p>';
911
+ } else {
912
+ html += '<p class="muted">No captured content.</p>';
913
+ }
914
+ body.innerHTML = html;
915
+ } catch (e) {
916
+ body.innerHTML = '<p class="error">Error: ' + escapeHtml(e.message) + '</p>';
917
+ }
918
+ }
919
+
920
+ function closeLogDrawer() {
921
+ document.getElementById('log-drawer').style.display = 'none';
922
+ }
923
+
924
+ function fmtRelativeTime(iso) {
925
+ const ms = Date.now() - new Date(iso).getTime();
926
+ if (ms < 1000) return 'just now';
927
+ if (ms < 60000) return Math.floor(ms / 1000) + 's ago';
928
+ if (ms < 3600000) return Math.floor(ms / 60000) + 'm ago';
929
+ if (ms < 86400000) return Math.floor(ms / 3600000) + 'h ago';
930
+ return Math.floor(ms / 86400000) + 'd ago';
931
+ }
932
+
933
+ function fmtLocalTime(iso) {
934
+ const d = new Date(iso);
935
+ const pad = (n) => String(n).padStart(2, '0');
936
+ return \`\${d.getFullYear()}-\${pad(d.getMonth() + 1)}-\${pad(d.getDate())} \${pad(d.getHours())}:\${pad(d.getMinutes())}:\${pad(d.getSeconds())}\`;
937
+ }
938
+
939
+ function renderLogs() {
940
+ // Trigger initial load (or filter change reload) on first render.
941
+ if (logsState.entries.length === 0 && !logsState.loading) {
942
+ loadLogsPage();
943
+ }
944
+ const totalLabel = state.logs && state.logs.total != null ? \`\${state.logs.total} total events\` : '';
945
+ const rows = logsState.entries.map((e) => {
946
+ const isReminder = e.type.startsWith('reminder');
947
+ const badge = isReminder
948
+ ? '<span class="badge info" style="background:rgba(88,166,255,0.18); color:#58a6ff">REMINDER</span>'
949
+ : '<span class="badge err">ENFORCEMENT</span>';
950
+ const sizeStr = e.contentBytes ? fmtBytes(e.contentBytes) : '—';
951
+ return \`<tr>
952
+ <td class="logs-time"><span title="\${escapeHtml(e.timestamp)}">\${escapeHtml(fmtLocalTime(e.timestamp))}</span><br><span class="muted" style="font-size:11px">\${fmtRelativeTime(e.timestamp)}</span></td>
953
+ <td>\${badge}</td>
954
+ <td class="logs-handler"><span class="muted">\${escapeHtml(e.event)}</span><br>\${escapeHtml(e.handler)}\${e.matcher ? \` <span class="muted">(\${escapeHtml(e.matcher)})</span>\` : ''}</td>
955
+ <td class="logs-size">\${sizeStr}</td>
956
+ <td><button class="logs-view" data-log-id="\${escapeHtml(e.id)}">View</button></td>
957
+ </tr>\`;
958
+ }).join('');
959
+ const empty = logsState.entries.length === 0
960
+ ? '<p class="muted" style="padding:24px 0">No events captured yet. Open a Claude Code session, send a prompt — reminders + any silenced enforcement hooks will appear here.</p>'
961
+ : '';
962
+ return \`<h2>Logs</h2>
963
+ <p class="sub">Real-time trace of every reminder injection and enforcement-silenced hook. \${totalLabel} · sidecars in <code>.claude/tmp/log-events/</code> (capped at 2000, oldest evicted).</p>
964
+ <div class="logs-controls">
965
+ <label>Filter:
966
+ <select id="logs-filter">
967
+ <option value="all" \${logsState.type === 'all' ? 'selected' : ''}>All</option>
968
+ <option value="reminder" \${logsState.type === 'reminder' ? 'selected' : ''}>Reminders only</option>
969
+ <option value="enforcement" \${logsState.type === 'enforcement' ? 'selected' : ''}>Enforcement-silenced only</option>
970
+ </select>
971
+ </label>
972
+ <button id="logs-reload">Reload</button>
973
+ </div>
974
+ \${empty || \`<table class="logs-table">
975
+ <thead><tr><th>Time</th><th>Type</th><th>Handler</th><th>Size</th><th></th></tr></thead>
976
+ <tbody>\${rows}</tbody>
977
+ </table>\`}
978
+ \${logsState.allLoaded || logsState.entries.length === 0 ? '' : '<div style="margin-top:12px"><button id="logs-load-more">Load older</button></div>'}\`;
979
+ }
980
+
733
981
  function renderMcp() {
734
982
  const m = state.mcp;
735
983
  if (!m.configFound) return '<h2>MCP & observer</h2><p class="muted">.mcp.json not found</p>';
@@ -831,9 +1079,33 @@ async function toggleReminder(id, disable) {
831
1079
  await refresh();
832
1080
  }
833
1081
 
1082
+ document.addEventListener('click', (e) => {
1083
+ const t = e.target;
1084
+ if (!t) return;
1085
+ if (t.id === 'log-drawer-close') return closeLogDrawer();
1086
+ if (t.classList && t.classList.contains('logs-view') && t.dataset.logId) {
1087
+ return viewLogDetail(t.dataset.logId);
1088
+ }
1089
+ if (t.id === 'logs-reload') {
1090
+ logsState.entries = [];
1091
+ logsState.allLoaded = false;
1092
+ return loadLogsPage();
1093
+ }
1094
+ if (t.id === 'logs-load-more') {
1095
+ return loadLogsPage({ append: true });
1096
+ }
1097
+ });
1098
+
834
1099
  document.addEventListener('change', (e) => {
835
1100
  const t = e.target;
836
1101
  if (!t || !t.classList) return;
1102
+ if (t.id === 'logs-filter') {
1103
+ logsState.type = t.value;
1104
+ logsState.entries = [];
1105
+ logsState.allLoaded = false;
1106
+ loadLogsPage();
1107
+ return;
1108
+ }
837
1109
  if (t.classList.contains('hook-toggle')) {
838
1110
  const event = t.dataset.event;
839
1111
  const matcher = t.dataset.matcher;
@@ -936,6 +1208,18 @@ function handler(cwd) {
936
1208
  if (route === '/' || route === '/index.html') return send(200, renderHtml(), 'text/html; charset=utf-8');
937
1209
  if (route === '/api/status') return send(200, JSON.stringify(collectStatus(cwd)), 'application/json');
938
1210
  if (route === '/api/claude-md') return send(200, readClaudeMdSafe(cwd), 'text/plain; charset=utf-8');
1211
+ if (route === '/api/logs') {
1212
+ const limit = Math.min(parseInt(url.searchParams.get('limit'), 10) || 100, 500);
1213
+ const before = url.searchParams.get('before') || null;
1214
+ const type = url.searchParams.get('type') || 'all';
1215
+ return send(200, JSON.stringify(listLogEvents(cwd, { limit, before, type })), 'application/json');
1216
+ }
1217
+ if (route.startsWith('/api/logs/')) {
1218
+ const id = route.slice('/api/logs/'.length);
1219
+ const ev = readLogEvent(cwd, id);
1220
+ if (!ev) return send(404, JSON.stringify({ error: 'not found' }), 'application/json');
1221
+ return send(200, JSON.stringify(ev), 'application/json');
1222
+ }
939
1223
  if (route === '/healthz') return send(200, 'ok', 'text/plain');
940
1224
  }
941
1225
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axiomatic-labs/claudeflow",
3
- "version": "2.13.28",
3
+ "version": "2.13.29",
4
4
  "description": "Claudeflow — AI-powered development toolkit for Claude Code. Skills, agents, hooks, and quality gates that ship production apps.",
5
5
  "bin": {
6
6
  "claudeflow": "./bin/cli.js"