@axiomatic-labs/claudeflow 2.13.30 → 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 +118 -14
  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) {
@@ -369,10 +403,11 @@ function listLogEvents(cwd, { limit = 200, before = null, type = 'all' } = {}) {
369
403
  summary: r.summary,
370
404
  contentBytes: r.contentBytes || 0,
371
405
  contentTokens: tokens,
406
+ tokenStatus: classifyTokens(tokens, thresholds),
372
407
  activeReminderIds: r.activeReminderIds || null,
373
408
  });
374
409
  }
375
- return { entries, total };
410
+ return { entries, total, thresholds };
376
411
  }
377
412
 
378
413
  function readLogEvent(cwd, id) {
@@ -386,19 +421,37 @@ function readLogEvent(cwd, id) {
386
421
 
387
422
  function getLogsInfo(cwd) {
388
423
  const dir = logEventsDir(cwd);
424
+ const thresholds = tokenThresholds();
389
425
  let total = 0;
390
426
  let mostRecent = null;
427
+ let latestReminderTokens = null;
428
+ let latestReminderStatus = null;
391
429
  try {
392
430
  const files = fs.readdirSync(dir).filter((f) => f.endsWith('.json')).sort();
393
431
  total = files.length;
394
- 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--) {
395
434
  try {
396
- const r = JSON.parse(fs.readFileSync(path.join(dir, files[files.length - 1]), 'utf8'));
397
- 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
+ }
398
444
  } catch {}
399
445
  }
400
446
  } catch {}
401
- return { available: true, total, mostRecent };
447
+ return {
448
+ available: true,
449
+ total,
450
+ mostRecent,
451
+ thresholds,
452
+ latestReminderTokens,
453
+ latestReminderStatus,
454
+ };
402
455
  }
403
456
 
404
457
  function getDoctorInfo(cwd) {
@@ -546,6 +599,14 @@ details[open] summary { padding-bottom: 8px; border-bottom: 1px solid var(--bord
546
599
  .logs-size { font-family: var(--mono); font-size: 12px; color: var(--muted); white-space: nowrap; }
547
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; }
548
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); }
549
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; }
550
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); }
551
612
  #log-drawer .drawer-head .title { font-weight: 600; }
@@ -694,7 +755,12 @@ function severityFor(id) {
694
755
  case 'claudeMd': return !s.claudeMd.exists ? 'err' : (s.claudeMd.optOut ? 'info' : 'ok');
695
756
  case 'hooks': return s.hooks.warnings > 0 ? 'warn' : 'ok';
696
757
  case 'reminders': return s.reminders && s.reminders.disabledCount > 0 ? 'info' : 'ok';
697
- 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
+ }
698
764
  case 'mcp': return !s.mcp.configFound ? 'info' : (s.mcp.playwright.match && s.mcp.staleLockfiles.length === 0 ? 'ok' : 'warn');
699
765
  case 'setupContext': return !s.setupContext.exists ? 'err' : (s.setupContext.toolingComplete ? 'ok' : 'warn');
700
766
  case 'activeRun': return s.activeRun.active ? 'info' : 'info';
