@axiomatic-labs/claudeflow 2.13.28 → 2.13.30
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/lib/panel.js +298 -0
- package/package.json +1 -1
package/lib/panel.js
CHANGED
|
@@ -309,6 +309,98 @@ 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
|
+
// Old sidecars (pre-token-estimate) lack `contentTokens`. Backfill on
|
|
357
|
+
// the fly using the same chars/4 heuristic — keeps the UI consistent
|
|
358
|
+
// for events captured before the upgrade.
|
|
359
|
+
const tokens = typeof r.contentTokens === 'number'
|
|
360
|
+
? r.contentTokens
|
|
361
|
+
: (r.contentBytes ? Math.round(r.contentBytes / 4) : 0);
|
|
362
|
+
entries.push({
|
|
363
|
+
id: r.id,
|
|
364
|
+
timestamp: r.timestamp,
|
|
365
|
+
type: r.type,
|
|
366
|
+
event: r.event,
|
|
367
|
+
matcher: r.matcher || '',
|
|
368
|
+
handler: r.handler,
|
|
369
|
+
summary: r.summary,
|
|
370
|
+
contentBytes: r.contentBytes || 0,
|
|
371
|
+
contentTokens: tokens,
|
|
372
|
+
activeReminderIds: r.activeReminderIds || null,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
return { entries, total };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function readLogEvent(cwd, id) {
|
|
379
|
+
if (!isValidLogId(id)) return null;
|
|
380
|
+
try {
|
|
381
|
+
return JSON.parse(fs.readFileSync(path.join(logEventsDir(cwd), `${id}.json`), 'utf8'));
|
|
382
|
+
} catch {
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function getLogsInfo(cwd) {
|
|
388
|
+
const dir = logEventsDir(cwd);
|
|
389
|
+
let total = 0;
|
|
390
|
+
let mostRecent = null;
|
|
391
|
+
try {
|
|
392
|
+
const files = fs.readdirSync(dir).filter((f) => f.endsWith('.json')).sort();
|
|
393
|
+
total = files.length;
|
|
394
|
+
if (total > 0) {
|
|
395
|
+
try {
|
|
396
|
+
const r = JSON.parse(fs.readFileSync(path.join(dir, files[files.length - 1]), 'utf8'));
|
|
397
|
+
mostRecent = r.timestamp;
|
|
398
|
+
} catch {}
|
|
399
|
+
}
|
|
400
|
+
} catch {}
|
|
401
|
+
return { available: true, total, mostRecent };
|
|
402
|
+
}
|
|
403
|
+
|
|
312
404
|
function getDoctorInfo(cwd) {
|
|
313
405
|
const checks = [checkCdpPortMismatch(cwd), checkStaleLockfiles(cwd)];
|
|
314
406
|
const issues = checks.filter((c) => c.severity !== 'ok' && c.severity !== 'info');
|
|
@@ -331,6 +423,7 @@ function collectStatus(cwd) {
|
|
|
331
423
|
activeRun: getActiveRunInfo(cwd),
|
|
332
424
|
reminders: getRemindersInfo(cwd),
|
|
333
425
|
enforcement: getEnforcementInfo(cwd),
|
|
426
|
+
logs: getLogsInfo(cwd),
|
|
334
427
|
doctor: getDoctorInfo(cwd),
|
|
335
428
|
};
|
|
336
429
|
}
|
|
@@ -441,6 +534,26 @@ details[open] summary { padding-bottom: 8px; border-bottom: 1px solid var(--bord
|
|
|
441
534
|
.reminder-row .reminder-desc { font-size: 12px; margin-left: 22px; margin-top: 2px; }
|
|
442
535
|
.reminder-row .reminder-id { font-size: 11px; margin-left: 22px; margin-top: 2px; }
|
|
443
536
|
.reminder-row code { background: var(--panel-2); padding: 1px 5px; border-radius: 3px; font-size: 11px; }
|
|
537
|
+
.logs-controls { display: flex; gap: 12px; align-items: center; margin-bottom: 14px; }
|
|
538
|
+
.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; }
|
|
539
|
+
.logs-controls button:hover { border-color: var(--accent); cursor: pointer; }
|
|
540
|
+
.logs-table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
541
|
+
.logs-table th, .logs-table td { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--border); vertical-align: top; }
|
|
542
|
+
.logs-table th { font-weight: 500; color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
543
|
+
.logs-table tr:hover td { background: rgba(124, 58, 237, 0.05); }
|
|
544
|
+
.logs-time { font-family: var(--mono); font-size: 12px; white-space: nowrap; }
|
|
545
|
+
.logs-handler { font-family: var(--mono); font-size: 12px; }
|
|
546
|
+
.logs-size { font-family: var(--mono); font-size: 12px; color: var(--muted); white-space: nowrap; }
|
|
547
|
+
.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; }
|
|
548
|
+
.logs-view:hover { border-color: var(--accent); }
|
|
549
|
+
#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; }
|
|
550
|
+
#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); }
|
|
551
|
+
#log-drawer .drawer-head .title { font-weight: 600; }
|
|
552
|
+
#log-drawer .drawer-head button { background: none; border: 1px solid var(--border); color: var(--fg); padding: 5px 10px; border-radius: 5px; cursor: pointer; }
|
|
553
|
+
#log-drawer .drawer-body { padding: 18px 22px; }
|
|
554
|
+
#log-drawer h3 { margin: 0 0 12px; font-size: 15px; }
|
|
555
|
+
#log-drawer h4 { margin: 0 0 6px; font-size: 13px; color: var(--muted); }
|
|
556
|
+
#log-drawer pre { max-height: none; }
|
|
444
557
|
.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
558
|
.toast.err { border-color: var(--err); }
|
|
446
559
|
.toast.ok { border-color: var(--ok); }
|
|
@@ -466,12 +579,20 @@ details[open] summary { padding-bottom: 8px; border-bottom: 1px solid var(--bord
|
|
|
466
579
|
<nav id="nav"></nav>
|
|
467
580
|
<section id="content"><p class="muted">Loading…</p></section>
|
|
468
581
|
</main>
|
|
582
|
+
<aside id="log-drawer">
|
|
583
|
+
<div class="drawer-head">
|
|
584
|
+
<span class="title">Log event details</span>
|
|
585
|
+
<button id="log-drawer-close">Close ✕</button>
|
|
586
|
+
</div>
|
|
587
|
+
<div class="drawer-body" id="log-drawer-body"></div>
|
|
588
|
+
</aside>
|
|
469
589
|
<script>
|
|
470
590
|
const SECTIONS = [
|
|
471
591
|
{ id: 'overview', label: 'Overview' },
|
|
472
592
|
{ id: 'claudeMd', label: 'CLAUDE.md' },
|
|
473
593
|
{ id: 'hooks', label: 'Hooks' },
|
|
474
594
|
{ id: 'reminders', label: 'Reminders' },
|
|
595
|
+
{ id: 'logs', label: 'Logs' },
|
|
475
596
|
{ id: 'mcp', label: 'MCP & observer' },
|
|
476
597
|
{ id: 'setupContext', label: 'Setup context' },
|
|
477
598
|
{ id: 'activeRun', label: 'Active run' },
|
|
@@ -542,6 +663,8 @@ async function refresh() {
|
|
|
542
663
|
restoreOpenDetails(open);
|
|
543
664
|
// If preview was visible/open, ensure it stays loaded after re-render.
|
|
544
665
|
if (open.has('claude-md-preview')) loadClaudeMdPreview();
|
|
666
|
+
// Refresh logs entries silently on the Logs tab so new events appear.
|
|
667
|
+
if (active === 'logs') loadLogsPage();
|
|
545
668
|
}
|
|
546
669
|
|
|
547
670
|
function renderHeader() {
|
|
@@ -571,6 +694,7 @@ function severityFor(id) {
|
|
|
571
694
|
case 'claudeMd': return !s.claudeMd.exists ? 'err' : (s.claudeMd.optOut ? 'info' : 'ok');
|
|
572
695
|
case 'hooks': return s.hooks.warnings > 0 ? 'warn' : 'ok';
|
|
573
696
|
case 'reminders': return s.reminders && s.reminders.disabledCount > 0 ? 'info' : 'ok';
|
|
697
|
+
case 'logs': return s.logs && s.logs.total > 0 ? 'ok' : 'info';
|
|
574
698
|
case 'mcp': return !s.mcp.configFound ? 'info' : (s.mcp.playwright.match && s.mcp.staleLockfiles.length === 0 ? 'ok' : 'warn');
|
|
575
699
|
case 'setupContext': return !s.setupContext.exists ? 'err' : (s.setupContext.toolingComplete ? 'ok' : 'warn');
|
|
576
700
|
case 'activeRun': return s.activeRun.active ? 'info' : 'info';
|
|
@@ -615,6 +739,7 @@ function renderContent() {
|
|
|
615
739
|
claudeMd: renderClaudeMd,
|
|
616
740
|
hooks: renderHooks,
|
|
617
741
|
reminders: renderReminders,
|
|
742
|
+
logs: renderLogs,
|
|
618
743
|
mcp: renderMcp,
|
|
619
744
|
setupContext: renderSetup,
|
|
620
745
|
activeRun: renderRun,
|
|
@@ -642,6 +767,9 @@ function renderOverview() {
|
|
|
642
767
|
const ef = s.enforcement && s.enforcement.available
|
|
643
768
|
? (s.enforcement.on ? \`ON (\${s.enforcement.count} blocking hooks)\` : \`OFF — \${s.enforcement.count} blocking hooks silenced\`)
|
|
644
769
|
: 'unavailable';
|
|
770
|
+
const lg = s.logs && s.logs.available
|
|
771
|
+
? \`\${s.logs.total} captured\${s.logs.mostRecent ? ' · last ' + fmtRelativeTime(s.logs.mostRecent) : ''}\`
|
|
772
|
+
: 'no events';
|
|
645
773
|
return \`<h2>Overview</h2>
|
|
646
774
|
<p class="sub">Snapshot at \${new Date(s.timestamp).toLocaleTimeString()}</p>
|
|
647
775
|
<div class="card">
|
|
@@ -653,6 +781,7 @@ function renderOverview() {
|
|
|
653
781
|
\${row('Active run', ar, { kind: 'info', text: s.activeRun.active ? '·' : '·' })}
|
|
654
782
|
\${row('Reminders', rm, { kind: s.reminders && s.reminders.available ? (s.reminders.disabledCount ? 'info' : 'ok') : 'err', text: s.reminders && s.reminders.disabledCount ? '·' : '✓' })}
|
|
655
783
|
\${row('Enforcement', ef, { kind: s.enforcement && s.enforcement.available ? (s.enforcement.on ? 'ok' : 'err') : 'err', text: s.enforcement && s.enforcement.available ? (s.enforcement.on ? '✓' : '✗') : '·' })}
|
|
784
|
+
\${row('Logs', lg, { kind: s.logs && s.logs.total > 0 ? 'ok' : 'info', text: '·' })}
|
|
656
785
|
\${row('Doctor', dc, { kind: s.doctor.issueCount === 0 ? 'ok' : 'warn', text: s.doctor.issueCount === 0 ? '✓' : '!' })}
|
|
657
786
|
</div>\`;
|
|
658
787
|
}
|
|
@@ -730,6 +859,139 @@ function renderReminders() {
|
|
|
730
859
|
\${sections}\`;
|
|
731
860
|
}
|
|
732
861
|
|
|
862
|
+
// Logs tab — paginated table backed by /api/logs. Lazy-loaded (we don't
|
|
863
|
+
// inline 500 rows into the initial render). The "View" button fetches
|
|
864
|
+
// the full sidecar from /api/logs/:id and renders it in the drawer.
|
|
865
|
+
let logsState = { entries: [], type: 'all', loading: false, allLoaded: false };
|
|
866
|
+
|
|
867
|
+
async function loadLogsPage({ append = false } = {}) {
|
|
868
|
+
const params = new URLSearchParams({ limit: '100', type: logsState.type });
|
|
869
|
+
if (append && logsState.entries.length > 0) {
|
|
870
|
+
params.set('before', logsState.entries[logsState.entries.length - 1].id);
|
|
871
|
+
}
|
|
872
|
+
logsState.loading = true;
|
|
873
|
+
try {
|
|
874
|
+
const r = await fetch('/api/logs?' + params.toString());
|
|
875
|
+
const j = await r.json();
|
|
876
|
+
if (append) logsState.entries = logsState.entries.concat(j.entries);
|
|
877
|
+
else logsState.entries = j.entries;
|
|
878
|
+
logsState.allLoaded = j.entries.length < 100;
|
|
879
|
+
} catch (e) {
|
|
880
|
+
showToast('Logs fetch failed: ' + e.message, 'err');
|
|
881
|
+
} finally {
|
|
882
|
+
logsState.loading = false;
|
|
883
|
+
if (active === 'logs') renderContent();
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
async function viewLogDetail(id) {
|
|
888
|
+
const drawer = document.getElementById('log-drawer');
|
|
889
|
+
const body = document.getElementById('log-drawer-body');
|
|
890
|
+
drawer.style.display = 'block';
|
|
891
|
+
body.innerHTML = '<p class="muted">Loading…</p>';
|
|
892
|
+
try {
|
|
893
|
+
const r = await fetch('/api/logs/' + encodeURIComponent(id));
|
|
894
|
+
if (!r.ok) { body.innerHTML = '<p class="error">Event not found.</p>'; return; }
|
|
895
|
+
const ev = await r.json();
|
|
896
|
+
const meta = [
|
|
897
|
+
['ID', ev.id],
|
|
898
|
+
['Timestamp (UTC)', ev.timestamp],
|
|
899
|
+
['Local time', new Date(ev.timestamp).toLocaleString()],
|
|
900
|
+
['Type', ev.type],
|
|
901
|
+
['Event', ev.event],
|
|
902
|
+
['Matcher', ev.matcher || '(any)'],
|
|
903
|
+
['Handler', ev.handler],
|
|
904
|
+
];
|
|
905
|
+
let html = '<h3>' + escapeHtml(ev.summary || ev.type) + '</h3>';
|
|
906
|
+
html += '<div class="card">' + meta.map(([k, v]) => row(k, v)).join('') + '</div>';
|
|
907
|
+
if (Array.isArray(ev.activeReminderIds)) {
|
|
908
|
+
html += '<h4 style="margin-top:14px">Active reminders at fire time</h4>';
|
|
909
|
+
html += '<p class="sub">' + ev.activeReminderIds.map(escapeHtml).join(', ') + '</p>';
|
|
910
|
+
}
|
|
911
|
+
if (typeof ev.contentBytes === 'number' && ev.contentBytes > 0) {
|
|
912
|
+
const tokens = typeof ev.contentTokens === 'number' ? ev.contentTokens : Math.round(ev.contentBytes / 4);
|
|
913
|
+
html += '<h4 style="margin-top:14px">Injected content — ≈' + tokens.toLocaleString() + ' tokens <span class="muted" style="font-size:11px">(approx · ' + fmtBytes(ev.contentBytes) + ' raw)</span></h4>';
|
|
914
|
+
}
|
|
915
|
+
if (typeof ev.content === 'string' && ev.content.length > 0) {
|
|
916
|
+
html += '<pre>' + escapeHtml(ev.content) + '</pre>';
|
|
917
|
+
} else if (ev.type === 'enforcement-silenced') {
|
|
918
|
+
html += '<p class="muted">No content — the hook was silenced before running.</p>';
|
|
919
|
+
} else {
|
|
920
|
+
html += '<p class="muted">No captured content.</p>';
|
|
921
|
+
}
|
|
922
|
+
body.innerHTML = html;
|
|
923
|
+
} catch (e) {
|
|
924
|
+
body.innerHTML = '<p class="error">Error: ' + escapeHtml(e.message) + '</p>';
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function closeLogDrawer() {
|
|
929
|
+
document.getElementById('log-drawer').style.display = 'none';
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function fmtRelativeTime(iso) {
|
|
933
|
+
const ms = Date.now() - new Date(iso).getTime();
|
|
934
|
+
if (ms < 1000) return 'just now';
|
|
935
|
+
if (ms < 60000) return Math.floor(ms / 1000) + 's ago';
|
|
936
|
+
if (ms < 3600000) return Math.floor(ms / 60000) + 'm ago';
|
|
937
|
+
if (ms < 86400000) return Math.floor(ms / 3600000) + 'h ago';
|
|
938
|
+
return Math.floor(ms / 86400000) + 'd ago';
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
function fmtLocalTime(iso) {
|
|
942
|
+
const d = new Date(iso);
|
|
943
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
944
|
+
return \`\${d.getFullYear()}-\${pad(d.getMonth() + 1)}-\${pad(d.getDate())} \${pad(d.getHours())}:\${pad(d.getMinutes())}:\${pad(d.getSeconds())}\`;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
function renderLogs() {
|
|
948
|
+
// Trigger initial load (or filter change reload) on first render.
|
|
949
|
+
if (logsState.entries.length === 0 && !logsState.loading) {
|
|
950
|
+
loadLogsPage();
|
|
951
|
+
}
|
|
952
|
+
const totalLabel = state.logs && state.logs.total != null ? \`\${state.logs.total} total events\` : '';
|
|
953
|
+
const rows = logsState.entries.map((e) => {
|
|
954
|
+
const isReminder = e.type.startsWith('reminder');
|
|
955
|
+
const badge = isReminder
|
|
956
|
+
? '<span class="badge info" style="background:rgba(88,166,255,0.18); color:#58a6ff">REMINDER</span>'
|
|
957
|
+
: '<span class="badge err">ENFORCEMENT</span>';
|
|
958
|
+
const tokensStr = e.contentTokens
|
|
959
|
+
? \`≈\${e.contentTokens.toLocaleString()}\`
|
|
960
|
+
: '—';
|
|
961
|
+
const bytesStr = e.contentBytes ? fmtBytes(e.contentBytes) : '';
|
|
962
|
+
const tokensTitle = e.contentBytes
|
|
963
|
+
? \`≈\${e.contentTokens} tokens (approx, chars/4 heuristic) · \${fmtBytes(e.contentBytes)} raw\`
|
|
964
|
+
: 'No content (enforcement-silenced)';
|
|
965
|
+
return \`<tr>
|
|
966
|
+
<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>
|
|
967
|
+
<td>\${badge}</td>
|
|
968
|
+
<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>
|
|
969
|
+
<td class="logs-size" title="\${escapeHtml(tokensTitle)}">\${tokensStr}\${bytesStr ? \`<br><span class="muted" style="font-size:11px">\${bytesStr}</span>\` : ''}</td>
|
|
970
|
+
<td><button class="logs-view" data-log-id="\${escapeHtml(e.id)}">View</button></td>
|
|
971
|
+
</tr>\`;
|
|
972
|
+
}).join('');
|
|
973
|
+
const empty = logsState.entries.length === 0
|
|
974
|
+
? '<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>'
|
|
975
|
+
: '';
|
|
976
|
+
return \`<h2>Logs</h2>
|
|
977
|
+
<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>
|
|
978
|
+
<div class="logs-controls">
|
|
979
|
+
<label>Filter:
|
|
980
|
+
<select id="logs-filter">
|
|
981
|
+
<option value="all" \${logsState.type === 'all' ? 'selected' : ''}>All</option>
|
|
982
|
+
<option value="reminder" \${logsState.type === 'reminder' ? 'selected' : ''}>Reminders only</option>
|
|
983
|
+
<option value="enforcement" \${logsState.type === 'enforcement' ? 'selected' : ''}>Enforcement-silenced only</option>
|
|
984
|
+
</select>
|
|
985
|
+
</label>
|
|
986
|
+
<button id="logs-reload">Reload</button>
|
|
987
|
+
</div>
|
|
988
|
+
\${empty || \`<table class="logs-table">
|
|
989
|
+
<thead><tr><th>Time</th><th>Type</th><th>Handler</th><th title="Approximate token count (chars/4 heuristic — within ±10–15% of Claude's actual tokenizer for markdown/code mix)">≈ Tokens</th><th></th></tr></thead>
|
|
990
|
+
<tbody>\${rows}</tbody>
|
|
991
|
+
</table>\`}
|
|
992
|
+
\${logsState.allLoaded || logsState.entries.length === 0 ? '' : '<div style="margin-top:12px"><button id="logs-load-more">Load older</button></div>'}\`;
|
|
993
|
+
}
|
|
994
|
+
|
|
733
995
|
function renderMcp() {
|
|
734
996
|
const m = state.mcp;
|
|
735
997
|
if (!m.configFound) return '<h2>MCP & observer</h2><p class="muted">.mcp.json not found</p>';
|
|
@@ -831,9 +1093,33 @@ async function toggleReminder(id, disable) {
|
|
|
831
1093
|
await refresh();
|
|
832
1094
|
}
|
|
833
1095
|
|
|
1096
|
+
document.addEventListener('click', (e) => {
|
|
1097
|
+
const t = e.target;
|
|
1098
|
+
if (!t) return;
|
|
1099
|
+
if (t.id === 'log-drawer-close') return closeLogDrawer();
|
|
1100
|
+
if (t.classList && t.classList.contains('logs-view') && t.dataset.logId) {
|
|
1101
|
+
return viewLogDetail(t.dataset.logId);
|
|
1102
|
+
}
|
|
1103
|
+
if (t.id === 'logs-reload') {
|
|
1104
|
+
logsState.entries = [];
|
|
1105
|
+
logsState.allLoaded = false;
|
|
1106
|
+
return loadLogsPage();
|
|
1107
|
+
}
|
|
1108
|
+
if (t.id === 'logs-load-more') {
|
|
1109
|
+
return loadLogsPage({ append: true });
|
|
1110
|
+
}
|
|
1111
|
+
});
|
|
1112
|
+
|
|
834
1113
|
document.addEventListener('change', (e) => {
|
|
835
1114
|
const t = e.target;
|
|
836
1115
|
if (!t || !t.classList) return;
|
|
1116
|
+
if (t.id === 'logs-filter') {
|
|
1117
|
+
logsState.type = t.value;
|
|
1118
|
+
logsState.entries = [];
|
|
1119
|
+
logsState.allLoaded = false;
|
|
1120
|
+
loadLogsPage();
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
837
1123
|
if (t.classList.contains('hook-toggle')) {
|
|
838
1124
|
const event = t.dataset.event;
|
|
839
1125
|
const matcher = t.dataset.matcher;
|
|
@@ -936,6 +1222,18 @@ function handler(cwd) {
|
|
|
936
1222
|
if (route === '/' || route === '/index.html') return send(200, renderHtml(), 'text/html; charset=utf-8');
|
|
937
1223
|
if (route === '/api/status') return send(200, JSON.stringify(collectStatus(cwd)), 'application/json');
|
|
938
1224
|
if (route === '/api/claude-md') return send(200, readClaudeMdSafe(cwd), 'text/plain; charset=utf-8');
|
|
1225
|
+
if (route === '/api/logs') {
|
|
1226
|
+
const limit = Math.min(parseInt(url.searchParams.get('limit'), 10) || 100, 500);
|
|
1227
|
+
const before = url.searchParams.get('before') || null;
|
|
1228
|
+
const type = url.searchParams.get('type') || 'all';
|
|
1229
|
+
return send(200, JSON.stringify(listLogEvents(cwd, { limit, before, type })), 'application/json');
|
|
1230
|
+
}
|
|
1231
|
+
if (route.startsWith('/api/logs/')) {
|
|
1232
|
+
const id = route.slice('/api/logs/'.length);
|
|
1233
|
+
const ev = readLogEvent(cwd, id);
|
|
1234
|
+
if (!ev) return send(404, JSON.stringify({ error: 'not found' }), 'application/json');
|
|
1235
|
+
return send(200, JSON.stringify(ev), 'application/json');
|
|
1236
|
+
}
|
|
939
1237
|
if (route === '/healthz') return send(200, 'ok', 'text/plain');
|
|
940
1238
|
}
|
|
941
1239
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axiomatic-labs/claudeflow",
|
|
3
|
-
"version": "2.13.
|
|
3
|
+
"version": "2.13.30",
|
|
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"
|