@axiomatic-labs/claudeflow 2.13.29 → 2.13.31

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 +133 -15
  2. package/package.json +1 -1
package/lib/panel.js CHANGED
@@ -309,6 +309,39 @@ function getRemindersInfo(cwd) {
309
309
  };
310
310
  }
311
311
 
312
+ // ─── token-budget thresholds ─────────────────────────────────────
313
+ //
314
+ // Anthropic does NOT publish official limits for per-turn additionalContext.
315
+ // These defaults are heuristics derived from:
316
+ // - GitHub issue anthropics/claude-code#45188 (system prompt 100K+ tokens
317
+ // made sessions unusable without /compact)
318
+ // - Community guidance: keep injected per-turn content well under 10–15%
319
+ // of the 200K context window so user content + tool results have room
320
+ // - Practical observation that 5–10K tokens for project rules is common
321
+ //
322
+ // Override per project via env vars before launching `claudeflow panel`:
323
+ // CLAUDEFLOW_TOKEN_HEAVY=8000 (yellow threshold)
324
+ // CLAUDEFLOW_TOKEN_HIGH=20000 (red threshold)
325
+ const TOKEN_HEAVY_DEFAULT = 8000;
326
+ const TOKEN_HIGH_DEFAULT = 20000;
327
+
328
+ function tokenThresholds() {
329
+ const heavy = parseInt(process.env.CLAUDEFLOW_TOKEN_HEAVY, 10);
330
+ const high = parseInt(process.env.CLAUDEFLOW_TOKEN_HIGH, 10);
331
+ return {
332
+ heavy: Number.isFinite(heavy) && heavy > 0 ? heavy : TOKEN_HEAVY_DEFAULT,
333
+ high: Number.isFinite(high) && high > 0 ? high : TOKEN_HIGH_DEFAULT,
334
+ };
335
+ }
336
+
337
+ function classifyTokens(tokens, thresholds = tokenThresholds()) {
338
+ if (!tokens || tokens <= 0) return 'none';
339
+ if (tokens >= thresholds.high) return 'high';
340
+ if (tokens >= thresholds.heavy) return 'heavy';
341
+ if (tokens < 2000) return 'light';
342
+ return 'normal';
343
+ }
344
+
312
345
  // ─── log events (Logs tab) ───────────────────────────────────────
313
346
  //
314
347
  // Sidecar files live in `.claude/tmp/log-events/<id>.json`. Each is one
