@axiomatic-labs/claudeflow 2.13.30 → 2.13.32
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 +132 -14
- 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,14 @@ 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,
|
|
408
|
+
capturedFromStdout: r.capturedFromStdout === true,
|
|
409
|
+
stdoutTruncated: r.stdoutTruncated === true,
|
|
410
|
+
parseError: r.parseError || null,
|
|
373
411
|
});
|
|
374
412
|
}
|
|
375
|
-
return { entries, total };
|
|
413
|
+
return { entries, total, thresholds };
|
|
376
414
|
}
|
|
377
415
|
|
|
378
416
|
function readLogEvent(cwd, id) {
|
|
@@ -386,19 +424,37 @@ function readLogEvent(cwd, id) {
|
|
|
386
424
|
|
|
387
425
|
function getLogsInfo(cwd) {
|
|
388
426
|
const dir = logEventsDir(cwd);
|
|
427
|
+
const thresholds = tokenThresholds();
|
|
389
428
|
let total = 0;
|
|
390
429
|
let mostRecent = null;
|
|
430
|
+
let latestReminderTokens = null;
|
|
431
|
+
let latestReminderStatus = null;
|
|
391
432
|
try {
|
|
392
433
|
const files = fs.readdirSync(dir).filter((f) => f.endsWith('.json')).sort();
|
|
393
434
|
total = files.length;
|
|
394
|
-
|
|
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--) {
|
|
395
437
|
try {
|
|
396
|
-
const r = JSON.parse(fs.readFileSync(path.join(dir, files[
|
|
397
|
-
mostRecent = r.timestamp;
|
|
438
|
+
const r = JSON.parse(fs.readFileSync(path.join(dir, files[i]), 'utf8'));
|
|
439
|
+
if (mostRecent === null) mostRecent = r.timestamp;
|
|
440
|
+
if (latestReminderTokens === null && r.type && r.type.startsWith('reminder')) {
|
|
441
|
+
const tokens = typeof r.contentTokens === 'number'
|
|
442
|
+
? r.contentTokens
|
|
443
|
+
: (r.contentBytes ? Math.round(r.contentBytes / 4) : 0);
|
|
444
|
+
latestReminderTokens = tokens;
|
|
445
|
+
latestReminderStatus = classifyTokens(tokens, thresholds);
|
|
446
|
+
}
|
|
398
447
|
} catch {}
|
|
399
448
|
}
|
|
400
449
|
} catch {}
|
|
401
|
-
return {
|
|
450
|
+
return {
|
|
451
|
+
available: true,
|
|
452
|
+
total,
|
|
453
|
+
mostRecent,
|
|
454
|
+
thresholds,
|
|
455
|
+
latestReminderTokens,
|
|
456
|
+
latestReminderStatus,
|
|
457
|
+
};
|
|
402
458
|
}
|
|
403
459
|
|
|
404
460
|
function getDoctorInfo(cwd) {
|
|
@@ -546,6 +602,14 @@ details[open] summary { padding-bottom: 8px; border-bottom: 1px solid var(--bord
|
|
|
546
602
|
.logs-size { font-family: var(--mono); font-size: 12px; color: var(--muted); white-space: nowrap; }
|
|
547
603
|
.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
604
|
.logs-view:hover { border-color: var(--accent); }
|
|
605
|
+
.logs-table tr.token-light td:first-child { border-left: 3px solid #3fb950; }
|
|
606
|
+
.logs-table tr.token-normal td:first-child { border-left: 3px solid var(--border); }
|
|
607
|
+
.logs-table tr.token-heavy td:first-child { border-left: 3px solid var(--warn); }
|
|
608
|
+
.logs-table tr.token-high td:first-child { border-left: 3px solid var(--err); }
|
|
609
|
+
.token-badge { display: inline-block; padding: 1px 7px; border-radius: 999px; font-size: 10px; font-family: var(--mono); margin-left: 6px; vertical-align: middle; }
|
|
610
|
+
.token-badge.heavy { background: rgba(210,153,34,0.18); color: var(--warn); }
|
|
611
|
+
.token-badge.high { background: rgba(248,81,73,0.20); color: var(--err); }
|
|
612
|
+
.token-badge.light { background: rgba(63,185,80,0.18); color: var(--ok); }
|
|
549
613
|
#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
614
|
#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
615
|
#log-drawer .drawer-head .title { font-weight: 600; }
|
|
@@ -694,7 +758,12 @@ function severityFor(id) {
|
|
|
694
758
|
case 'claudeMd': return !s.claudeMd.exists ? 'err' : (s.claudeMd.optOut ? 'info' : 'ok');
|
|
695
759
|
case 'hooks': return s.hooks.warnings > 0 ? 'warn' : 'ok';
|
|
696
760
|
case 'reminders': return s.reminders && s.reminders.disabledCount > 0 ? 'info' : 'ok';
|
|
697
|
-
case 'logs':
|
|
761
|
+
case 'logs': {
|
|
762
|
+
if (!s.logs) return 'info';
|
|
763
|
+
if (s.logs.latestReminderStatus === 'high') return 'err';
|
|
764
|
+
if (s.logs.latestReminderStatus === 'heavy') return 'warn';
|
|
765
|
+
return s.logs.total > 0 ? 'ok' : 'info';
|
|
766
|
+
}
|
|
698
767
|
case 'mcp': return !s.mcp.configFound ? 'info' : (s.mcp.playwright.match && s.mcp.staleLockfiles.length === 0 ? 'ok' : 'warn');
|
|
699
768
|
case 'setupContext': return !s.setupContext.exists ? 'err' : (s.setupContext.toolingComplete ? 'ok' : 'warn');
|
|
700
769
|
case 'activeRun': return s.activeRun.active ? 'info' : 'info';
|
|
@@ -767,9 +836,17 @@ function renderOverview() {
|
|
|
767
836
|
const ef = s.enforcement && s.enforcement.available
|
|
768
837
|
? (s.enforcement.on ? \`ON (\${s.enforcement.count} blocking hooks)\` : \`OFF — \${s.enforcement.count} blocking hooks silenced\`)
|
|
769
838
|
: 'unavailable';
|
|
839
|
+
const reminderTokens = s.logs && s.logs.latestReminderTokens;
|
|
840
|
+
const reminderStatus = s.logs && s.logs.latestReminderStatus;
|
|
770
841
|
const lg = s.logs && s.logs.available
|
|
771
|
-
? \`\${s.logs.total} captured\${s.logs.mostRecent ? ' · last ' + fmtRelativeTime(s.logs.mostRecent) : ''}\`
|
|
842
|
+
? \`\${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
843
|
: 'no events';
|
|
844
|
+
const logsKind = (() => {
|
|
845
|
+
if (!s.logs || !s.logs.available) return 'info';
|
|
846
|
+
if (reminderStatus === 'high') return 'err';
|
|
847
|
+
if (reminderStatus === 'heavy') return 'warn';
|
|
848
|
+
return s.logs.total > 0 ? 'ok' : 'info';
|
|
849
|
+
})();
|
|
773
850
|
return \`<h2>Overview</h2>
|
|
774
851
|
<p class="sub">Snapshot at \${new Date(s.timestamp).toLocaleTimeString()}</p>
|
|
775
852
|
<div class="card">
|
|
@@ -781,7 +858,7 @@ function renderOverview() {
|
|
|
781
858
|
\${row('Active run', ar, { kind: 'info', text: s.activeRun.active ? '·' : '·' })}
|
|
782
859
|
\${row('Reminders', rm, { kind: s.reminders && s.reminders.available ? (s.reminders.disabledCount ? 'info' : 'ok') : 'err', text: s.reminders && s.reminders.disabledCount ? '·' : '✓' })}
|
|
783
860
|
\${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:
|
|
861
|
+
\${row('Logs', lg, { kind: logsKind, text: logsKind === 'err' ? '✗' : logsKind === 'warn' ? '!' : '·' })}
|
|
785
862
|
\${row('Doctor', dc, { kind: s.doctor.issueCount === 0 ? 'ok' : 'warn', text: s.doctor.issueCount === 0 ? '✓' : '!' })}
|
|
786
863
|
</div>\`;
|
|
787
864
|
}
|
|
@@ -908,9 +985,38 @@ async function viewLogDetail(id) {
|
|
|
908
985
|
html += '<h4 style="margin-top:14px">Active reminders at fire time</h4>';
|
|
909
986
|
html += '<p class="sub">' + ev.activeReminderIds.map(escapeHtml).join(', ') + '</p>';
|
|
910
987
|
}
|
|
988
|
+
// Provenance: did we authentically capture the hook's stdout, or is the
|
|
989
|
+
// record empty / parsed-failed / truncated? Make this visible at the top
|
|
990
|
+
// of the drawer so the user can trust (or distrust) the content shown.
|
|
991
|
+
if (typeof ev.capturedFromStdout === 'boolean') {
|
|
992
|
+
const provBadge = ev.capturedFromStdout
|
|
993
|
+
? '<span class="badge ok" title="Bytes captured directly from the hook's stdout — this is the actual additionalContext that Claude Code received.">✓ stdout-captured</span>'
|
|
994
|
+
: (ev.parseError
|
|
995
|
+
? '<span class="badge err" title="Hook stdout was captured but JSON.parse failed: ' + escapeHtml(ev.parseError) + '">✗ parse failed</span>'
|
|
996
|
+
: '<span class="badge warn" title="No content was captured — the hook may have crashed before writing or returned null.">⚠ no stdout</span>');
|
|
997
|
+
html += '<h4 style="margin-top:14px">Provenance ' + provBadge + (ev.stdoutTruncated ? ' <span class="badge warn">⚠ truncated</span>' : '') + '</h4>';
|
|
998
|
+
}
|
|
911
999
|
if (typeof ev.contentBytes === 'number' && ev.contentBytes > 0) {
|
|
912
1000
|
const tokens = typeof ev.contentTokens === 'number' ? ev.contentTokens : Math.round(ev.contentBytes / 4);
|
|
913
|
-
|
|
1001
|
+
const th = (state.logs && state.logs.thresholds) || { heavy: 8000, high: 20000 };
|
|
1002
|
+
const cls = tokens >= th.high ? 'high' : tokens >= th.heavy ? 'heavy' : tokens < 2000 ? 'light' : 'normal';
|
|
1003
|
+
const statusBadge = cls === 'high'
|
|
1004
|
+
? '<span class="token-badge high">⚠ HIGH</span>'
|
|
1005
|
+
: cls === 'heavy'
|
|
1006
|
+
? '<span class="token-badge heavy">⚠ HEAVY</span>'
|
|
1007
|
+
: '';
|
|
1008
|
+
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>';
|
|
1009
|
+
if (cls === 'heavy' || cls === 'high') {
|
|
1010
|
+
const advice = cls === 'high'
|
|
1011
|
+
? 'This reminder exceeds <strong>' + th.high.toLocaleString() + ' tokens</strong> — eats meaningful context budget. Strongly consider:'
|
|
1012
|
+
: 'This reminder exceeds <strong>' + th.heavy.toLocaleString() + ' tokens</strong> — heavy. Consider:';
|
|
1013
|
+
html += '<div class="card" style="border-color:' + (cls === 'high' ? 'rgba(248,81,73,0.4)' : 'rgba(210,153,34,0.4)') + '"><p>' + advice + '</p>';
|
|
1014
|
+
html += '<ul style="margin:6px 0 0 18px; padding:0">';
|
|
1015
|
+
html += '<li>Trim CLAUDE.md (currently the largest contributor) — see the CLAUDE.md tab</li>';
|
|
1016
|
+
html += '<li>Disable individual reminder IDs you don\\\'t need in the Reminders tab</li>';
|
|
1017
|
+
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>';
|
|
1018
|
+
html += '</ul></div>';
|
|
1019
|
+
}
|
|
914
1020
|
}
|
|
915
1021
|
if (typeof ev.content === 'string' && ev.content.length > 0) {
|
|
916
1022
|
html += '<pre>' + escapeHtml(ev.content) + '</pre>';
|
|
@@ -950,23 +1056,30 @@ function renderLogs() {
|
|
|
950
1056
|
loadLogsPage();
|
|
951
1057
|
}
|
|
952
1058
|
const totalLabel = state.logs && state.logs.total != null ? \`\${state.logs.total} total events\` : '';
|
|
1059
|
+
const thresholds = (state.logs && state.logs.thresholds) || { heavy: 8000, high: 20000 };
|
|
953
1060
|
const rows = logsState.entries.map((e) => {
|
|
954
1061
|
const isReminder = e.type.startsWith('reminder');
|
|
955
1062
|
const badge = isReminder
|
|
956
1063
|
? '<span class="badge info" style="background:rgba(88,166,255,0.18); color:#58a6ff">REMINDER</span>'
|
|
957
1064
|
: '<span class="badge err">ENFORCEMENT</span>';
|
|
1065
|
+
const tokenStatus = e.tokenStatus || 'none';
|
|
1066
|
+
const tokenBadge = tokenStatus === 'high'
|
|
1067
|
+
? '<span class="token-badge high" title="High — exceeds the high-water threshold (' + thresholds.high.toLocaleString() + ' tokens). Consider trimming.">⚠ HIGH</span>'
|
|
1068
|
+
: tokenStatus === 'heavy'
|
|
1069
|
+
? '<span class="token-badge heavy" title="Heavy — exceeds the heavy threshold (' + thresholds.heavy.toLocaleString() + ' tokens). Consider trimming.">⚠ HEAVY</span>'
|
|
1070
|
+
: '';
|
|
958
1071
|
const tokensStr = e.contentTokens
|
|
959
1072
|
? \`≈\${e.contentTokens.toLocaleString()}\`
|
|
960
1073
|
: '—';
|
|
961
1074
|
const bytesStr = e.contentBytes ? fmtBytes(e.contentBytes) : '';
|
|
962
1075
|
const tokensTitle = e.contentBytes
|
|
963
|
-
? \`≈\${e.contentTokens} tokens (
|
|
1076
|
+
? \`≈\${e.contentTokens} tokens (chars/4 heuristic) · \${fmtBytes(e.contentBytes)} raw · status: \${tokenStatus}\`
|
|
964
1077
|
: 'No content (enforcement-silenced)';
|
|
965
|
-
return \`<tr>
|
|
1078
|
+
return \`<tr class="token-\${tokenStatus}">
|
|
966
1079
|
<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
1080
|
<td>\${badge}</td>
|
|
968
1081
|
<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>
|
|
1082
|
+
<td class="logs-size" title="\${escapeHtml(tokensTitle)}">\${tokensStr}\${tokenBadge}\${bytesStr ? \`<br><span class="muted" style="font-size:11px">\${bytesStr}</span>\` : ''}</td>
|
|
970
1083
|
<td><button class="logs-view" data-log-id="\${escapeHtml(e.id)}">View</button></td>
|
|
971
1084
|
</tr>\`;
|
|
972
1085
|
}).join('');
|
|
@@ -975,6 +1088,11 @@ function renderLogs() {
|
|
|
975
1088
|
: '';
|
|
976
1089
|
return \`<h2>Logs</h2>
|
|
977
1090
|
<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>
|
|
1091
|
+
<p class="sub muted" style="font-size:11px">
|
|
1092
|
+
Token-budget thresholds: <strong style="color:var(--warn)">heavy ≥\${thresholds.heavy.toLocaleString()}</strong> · <strong style="color:var(--err)">high ≥\${thresholds.high.toLocaleString()}</strong>.
|
|
1093
|
+
Heuristic — Anthropic doesn't publish official limits for per-turn additionalContext.
|
|
1094
|
+
Override via <code>CLAUDEFLOW_TOKEN_HEAVY</code> / <code>CLAUDEFLOW_TOKEN_HIGH</code> env vars.
|
|
1095
|
+
</p>
|
|
978
1096
|
<div class="logs-controls">
|
|
979
1097
|
<label>Filter:
|
|
980
1098
|
<select id="logs-filter">
|
|
@@ -986,7 +1104,7 @@ function renderLogs() {
|
|
|
986
1104
|
<button id="logs-reload">Reload</button>
|
|
987
1105
|
</div>
|
|
988
1106
|
\${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
|
|
1107
|
+
<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
1108
|
<tbody>\${rows}</tbody>
|
|
991
1109
|
</table>\`}
|
|
992
1110
|
\${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.
|
|
3
|
+
"version": "2.13.32",
|
|
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"
|