@axiomatic-labs/claudeflow 2.13.33 → 2.13.35

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 +97 -6
  2. package/package.json +1 -1
package/lib/panel.js CHANGED
@@ -385,6 +385,7 @@ function listLogEvents(cwd, { limit = 200, before = null, type = 'all' } = {}) {
385
385
  if (type !== 'all') {
386
386
  if (type === 'enforcement' && !r.type.startsWith('enforcement')) continue;
387
387
  if (type === 'reminder' && !r.type.startsWith('reminder')) continue;
388
+ if (type === 'blocked' && r.type !== 'enforcement-blocked') continue;
388
389
  }
389
390
  // Strip large `content` from list view; clients fetch it via /api/logs/:id
390
391
  // Old sidecars (pre-token-estimate) lack `contentTokens`. Backfill on
@@ -408,6 +409,8 @@ function listLogEvents(cwd, { limit = 200, before = null, type = 'all' } = {}) {
408
409
  capturedFromStdout: r.capturedFromStdout === true,
409
410
  stdoutTruncated: r.stdoutTruncated === true,
410
411
  parseError: r.parseError || null,
412
+ decision: r.decision || null,
413
+ reason: r.reason || null,
411
414
  });
412
415
  }
413
416
  return { entries, total, thresholds };
@@ -422,6 +425,29 @@ function readLogEvent(cwd, id) {
422
425
  }
423
426
  }
424
427
 