@@ -331,10 +364,11 @@ function listLogEvents(cwd, { limit = 200, before = null, type = 'all' } = {}) {
331
364
  try {
332
365
  files = fs.readdirSync(dir).filter((f) => f.endsWith('.json'));
333
366
  } catch {
334
- return { entries: [], total: 0 };
367
+ return { entries: [], total: 0, thresholds: tokenThresholds() };
335
368
  }
336
369
  files.sort().reverse(); // filenames begin with sortable timestamp; newest first
337
370
  const total = files.length;
371
+ const thresholds = tokenThresholds();
338
372
 
339
373
  let beforeIdx = 0;
340
374
  if (before) {
@@ -353,6 +387,12 @@ function listLogEvents(cwd, { limit = 200, before = null, type = 'all' } = {}) {
353
387
  if (type === 'reminder' && !r.type.startsWith('reminder')) continue;
354
388
  }
355
389
  // Strip large `content` from list view; clients fetch it via /api/logs/:id
390
+ // Old sidecars (pre-token-estimate) lack `contentTokens`. Backfill on
391
+ // the fly using the same chars/4 heuristic — keeps the UI consistent
392
+ // for events captured before the upgrade.
393
+ const tokens = typeof r.contentTokens === 'number'
394
+ ? r.contentTokens
395
+ : (r.contentBytes ? Math.round(r.contentBytes / 4) : 0);
356
396
  entries.push({
357
397
  id: r.id,
358
398
  timestamp: r.timestamp,
@@ -362,10 +402,12 @@ function listLogEvents(cwd, { limit = 200, before = null, type = 'all' } = {}) {
362
402
  handler: r.handler,
363
403
  summary: r.summary,
364
404
  contentBytes: r.contentBytes || 0,
405
+ contentTokens: tokens,
406
+ tokenStatus: classifyTokens(tokens, thresholds),
365
407
  activeReminderIds: r.activeReminderIds || null,
366
408
  });
367
409
  }
368
- return { entries, total };
410
+ return { entries, total, thresholds };
369
411
  }
370
412
 
371
413
  function readLogEvent(cwd, id) {
@@ -379,19 +421,37 @@ function readLogEvent(cwd, id) {
379
421
 
380
422
  function getLogsInfo(cwd) {
381
423
  const dir = logEventsDir(cwd);
424
+ const thresholds = tokenThresholds();
382
425
  let total = 0;
383
426
  let mostRecent = null;
427
+ let latestReminderTokens = null;
428
+ let latestReminderStatus = null;
384
429
  try {
385
430
  const files = fs.readdirSync(dir).filter((f) => f.endsWith('.json')).sort();
386
431
  total = files.length;
387
- if (total > 0) {
432
+ // Walk from newest to find both latest event timestamp and latest reminder.
433
+ for (let i = files.length - 1; i >= 0 && (mostRecent === null || latestReminderTokens === null); i--) {
388
434
  try {
389
- const r = JSON.parse(fs.readFileSync(path.join(dir, files[files.length - 1]), 'utf8'));
390
- mostRecent = r.timestamp;
435
+ const r = JSON.parse(fs.readFileSync(path.join(dir, files[i]), 'utf8'));
436
+ if (mostRecent === null) mostRecent = r.timestamp;
437
+ if (latestReminderTokens === null && r.type && r.type.startsWith('reminder')) {
438
+ const tokens = typeof r.contentTokens === 'number'
439
+ ? r.contentTokens
440
+ : (r.contentBytes ? Math.round(r.contentBytes / 4) : 0);
441
+ latestReminderTokens = tokens;
442
+ latestReminderStatus = classifyTokens(tokens, thresholds);
443
+ }
391
444
  } catch {}
392
445
  }
393
446
  } catch {}
394
- return { available: true, total, mostRecent };
447
+ return {
448
+ available: true,
449
+ total,
450
+ mostRecent,
451
+ thresholds,
452
+ latestReminderTokens,
453
+ latestReminderStatus,
454
+ };
395
455
  }
396
456
 
397
457
  function getDoctorInfo(cwd) {
@@ -539,6 +599,14 @@ details[open] summary { padding-bottom: 8px; border-bottom: 1px solid var(--bord
539
599
  .logs-size { font-family: var(--mono); font-size: 12px; color: var(--muted); white-space: nowrap; }
540
600
  .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
601
  .logs-view:hover { border-color: var(--accent); }
602
+ .logs-table tr.token-light td:first-child { border-left: 3px solid #3fb950; }
603
+ .logs-table tr.token-normal td:first-child { border-left: 3px solid var(--border); }
604
+ .logs-table tr.token-heavy td:first-child { border-left: 3px solid var(--warn); }
605
+ .logs-table tr.token-high td:first-child { border-left: 3px solid var(--err); }
606
+ .token-badge { display: inline-block; padding: 1px 7px; border-radius: 999px; font-size: 10px; font-family: var(--mono); margin-left: 6px; vertical-align: middle; }
607
+ .token-badge.heavy { background: rgba(210,153,34,0.18); color: var(--warn); }
608
+ .token-badge.high { background: rgba(248,81,73,0.20); color: var(--err); }
609
+ .token-badge.light { background: rgba(63,185,80,0.18); color: var(--ok); }
542
610
  #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
611
  #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
612
  #log-drawer .drawer-head .title { font-weight: 600; }
@@ -687,7 +755,12 @@ function severityFor(id) {
687
755
  case 'claudeMd': return !s.claudeMd.exists ? 'err' : (s.claudeMd.optOut ? 'info' : 'ok');
688
756
  case 'hooks': return s.hooks.warnings > 0 ? 'warn' : 'ok';
689
757
  case 'reminders': return s.reminders && s.reminders.disabledCount > 0 ? 'info' : 'ok';
690
- case 'logs': return s.logs && s.logs.total > 0 ? 'ok' : 'info';
758
+ case 'logs': {
759
+ if (!s.logs) return 'info';
760
+ if (s.logs.latestReminderStatus === 'high') return 'err';
761
+ if (s.logs.latestReminderStatus === 'heavy') return 'warn';
762
+ return s.logs.total > 0 ? 'ok' : 'info';
763
+ }
691
764
  case 'mcp': return !s.mcp.configFound ? 'info' : (s.mcp.playwright.match && s.mcp.staleLockfiles.length === 0 ? 'ok' : 'warn');
692
765
  case 'setupContext': return !s.setupContext.exists ? 'err' : (s.setupContext.toolingComplete ? 'ok' : 'warn');
693
766
  case 'activeRun': return s.activeRun.active ? 'info' : 'info';
@@ -760,9 +833,17 @@ function renderOverview() {
760
833
  const ef = s.enforcement && s.enforcement.available
761
834
  ? (s.enforcement.on ? \`ON (\${s.enforcement.count} blocking hooks)\` : \`OFF — \${s.enforcement.count} blocking hooks silenced\`)
762
835
  : 'unavailable';
836
+ const reminderTokens = s.logs && s.logs.latestReminderTokens;
837
+ const reminderStatus = s.logs && s.logs.latestReminderStatus;
763
838
  const lg = s.logs && s.logs.available
764
- ? \`\${s.logs.total} captured\${s.logs.mostRecent ? ' · last ' + fmtRelativeTime(s.logs.mostRecent) : ''}\`
839
+ ? \`\${s.logs.total} captured\${s.logs.mostRecent ? ' · last ' + fmtRelativeTime(s.logs.mostRecent) : ''}\${reminderTokens ? ' · latest reminder ≈' + reminderTokens.toLocaleString() + ' tokens' + (reminderStatus === 'heavy' ? ' ⚠ heavy' : reminderStatus === 'high' ? ' ✗ HIGH' : '') : ''}\`
765
840
  : 'no events';
841
+ const logsKind = (() => {
842
+ if (!s.logs || !s.logs.available) return 'info';
843
+ if (reminderStatus === 'high') return 'err';
844
+ if (reminderStatus === 'heavy') return 'warn';
845
+ return s.logs.total > 0 ? 'ok' : 'info';
846
+ })();
766
847
  return \`<h2>Overview</h2>
767
848
  <p class="sub">Snapshot at \${new Date(s.timestamp).toLocaleTimeString()}</p>
768
849
  <div class="card">
@@ -774,7 +855,7 @@ function renderOverview() {
774
855
  \${row('Active run', ar, { kind: 'info', text: s.activeRun.active ? '·' : '·' })}
775
856
  \${row('Reminders', rm, { kind: s.reminders && s.reminders.available ? (s.reminders.disabledCount ? 'info' : 'ok') : 'err', text: s.reminders && s.reminders.disabledCount ? '·' : '✓' })}
776
857
  \${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: '·' })}
858
+ \${row('Logs', lg, { kind: logsKind, text: logsKind === 'err' ? '' : logsKind === 'warn' ? '!' : '·' })}
778
859
  \${row('Doctor', dc, { kind: s.doctor.issueCount === 0 ? 'ok' : 'warn', text: s.doctor.issueCount === 0 ? '✓' : '!' })}
779
860
  </div>\`;
780
861
  }
@@ -901,8 +982,27 @@ async function viewLogDetail(id) {
901
982
  html += '<h4 style="margin-top:14px">Active reminders at fire time</h4>';
902
983
  html += '<p class="sub">' + ev.activeReminderIds.map(escapeHtml).join(', ') + '</p>';
903
984
  }
904
- if (typeof ev.contentBytes === 'number') {
905
- html += '<h4 style="margin-top:14px">Injected content (' + fmtBytes(ev.contentBytes) + ')</h4>';
985
+ if (typeof ev.contentBytes === 'number' && ev.contentBytes > 0) {
986
+ const tokens = typeof ev.contentTokens === 'number' ? ev.contentTokens : Math.round(ev.contentBytes / 4);
987
+ const th = (state.logs && state.logs.thresholds) || { heavy: 8000, high: 20000 };
988
+ const cls = tokens >= th.high ? 'high' : tokens >= th.heavy ? 'heavy' : tokens < 2000 ? 'light' : 'normal';
989
+ const statusBadge = cls === 'high'
990
+ ? '<span class="token-badge high">⚠ HIGH</span>'
991
+ : cls === 'heavy'
992
+ ? '<span class="token-badge heavy">⚠ HEAVY</span>'
993
+ : '';
994
+ html += '<h4 style="margin-top:14px">Injected content — ≈' + tokens.toLocaleString() + ' tokens ' + statusBadge + ' <span class="muted" style="font-size:11px">(approx · ' + fmtBytes(ev.contentBytes) + ' raw)</span></h4>';
995
+ if (cls === 'heavy' || cls === 'high') {
996
+ const advice = cls === 'high'
997
+ ? 'This reminder exceeds <strong>' + th.high.toLocaleString() + ' tokens</strong> — eats meaningful context budget. Strongly consider:'
998
+ : 'This reminder exceeds <strong>' + th.heavy.toLocaleString() + ' tokens</strong> — heavy. Consider:';
999
+ html += '<div class="card" style="border-color:' + (cls === 'high' ? 'rgba(248,81,73,0.4)' : 'rgba(210,153,34,0.4)') + '"><p>' + advice + '</p>';
1000
+ html += '<ul style="margin:6px 0 0 18px; padding:0">';
1001
+ html += '<li>Trim CLAUDE.md (currently the largest contributor) — see the CLAUDE.md tab</li>';
1002
+ html += '<li>Disable individual reminder IDs you don\\\'t need in the Reminders tab</li>';
1003
+ html += '<li>Tune the threshold via <code>CLAUDEFLOW_TOKEN_HEAVY</code> / <code>CLAUDEFLOW_TOKEN_HIGH</code> env vars if your project genuinely needs more</li>';
1004
+ html += '</ul></div>';
1005
+ }
906
1006
  }
907
1007
  if (typeof ev.content === 'string' && ev.content.length > 0) {
908
1008
  html += '<pre>' + escapeHtml(ev.content) + '</pre>';
@@ -942,17 +1042,30 @@ function renderLogs() {
942
1042
  loadLogsPage();
943
1043
  }
944
1044
  const totalLabel = state.logs && state.logs.total != null ? \`\${state.logs.total} total events\` : '';
1045
+ const thresholds = (state.logs && state.logs.thresholds) || { heavy: 8000, high: 20000 };
945
1046
  const rows = logsState.entries.map((e) => {
946
1047
  const isReminder = e.type.startsWith('reminder');
947
1048
  const badge = isReminder
948
1049
  ? '<span class="badge info" style="background:rgba(88,166,255,0.18); color:#58a6ff">REMINDER</span>'
949
1050
  : '<span class="badge err">ENFORCEMENT</span>';
950
- const sizeStr = e.contentBytes ? fmtBytes(e.contentBytes) : '';
951
- return \`<tr>
1051
+ const tokenStatus = e.tokenStatus || 'none';
1052
+ const tokenBadge = tokenStatus === 'high'
1053
+ ? '<span class="token-badge high" title="High — exceeds the high-water threshold (' + thresholds.high.toLocaleString() + ' tokens). Consider trimming.">⚠ HIGH</span>'
1054
+ : tokenStatus === 'heavy'
1055
+ ? '<span class="token-badge heavy" title="Heavy — exceeds the heavy threshold (' + thresholds.heavy.toLocaleString() + ' tokens). Consider trimming.">⚠ HEAVY</span>'
1056
+ : '';
1057
+ const tokensStr = e.contentTokens
1058
+ ? \`≈\${e.contentTokens.toLocaleString()}\`
1059
+ : '—';
1060
+ const bytesStr = e.contentBytes ? fmtBytes(e.contentBytes) : '';
1061
+ const tokensTitle = e.contentBytes
1062
+ ? \`≈\${e.contentTokens} tokens (chars/4 heuristic) · \${fmtBytes(e.contentBytes)} raw · status: \${tokenStatus}\`
1063
+ : 'No content (enforcement-silenced)';
1064
+ return \`<tr class="token-\${tokenStatus}">
952
1065
  <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
1066
  <td>\${badge}</td>
954
1067
  <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>
1068
+ <td class="logs-size" title="\${escapeHtml(tokensTitle)}">\${tokensStr}\${tokenBadge}\${bytesStr ? \`<br><span class="muted" style="font-size:11px">\${bytesStr}</span>\` : ''}</td>
956
1069
  <td><button class="logs-view" data-log-id="\${escapeHtml(e.id)}">View</button></td>
957
1070
  </tr>\`;
958
1071
  }).join('');
@@ -961,6 +1074,11 @@ function renderLogs() {
961
1074
  : '';
962
1075
  return \`<h2>Logs</h2>
963
1076
  <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>
1077
+ <p class="sub muted" style="font-size:11px">
1078
+ Token-budget thresholds: <strong style="color:var(--warn)">heavy ≥\${thresholds.heavy.toLocaleString()}</strong> · <strong style="color:var(--err)">high ≥\${thresholds.high.toLocaleString()}</strong>.
1079
+ Heuristic — Anthropic doesn't publish official limits for per-turn additionalContext.
1080
+ Override via <code>CLAUDEFLOW_TOKEN_HEAVY</code> / <code>CLAUDEFLOW_TOKEN_HIGH</code> env vars.
1081
+ </p>
964
1082
  <div class="logs-controls">
965
1083
  <label>Filter:
966
1084
  <select id="logs-filter">
@@ -972,7 +1090,7 @@ function renderLogs() {
972
1090
  <button id="logs-reload">Reload</button>
973
1091
  </div>
974
1092
  \${empty || \`<table class="logs-table">
975
- <thead><tr><th>Time</th><th>Type</th><th>Handler</th><th>Size</th><th></th></tr></thead>
1093
+ <thead><tr><th>Time</th><th>Type</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>
976
1094
  <tbody>\${rows}</tbody>
977
1095
  </table>\`}
978
1096
  \${logsState.allLoaded || logsState.entries.length === 0 ? '' : '<div style="margin-top:12px"><button id="logs-load-more">Load older</button></div>'}\`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axiomatic-labs/claudeflow",
3
- "version": "2.13.29",
3
+ "version": "2.13.31",
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"