@@ -767,9 +833,17 @@ function renderOverview() {
767
833
  const ef = s.enforcement && s.enforcement.available
768
834
  ? (s.enforcement.on ? \`ON (\${s.enforcement.count} blocking hooks)\` : \`OFF — \${s.enforcement.count} blocking hooks silenced\`)
769
835
  : 'unavailable';
836
+ const reminderTokens = s.logs && s.logs.latestReminderTokens;
837
+ const reminderStatus = s.logs && s.logs.latestReminderStatus;
770
838
  const lg = s.logs && s.logs.available
771
- ? \`\${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' : '') : ''}\`
772
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
+ })();
773
847
  return \`<h2>Overview</h2>
774
848
  <p class="sub">Snapshot at \${new Date(s.timestamp).toLocaleTimeString()}</p>
775
849
  <div class="card">
@@ -781,7 +855,7 @@ function renderOverview() {
781
855
  \${row('Active run', ar, { kind: 'info', text: s.activeRun.active ? '·' : '·' })}
782
856
  \${row('Reminders', rm, { kind: s.reminders && s.reminders.available ? (s.reminders.disabledCount ? 'info' : 'ok') : 'err', text: s.reminders && s.reminders.disabledCount ? '·' : '✓' })}
783
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 ? '✓' : '✗') : '·' })}
784
- \${row('Logs', lg, { kind: s.logs && s.logs.total > 0 ? 'ok' : 'info', text: '·' })}
858
+ \${row('Logs', lg, { kind: logsKind, text: logsKind === 'err' ? '' : logsKind === 'warn' ? '!' : '·' })}
785
859
  \${row('Doctor', dc, { kind: s.doctor.issueCount === 0 ? 'ok' : 'warn', text: s.doctor.issueCount === 0 ? '✓' : '!' })}
786
860
  </div>\`;
787
861
  }
@@ -910,7 +984,25 @@ async function viewLogDetail(id) {
910
984
  }
911
985
  if (typeof ev.contentBytes === 'number' && ev.contentBytes > 0) {
912
986
  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>';
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
+ }
914
1006
  }
915
1007
  if (typeof ev.content === 'string' && ev.content.length > 0) {
916
1008
  html += '<pre>' + escapeHtml(ev.content) + '</pre>';
@@ -950,23 +1042,30 @@ function renderLogs() {
950
1042
  loadLogsPage();
951
1043
  }
952
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 };
953
1046
  const rows = logsState.entries.map((e) => {
954
1047
  const isReminder = e.type.startsWith('reminder');
955
1048
  const badge = isReminder
956
1049
  ? '<span class="badge info" style="background:rgba(88,166,255,0.18); color:#58a6ff">REMINDER</span>'
957
1050
  : '<span class="badge err">ENFORCEMENT</span>';
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
+ : '';
958
1057
  const tokensStr = e.contentTokens
959
1058
  ? \`≈\${e.contentTokens.toLocaleString()}\`
960
1059
  : '—';
961
1060
  const bytesStr = e.contentBytes ? fmtBytes(e.contentBytes) : '';
962
1061
  const tokensTitle = e.contentBytes
963
- ? \`≈\${e.contentTokens} tokens (approx, chars/4 heuristic) · \${fmtBytes(e.contentBytes)} raw\`
1062
+ ? \`≈\${e.contentTokens} tokens (chars/4 heuristic) · \${fmtBytes(e.contentBytes)} raw · status: \${tokenStatus}\`
964
1063
  : 'No content (enforcement-silenced)';
965
- return \`<tr>
1064
+ return \`<tr class="token-\${tokenStatus}">
966
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>
967
1066
  <td>\${badge}</td>
968
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>
969
- <td class="logs-size" title="\${escapeHtml(tokensTitle)}">\${tokensStr}\${bytesStr ? \`<br><span class="muted" style="font-size:11px">\${bytesStr}</span>\` : ''}</td>
1068
+ <td class="logs-size" title="\${escapeHtml(tokensTitle)}">\${tokensStr}\${tokenBadge}\${bytesStr ? \`<br><span class="muted" style="font-size:11px">\${bytesStr}</span>\` : ''}</td>
970
1069
  <td><button class="logs-view" data-log-id="\${escapeHtml(e.id)}">View</button></td>
971
1070
  </tr>\`;
972
1071
  }).join('');
@@ -975,6 +1074,11 @@ function renderLogs() {
975
1074
  : '';
976
1075
  return \`<h2>Logs</h2>
977
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>
978
1082
  <div class="logs-controls">
979
1083
  <label>Filter:
980
1084
  <select id="logs-filter">
@@ -986,7 +1090,7 @@ function renderLogs() {
986
1090
  <button id="logs-reload">Reload</button>
987
1091
  </div>
988
1092
  \${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>
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>
990
1094
  <tbody>\${rows}</tbody>
991
1095
  </table>\`}
992
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.30",
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"