428
+ // Destructive: removes every sidecar in .claude/tmp/log-events/ and truncates
429
+ // .claude/tmp/hooks.log. Returns counts so the UI can confirm the wipe.
430
+ function clearLogs(cwd) {
431
+ const dir = logEventsDir(cwd);
432
+ let removed = 0;
433
+ try {
434
+ const files = fs.readdirSync(dir).filter((f) => f.endsWith('.json'));
435
+ for (const f of files) {
436
+ try { fs.unlinkSync(path.join(dir, f)); removed++; } catch {}
437
+ }
438
+ } catch {}
439
+ // Truncate the human-readable log too so the user sees a fresh tail -f.
440
+ let logTruncated = false;
441
+ const logPath = path.join(cwd, '.claude', 'tmp', 'hooks.log');
442
+ try {
443
+ if (fs.existsSync(logPath)) {
444
+ fs.writeFileSync(logPath, '');
445
+ logTruncated = true;
446
+ }
447
+ } catch {}
448
+ return { removed, logTruncated };
449
+ }
450
+
425
451
  function getLogsInfo(cwd) {
426
452
  const dir = logEventsDir(cwd);
427
453
  const thresholds = tokenThresholds();
@@ -429,11 +455,16 @@ function getLogsInfo(cwd) {
429
455
  let mostRecent = null;
430
456
  let latestReminderTokens = null;
431
457
  let latestReminderStatus = null;
458
+ let recentBlockedCount = 0;
432
459
  try {
433
460
  const files = fs.readdirSync(dir).filter((f) => f.endsWith('.json')).sort();
434
461
  total = files.length;
435
- // Walk from newest to find both latest event timestamp and latest reminder.
436
- for (let i = files.length - 1; i >= 0 && (mostRecent === null || latestReminderTokens === null); i--) {
462
+ // Walk from newest. Count blocks within the last 50 events for the
463
+ // sidebar severity. Latest-reminder lookup short-circuits when both
464
+ // mostRecent + latestReminderTokens are populated AND we've scanned
465
+ // enough for the block tally.
466
+ const scanCap = Math.min(50, files.length);
467
+ for (let i = files.length - 1; i >= files.length - scanCap; i--) {
437
468
  try {
438
469
  const r = JSON.parse(fs.readFileSync(path.join(dir, files[i]), 'utf8'));
439
470
  if (mostRecent === null) mostRecent = r.timestamp;
@@ -444,6 +475,7 @@ function getLogsInfo(cwd) {
444
475
  latestReminderTokens = tokens;
445
476
  latestReminderStatus = classifyTokens(tokens, thresholds);
446
477
  }
478
+ if (r.type === 'enforcement-blocked') recentBlockedCount++;
447
479
  } catch {}
448
480
  }
449
481
  } catch {}
@@ -454,6 +486,7 @@ function getLogsInfo(cwd) {
454
486
  thresholds,
455
487
  latestReminderTokens,
456
488
  latestReminderStatus,
489
+ recentBlockedCount,
457
490
  };
458
491
  }
459
492
 
@@ -593,6 +626,10 @@ details[open] summary { padding-bottom: 8px; border-bottom: 1px solid var(--bord
593
626
  .logs-controls { display: flex; gap: 12px; align-items: center; margin-bottom: 14px; }
594
627
  .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; }
595
628
  .logs-controls button:hover { border-color: var(--accent); cursor: pointer; }
629
+ .logs-clear-btn { margin-left: auto; color: var(--err); border-color: rgba(248,81,73,0.4); }
630
+ .logs-clear-btn:hover { border-color: var(--err); background: rgba(248,81,73,0.08); }
631
+ .logs-clear-btn:disabled { opacity: 0.4; cursor: not-allowed; color: var(--muted); border-color: var(--border); }
632
+ .logs-clear-btn:disabled:hover { border-color: var(--border); background: var(--panel); }
596
633
  .logs-table { width: 100%; border-collapse: collapse; font-size: 13px; }
597
634
  .logs-table th, .logs-table td { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--border); vertical-align: top; }
598
635
  .logs-table th { font-weight: 500; color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px; }
@@ -611,6 +648,10 @@ details[open] summary { padding-bottom: 8px; border-bottom: 1px solid var(--bord
611
648
  .logs-status { font-family: var(--mono); font-size: 12px; white-space: nowrap; }
612
649
  .logs-status.injected { color: #58a6ff; }
613
650
  .logs-status.silenced { color: var(--err); font-weight: 600; }
651
+ .logs-status.blocked { color: var(--err); font-weight: 700; }
652
+ .logs-table tr.blocked .logs-handler { color: var(--err); }
653
+ .logs-table tr.blocked td:first-child { border-left: 3px solid var(--err) !important; }
654
+ .logs-reason-preview { font-size: 11px; color: var(--err); margin-top: 2px; max-width: 480px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
614
655
  .token-badge { display: inline-block; padding: 1px 7px; border-radius: 999px; font-size: 10px; font-family: var(--mono); margin-left: 6px; vertical-align: middle; }
615
656
  .token-badge.heavy { background: rgba(210,153,34,0.18); color: var(--warn); }
616
657
  .token-badge.high { background: rgba(248,81,73,0.20); color: var(--err); }
@@ -765,6 +806,7 @@ function severityFor(id) {
765
806
  case 'reminders': return s.reminders && s.reminders.disabledCount > 0 ? 'info' : 'ok';
766
807
  case 'logs': {
767
808
  if (!s.logs) return 'info';
809
+ if (s.logs.recentBlockedCount > 0) return 'err';
768
810
  if (s.logs.latestReminderStatus === 'high') return 'err';
769
811
  if (s.logs.latestReminderStatus === 'heavy') return 'warn';
770
812
  return s.logs.total > 0 ? 'ok' : 'info';
@@ -1001,6 +1043,17 @@ async function viewLogDetail(id) {
1001
1043
  : '<span class="badge warn" title="No content was captured — the hook may have crashed before writing or returned null.">⚠ no stdout</span>');
1002
1044
  html += '<h4 style="margin-top:14px">Provenance ' + provBadge + (ev.stdoutTruncated ? ' <span class="badge warn">⚠ truncated</span>' : '') + '</h4>';
1003
1045
  }
1046
+ // Enforcement-blocked detail: show decision + full reason verbatim.
1047
+ if (ev.type === 'enforcement-blocked') {
1048
+ html += '<h4 style="margin-top:14px">Decision: <span class="badge err">' + escapeHtml(ev.decision || 'block').toUpperCase() + '</span></h4>';
1049
+ if (ev.reason) {
1050
+ html += '<h4 style="margin-top:14px">Reason</h4>';
1051
+ html += '<pre style="border-color: rgba(248,81,73,0.5)">' + escapeHtml(ev.reason) + '</pre>';
1052
+ }
1053
+ if (ev.capturedStdout) {
1054
+ html += '<details style="margin-top:14px"><summary>Raw stdout (' + fmtBytes(ev.stdoutBytes || 0) + ')</summary><pre>' + escapeHtml(ev.capturedStdout) + '</pre></details>';
1055
+ }
1056
+ }
1004
1057
  if (typeof ev.contentBytes === 'number' && ev.contentBytes > 0) {
1005
1058
  const tokens = typeof ev.contentTokens === 'number' ? ev.contentTokens : Math.round(ev.contentBytes / 4);
1006
1059
  const th = (state.logs && state.logs.thresholds) || { heavy: 8000, high: 20000 };
@@ -1065,11 +1118,14 @@ function renderLogs() {
1065
1118
  const rows = logsState.entries.map((e) => {
1066
1119
  const isReminder = e.type.startsWith('reminder');
1067
1120
  const isSilenced = e.type === 'enforcement-silenced';
1121
+ const isBlocked = e.type === 'enforcement-blocked';
1068
1122
  const badge = isReminder
1069
1123
  ? '<span class="badge info" style="background:rgba(88,166,255,0.18); color:#58a6ff">REMINDER</span>'
1070
1124
  : '<span class="badge err">ENFORCEMENT</span>';
1071
1125
  const statusCell = isSilenced
1072
1126
  ? '<span class="logs-status silenced" title="Master Enforcement OFF silenced this hook — it did NOT run. Claude Code received no output for this event.">✗ SILENCED</span>'
1127
+ : isBlocked
1128
+ ? '<span class="logs-status blocked" title="Hook ran and BLOCKED this action. Claude Code rejected the tool call. Click View for the full reason.">⛔ BLOCKED</span>'
1073
1129
  : '<span class="logs-status injected" title="Hook ran and emitted additionalContext to Claude Code">✓ injected</span>';
1074
1130
  const tokenStatus = e.tokenStatus || 'none';
1075
1131
  const tokenBadge = tokenStatus === 'high'
@@ -1083,13 +1139,18 @@ function renderLogs() {
1083
1139
  const bytesStr = e.contentBytes ? fmtBytes(e.contentBytes) : '';
1084
1140
  const tokensTitle = e.contentBytes
1085
1141
  ? \`≈\${e.contentTokens} tokens (chars/4 heuristic) · \${fmtBytes(e.contentBytes)} raw · status: \${tokenStatus}\`
1086
- : 'No content (hook was silenced — never ran)';
1087
- const rowCls = \`token-\${tokenStatus}\${isSilenced ? ' silenced' : ''}\`;
1142
+ : isBlocked
1143
+ ? 'Hook BLOCKED the action. Click View for full reason.'
1144
+ : 'No content (hook was silenced — never ran)';
1145
+ const rowCls = \`token-\${tokenStatus}\${isSilenced ? ' silenced' : ''}\${isBlocked ? ' blocked' : ''}\`;
1146
+ const reasonPreview = isBlocked && e.reason
1147
+ ? \`<div class="logs-reason-preview" title="\${escapeHtml(e.reason)}">\${escapeHtml(e.reason.split('\\n')[0].slice(0, 120))}</div>\`
1148
+ : '';
1088
1149
  return \`<tr class="\${rowCls}">
1089
1150
  <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>
1090
1151
  <td>\${badge}</td>
1091
1152
  <td>\${statusCell}</td>
1092
- <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>
1153
+ <td class="logs-handler"><span class="muted">\${escapeHtml(e.event)}</span><br>\${escapeHtml(e.handler)}\${e.matcher ? \` <span class="muted">(\${escapeHtml(e.matcher)})</span>\` : ''}\${reasonPreview}</td>
1093
1154
  <td class="logs-size" title="\${escapeHtml(tokensTitle)}">\${tokensStr}\${tokenBadge}\${bytesStr ? \`<br><span class="muted" style="font-size:11px">\${bytesStr}</span>\` : ''}</td>
1094
1155
  <td><button class="logs-view" data-log-id="\${escapeHtml(e.id)}">View</button></td>
1095
1156
  </tr>\`;
@@ -1109,10 +1170,12 @@ function renderLogs() {
1109
1170
  <select id="logs-filter">
1110
1171
  <option value="all" \${logsState.type === 'all' ? 'selected' : ''}>All</option>
1111
1172
  <option value="reminder" \${logsState.type === 'reminder' ? 'selected' : ''}>Reminders only</option>
1112
- <option value="enforcement" \${logsState.type === 'enforcement' ? 'selected' : ''}>Enforcement-silenced only</option>
1173
+ <option value="enforcement" \${logsState.type === 'enforcement' ? 'selected' : ''}>Enforcement (silenced + blocked)</option>
1174
+ <option value="blocked" \${logsState.type === 'blocked' ? 'selected' : ''}>Blocked actions only</option>
1113
1175
  </select>
1114
1176
  </label>
1115
1177
  <button id="logs-reload">Reload</button>
1178
+ <button id="logs-clear" class="logs-clear-btn" \${state.logs && state.logs.total ? '' : 'disabled'} title="Delete every sidecar in .claude/tmp/log-events/ and truncate .claude/tmp/hooks.log. Cannot be undone.">Clear all</button>
1116
1179
  </div>
1117
1180
  \${empty || \`<table class="logs-table">
1118
1181
  <thead><tr><th>Time</th><th>Type</th><th title="Did the hook actually run? ✓ injected = hook ran and emitted to Claude Code · ✗ SILENCED = master Enforcement OFF blocked the hook from running.">Status</th><th>Handler</th><th title="Approximate token count (chars/4 heuristic, ±10–15% vs Claude's real tokenizer). Heavy ≥\${thresholds.heavy.toLocaleString()} → yellow border. High ≥\${thresholds.high.toLocaleString()} → red border. Anthropic does not publish official thresholds for per-turn additionalContext.">≈ Tokens</th><th></th></tr></thead>
@@ -1237,8 +1300,31 @@ document.addEventListener('click', (e) => {
1237
1300
  if (t.id === 'logs-load-more') {
1238
1301
  return loadLogsPage({ append: true });
1239
1302
  }
1303
+ if (t.id === 'logs-clear') {
1304
+ const total = (state.logs && state.logs.total) || 0;
1305
+ if (!total) return;
1306
+ if (!confirm('Delete ' + total + ' captured log event(s) and truncate .claude/tmp/hooks.log?\\n\\nThis cannot be undone. (Future events will be captured normally.)')) return;
1307
+ return clearLogsAction();
1308
+ }
1240
1309
  });
1241
1310
 
1311
+ async function clearLogsAction() {
1312
+ try {
1313
+ const r = await fetch('/api/logs', { method: 'DELETE' });
1314
+ const result = await r.json();
1315
+ if (!r.ok) {
1316
+ showToast('Clear failed: ' + (result.error || r.status), 'err');
1317
+ return;
1318
+ }
1319
+ showToast('Cleared ' + result.removed + ' event(s)' + (result.logTruncated ? ' + truncated hooks.log' : ''), 'ok');
1320
+ logsState.entries = [];
1321
+ logsState.allLoaded = false;
1322
+ await refresh();
1323
+ } catch (e) {
1324
+ showToast('Network error: ' + e.message, 'err');
1325
+ }
1326
+ }
1327
+
1242
1328
  document.addEventListener('change', (e) => {
1243
1329
  const t = e.target;
1244
1330
  if (!t || !t.classList) return;
@@ -1380,6 +1466,11 @@ function handler(cwd) {
1380
1466
  return send(status, JSON.stringify(result), 'application/json');
1381
1467
  }
1382
1468
 
1469
+ if (req.method === 'DELETE' && route === '/api/logs') {
1470
+ const result = clearLogs(cwd);
1471
+ return send(200, JSON.stringify(result), 'application/json');
1472
+ }
1473
+
1383
1474
  if (req.method === 'POST' && route === '/api/enforcement/toggle') {
1384
1475
  const raw = await readRequestBody(req);
1385
1476
  let payload;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axiomatic-labs/claudeflow",
3
- "version": "2.13.33",
3
+ "version": "2.13.35",
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"