@axiomatic-labs/claudeflow 2.13.34 → 2.13.36
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 +144 -9
- package/package.json +1 -1
package/lib/panel.js
CHANGED
|
@@ -286,6 +286,30 @@ function getEnforcementInfo(cwd) {
|
|
|
286
286
|
};
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
+
function getActivePlanInfo(cwd) {
|
|
290
|
+
const p = path.join(cwd, '.claudeflow', 'state', 'active-plan.json');
|
|
291
|
+
let raw;
|
|
292
|
+
try { raw = fs.readFileSync(p, 'utf8'); }
|
|
293
|
+
catch { return { available: false }; }
|
|
294
|
+
let record;
|
|
295
|
+
try { record = JSON.parse(raw); }
|
|
296
|
+
catch (err) { return { available: false, error: err.message }; }
|
|
297
|
+
const captured = record.captured_at ? Date.parse(record.captured_at) : null;
|
|
298
|
+
const ageSeconds = captured ? Math.floor((Date.now() - captured) / 1000) : null;
|
|
299
|
+
const content = typeof record.content === 'string' ? record.content : '';
|
|
300
|
+
return {
|
|
301
|
+
available: true,
|
|
302
|
+
path: record.path || null,
|
|
303
|
+
source: record.source || null,
|
|
304
|
+
sessionId: record.session_id || null,
|
|
305
|
+
capturedAt: record.captured_at || null,
|
|
306
|
+
ageSeconds,
|
|
307
|
+
sha256: record.sha256 || null,
|
|
308
|
+
contentBytes: record.contentBytes || content.length,
|
|
309
|
+
summaryPreview: content.slice(0, 240).replace(/\s+/g, ' ').trim(),
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
289
313
|
function getRemindersInfo(cwd) {
|
|
290
314
|
const helpers = loadReminderHelpers(cwd);
|
|
291
315
|
if (!helpers || typeof helpers.listReminderCatalog !== 'function') {
|
|
@@ -385,6 +409,7 @@ function listLogEvents(cwd, { limit = 200, before = null, type = 'all' } = {}) {
|
|
|
385
409
|
if (type !== 'all') {
|
|
386
410
|
if (type === 'enforcement' && !r.type.startsWith('enforcement')) continue;
|
|
387
411
|
if (type === 'reminder' && !r.type.startsWith('reminder')) continue;
|
|
412
|
+
if (type === 'blocked' && r.type !== 'enforcement-blocked') continue;
|
|
388
413
|
}
|
|
389
414
|
// Strip large `content` from list view; clients fetch it via /api/logs/:id
|
|
390
415
|
// Old sidecars (pre-token-estimate) lack `contentTokens`. Backfill on
|
|
@@ -408,6 +433,8 @@ function listLogEvents(cwd, { limit = 200, before = null, type = 'all' } = {}) {
|
|
|
408
433
|
capturedFromStdout: r.capturedFromStdout === true,
|
|
409
434
|
stdoutTruncated: r.stdoutTruncated === true,
|
|
410
435
|
parseError: r.parseError || null,
|
|
436
|
+
decision: r.decision || null,
|
|
437
|
+
reason: r.reason || null,
|
|
411
438
|
});
|
|
412
439
|
}
|
|
413
440
|
return { entries, total, thresholds };
|
|
@@ -452,11 +479,16 @@ function getLogsInfo(cwd) {
|
|
|
452
479
|
let mostRecent = null;
|
|
453
480
|
let latestReminderTokens = null;
|
|
454
481
|
let latestReminderStatus = null;
|
|
482
|
+
let recentBlockedCount = 0;
|
|
455
483
|
try {
|
|
456
484
|
const files = fs.readdirSync(dir).filter((f) => f.endsWith('.json')).sort();
|
|
457
485
|
total = files.length;
|
|
458
|
-
// Walk from newest
|
|
459
|
-
|
|
486
|
+
// Walk from newest. Count blocks within the last 50 events for the
|
|
487
|
+
// sidebar severity. Latest-reminder lookup short-circuits when both
|
|
488
|
+
// mostRecent + latestReminderTokens are populated AND we've scanned
|
|
489
|
+
// enough for the block tally.
|
|
490
|
+
const scanCap = Math.min(50, files.length);
|
|
491
|
+
for (let i = files.length - 1; i >= files.length - scanCap; i--) {
|
|
460
492
|
try {
|
|
461
493
|
const r = JSON.parse(fs.readFileSync(path.join(dir, files[i]), 'utf8'));
|
|
462
494
|
if (mostRecent === null) mostRecent = r.timestamp;
|
|
@@ -467,6 +499,7 @@ function getLogsInfo(cwd) {
|
|
|
467
499
|
latestReminderTokens = tokens;
|
|
468
500
|
latestReminderStatus = classifyTokens(tokens, thresholds);
|
|
469
501
|
}
|
|
502
|
+
if (r.type === 'enforcement-blocked') recentBlockedCount++;
|
|
470
503
|
} catch {}
|
|
471
504
|
}
|
|
472
505
|
} catch {}
|
|
@@ -477,6 +510,7 @@ function getLogsInfo(cwd) {
|
|
|
477
510
|
thresholds,
|
|
478
511
|
latestReminderTokens,
|
|
479
512
|
latestReminderStatus,
|
|
513
|
+
recentBlockedCount,
|
|
480
514
|
};
|
|
481
515
|
}
|
|
482
516
|
|
|
@@ -502,6 +536,7 @@ function collectStatus(cwd) {
|
|
|
502
536
|
activeRun: getActiveRunInfo(cwd),
|
|
503
537
|
reminders: getRemindersInfo(cwd),
|
|
504
538
|
enforcement: getEnforcementInfo(cwd),
|
|
539
|
+
activePlan: getActivePlanInfo(cwd),
|
|
505
540
|
logs: getLogsInfo(cwd),
|
|
506
541
|
doctor: getDoctorInfo(cwd),
|
|
507
542
|
};
|
|
@@ -638,6 +673,10 @@ details[open] summary { padding-bottom: 8px; border-bottom: 1px solid var(--bord
|
|
|
638
673
|
.logs-status { font-family: var(--mono); font-size: 12px; white-space: nowrap; }
|
|
639
674
|
.logs-status.injected { color: #58a6ff; }
|
|
640
675
|
.logs-status.silenced { color: var(--err); font-weight: 600; }
|
|
676
|
+
.logs-status.blocked { color: var(--err); font-weight: 700; }
|
|
677
|
+
.logs-table tr.blocked .logs-handler { color: var(--err); }
|
|
678
|
+
.logs-table tr.blocked td:first-child { border-left: 3px solid var(--err) !important; }
|
|
679
|
+
.logs-reason-preview { font-size: 11px; color: var(--err); margin-top: 2px; max-width: 480px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
641
680
|
.token-badge { display: inline-block; padding: 1px 7px; border-radius: 999px; font-size: 10px; font-family: var(--mono); margin-left: 6px; vertical-align: middle; }
|
|
642
681
|
.token-badge.heavy { background: rgba(210,153,34,0.18); color: var(--warn); }
|
|
643
682
|
.token-badge.high { background: rgba(248,81,73,0.20); color: var(--err); }
|
|
@@ -689,6 +728,7 @@ const SECTIONS = [
|
|
|
689
728
|
{ id: 'hooks', label: 'Hooks' },
|
|
690
729
|
{ id: 'reminders', label: 'Reminders' },
|
|
691
730
|
{ id: 'logs', label: 'Logs' },
|
|
731
|
+
{ id: 'activePlan', label: 'Active plan' },
|
|
692
732
|
{ id: 'mcp', label: 'MCP & observer' },
|
|
693
733
|
{ id: 'setupContext', label: 'Setup context' },
|
|
694
734
|
{ id: 'activeRun', label: 'Active run' },
|
|
@@ -746,6 +786,7 @@ async function loadClaudeMdPreview() {
|
|
|
746
786
|
|
|
747
787
|
async function refresh() {
|
|
748
788
|
const open = captureOpenDetails();
|
|
789
|
+
const prevPlanSha = state && state.activePlan && state.activePlan.sha256;
|
|
749
790
|
try {
|
|
750
791
|
const r = await fetch('/api/status');
|
|
751
792
|
state = await r.json();
|
|
@@ -753,12 +794,16 @@ async function refresh() {
|
|
|
753
794
|
showToast && showToast('Refresh failed: ' + e.message, 'err');
|
|
754
795
|
return;
|
|
755
796
|
}
|
|
797
|
+
// Invalidate cached active-plan body if the underlying sha256 changed.
|
|
798
|
+
const newPlanSha = state && state.activePlan && state.activePlan.sha256;
|
|
799
|
+
if (newPlanSha !== prevPlanSha) activePlanCache = null;
|
|
756
800
|
renderHeader();
|
|
757
801
|
renderNav();
|
|
758
802
|
renderContent();
|
|
759
803
|
restoreOpenDetails(open);
|
|
760
804
|
// If preview was visible/open, ensure it stays loaded after re-render.
|
|
761
805
|
if (open.has('claude-md-preview')) loadClaudeMdPreview();
|
|
806
|
+
if (open.has('active-plan-full')) loadActivePlanPreview();
|
|
762
807
|
// Refresh logs entries silently on the Logs tab so new events appear.
|
|
763
808
|
if (active === 'logs') loadLogsPage();
|
|
764
809
|
}
|
|
@@ -790,8 +835,10 @@ function severityFor(id) {
|
|
|
790
835
|
case 'claudeMd': return !s.claudeMd.exists ? 'err' : (s.claudeMd.optOut ? 'info' : 'ok');
|
|
791
836
|
case 'hooks': return s.hooks.warnings > 0 ? 'warn' : 'ok';
|
|
792
837
|
case 'reminders': return s.reminders && s.reminders.disabledCount > 0 ? 'info' : 'ok';
|
|
838
|
+
case 'activePlan': return s.activePlan && s.activePlan.available ? 'info' : 'info';
|
|
793
839
|
case 'logs': {
|
|
794
840
|
if (!s.logs) return 'info';
|
|
841
|
+
if (s.logs.recentBlockedCount > 0) return 'err';
|
|
795
842
|
if (s.logs.latestReminderStatus === 'high') return 'err';
|
|
796
843
|
if (s.logs.latestReminderStatus === 'heavy') return 'warn';
|
|
797
844
|
return s.logs.total > 0 ? 'ok' : 'info';
|
|
@@ -841,6 +888,7 @@ function renderContent() {
|
|
|
841
888
|
hooks: renderHooks,
|
|
842
889
|
reminders: renderReminders,
|
|
843
890
|
logs: renderLogs,
|
|
891
|
+
activePlan: renderActivePlan,
|
|
844
892
|
mcp: renderMcp,
|
|
845
893
|
setupContext: renderSetup,
|
|
846
894
|
activeRun: renderRun,
|
|
@@ -868,6 +916,9 @@ function renderOverview() {
|
|
|
868
916
|
const ef = s.enforcement && s.enforcement.available
|
|
869
917
|
? (s.enforcement.on ? \`ON (\${s.enforcement.count} blocking hooks)\` : \`OFF — \${s.enforcement.count} blocking hooks silenced\`)
|
|
870
918
|
: 'unavailable';
|
|
919
|
+
const activePlanRow = s.activePlan && s.activePlan.available
|
|
920
|
+
? \`\${s.activePlan.path || '(inline)'} · approved \${s.activePlan.ageSeconds == null ? 'unknown' : (s.activePlan.ageSeconds < 60 ? s.activePlan.ageSeconds + 's' : Math.floor(s.activePlan.ageSeconds / 60) + 'm')} ago · \${fmtBytes(s.activePlan.contentBytes || 0)}\`
|
|
921
|
+
: 'none';
|
|
871
922
|
const reminderTokens = s.logs && s.logs.latestReminderTokens;
|
|
872
923
|
const reminderStatus = s.logs && s.logs.latestReminderStatus;
|
|
873
924
|
const lg = s.logs && s.logs.available
|
|
@@ -890,6 +941,7 @@ function renderOverview() {
|
|
|
890
941
|
\${row('Active run', ar, { kind: 'info', text: s.activeRun.active ? '·' : '·' })}
|
|
891
942
|
\${row('Reminders', rm, { kind: s.reminders && s.reminders.available ? (s.reminders.disabledCount ? 'info' : 'ok') : 'err', text: s.reminders && s.reminders.disabledCount ? '·' : '✓' })}
|
|
892
943
|
\${row('Enforcement', ef, { kind: s.enforcement && s.enforcement.available ? (s.enforcement.on ? 'ok' : 'err') : 'err', text: s.enforcement && s.enforcement.available ? (s.enforcement.on ? '✓' : '✗') : '·' })}
|
|
944
|
+
\${row('Active plan', activePlanRow, { kind: s.activePlan && s.activePlan.available ? 'info' : 'info', text: '·' })}
|
|
893
945
|
\${row('Logs', lg, { kind: logsKind, text: logsKind === 'err' ? '✗' : logsKind === 'warn' ? '!' : '·' })}
|
|
894
946
|
\${row('Doctor', dc, { kind: s.doctor.issueCount === 0 ? 'ok' : 'warn', text: s.doctor.issueCount === 0 ? '✓' : '!' })}
|
|
895
947
|
</div>\`;
|
|
@@ -1028,6 +1080,17 @@ async function viewLogDetail(id) {
|
|
|
1028
1080
|
: '<span class="badge warn" title="No content was captured — the hook may have crashed before writing or returned null.">⚠ no stdout</span>');
|
|
1029
1081
|
html += '<h4 style="margin-top:14px">Provenance ' + provBadge + (ev.stdoutTruncated ? ' <span class="badge warn">⚠ truncated</span>' : '') + '</h4>';
|
|
1030
1082
|
}
|
|
1083
|
+
// Enforcement-blocked detail: show decision + full reason verbatim.
|
|
1084
|
+
if (ev.type === 'enforcement-blocked') {
|
|
1085
|
+
html += '<h4 style="margin-top:14px">Decision: <span class="badge err">' + escapeHtml(ev.decision || 'block').toUpperCase() + '</span></h4>';
|
|
1086
|
+
if (ev.reason) {
|
|
1087
|
+
html += '<h4 style="margin-top:14px">Reason</h4>';
|
|
1088
|
+
html += '<pre style="border-color: rgba(248,81,73,0.5)">' + escapeHtml(ev.reason) + '</pre>';
|
|
1089
|
+
}
|
|
1090
|
+
if (ev.capturedStdout) {
|
|
1091
|
+
html += '<details style="margin-top:14px"><summary>Raw stdout (' + fmtBytes(ev.stdoutBytes || 0) + ')</summary><pre>' + escapeHtml(ev.capturedStdout) + '</pre></details>';
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1031
1094
|
if (typeof ev.contentBytes === 'number' && ev.contentBytes > 0) {
|
|
1032
1095
|
const tokens = typeof ev.contentTokens === 'number' ? ev.contentTokens : Math.round(ev.contentBytes / 4);
|
|
1033
1096
|
const th = (state.logs && state.logs.thresholds) || { heavy: 8000, high: 20000 };
|
|
@@ -1092,11 +1155,14 @@ function renderLogs() {
|
|
|
1092
1155
|
const rows = logsState.entries.map((e) => {
|
|
1093
1156
|
const isReminder = e.type.startsWith('reminder');
|
|
1094
1157
|
const isSilenced = e.type === 'enforcement-silenced';
|
|
1158
|
+
const isBlocked = e.type === 'enforcement-blocked';
|
|
1095
1159
|
const badge = isReminder
|
|
1096
1160
|
? '<span class="badge info" style="background:rgba(88,166,255,0.18); color:#58a6ff">REMINDER</span>'
|
|
1097
1161
|
: '<span class="badge err">ENFORCEMENT</span>';
|
|
1098
1162
|
const statusCell = isSilenced
|
|
1099
1163
|
? '<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>'
|
|
1164
|
+
: isBlocked
|
|
1165
|
+
? '<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>'
|
|
1100
1166
|
: '<span class="logs-status injected" title="Hook ran and emitted additionalContext to Claude Code">✓ injected</span>';
|
|
1101
1167
|
const tokenStatus = e.tokenStatus || 'none';
|
|
1102
1168
|
const tokenBadge = tokenStatus === 'high'
|
|
@@ -1110,13 +1176,18 @@ function renderLogs() {
|
|
|
1110
1176
|
const bytesStr = e.contentBytes ? fmtBytes(e.contentBytes) : '';
|
|
1111
1177
|
const tokensTitle = e.contentBytes
|
|
1112
1178
|
? \`≈\${e.contentTokens} tokens (chars/4 heuristic) · \${fmtBytes(e.contentBytes)} raw · status: \${tokenStatus}\`
|
|
1113
|
-
:
|
|
1114
|
-
|
|
1179
|
+
: isBlocked
|
|
1180
|
+
? 'Hook BLOCKED the action. Click View for full reason.'
|
|
1181
|
+
: 'No content (hook was silenced — never ran)';
|
|
1182
|
+
const rowCls = \`token-\${tokenStatus}\${isSilenced ? ' silenced' : ''}\${isBlocked ? ' blocked' : ''}\`;
|
|
1183
|
+
const reasonPreview = isBlocked && e.reason
|
|
1184
|
+
? \`<div class="logs-reason-preview" title="\${escapeHtml(e.reason)}">\${escapeHtml(e.reason.split('\\n')[0].slice(0, 120))}</div>\`
|
|
1185
|
+
: '';
|
|
1115
1186
|
return \`<tr class="\${rowCls}">
|
|
1116
1187
|
<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>
|
|
1117
1188
|
<td>\${badge}</td>
|
|
1118
1189
|
<td>\${statusCell}</td>
|
|
1119
|
-
<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>
|
|
1190
|
+
<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>
|
|
1120
1191
|
<td class="logs-size" title="\${escapeHtml(tokensTitle)}">\${tokensStr}\${tokenBadge}\${bytesStr ? \`<br><span class="muted" style="font-size:11px">\${bytesStr}</span>\` : ''}</td>
|
|
1121
1192
|
<td><button class="logs-view" data-log-id="\${escapeHtml(e.id)}">View</button></td>
|
|
1122
1193
|
</tr>\`;
|
|
@@ -1136,7 +1207,8 @@ function renderLogs() {
|
|
|
1136
1207
|
<select id="logs-filter">
|
|
1137
1208
|
<option value="all" \${logsState.type === 'all' ? 'selected' : ''}>All</option>
|
|
1138
1209
|
<option value="reminder" \${logsState.type === 'reminder' ? 'selected' : ''}>Reminders only</option>
|
|
1139
|
-
<option value="enforcement" \${logsState.type === 'enforcement' ? 'selected' : ''}>Enforcement
|
|
1210
|
+
<option value="enforcement" \${logsState.type === 'enforcement' ? 'selected' : ''}>Enforcement (silenced + blocked)</option>
|
|
1211
|
+
<option value="blocked" \${logsState.type === 'blocked' ? 'selected' : ''}>Blocked actions only</option>
|
|
1140
1212
|
</select>
|
|
1141
1213
|
</label>
|
|
1142
1214
|
<button id="logs-reload">Reload</button>
|
|
@@ -1149,6 +1221,38 @@ function renderLogs() {
|
|
|
1149
1221
|
\${logsState.allLoaded || logsState.entries.length === 0 ? '' : '<div style="margin-top:12px"><button id="logs-load-more">Load older</button></div>'}\`;
|
|
1150
1222
|
}
|
|
1151
1223
|
|
|
1224
|
+
function renderActivePlan() {
|
|
1225
|
+
const p = state.activePlan;
|
|
1226
|
+
if (!p || !p.available) {
|
|
1227
|
+
return \`<h2>Active plan</h2>
|
|
1228
|
+
<p class="sub">No approved plan on disk. <code>.claudeflow/state/active-plan.json</code> doesn't exist.</p>
|
|
1229
|
+
<p class="muted" style="font-size:12px">When you approve a plan via ExitPlanMode in Claude Code, the
|
|
1230
|
+
<code>capture-approved-plan</code> hook will persist it here. The next time you run
|
|
1231
|
+
<code>claudeflow-build</code>, phase-0 will prompt to use it as the source of truth.</p>\`;
|
|
1232
|
+
}
|
|
1233
|
+
const age = p.ageSeconds == null
|
|
1234
|
+
? 'unknown age'
|
|
1235
|
+
: p.ageSeconds < 60 ? p.ageSeconds + 's ago'
|
|
1236
|
+
: p.ageSeconds < 3600 ? Math.floor(p.ageSeconds / 60) + 'm ago'
|
|
1237
|
+
: p.ageSeconds < 86400 ? Math.floor(p.ageSeconds / 3600) + 'h ago'
|
|
1238
|
+
: Math.floor(p.ageSeconds / 86400) + 'd ago';
|
|
1239
|
+
const sha = p.sha256 ? p.sha256.slice(0, 12) + '…' : '(none)';
|
|
1240
|
+
const preview = escapeHtml(p.summaryPreview || '(empty)');
|
|
1241
|
+
return \`<h2>Active plan</h2>
|
|
1242
|
+
<p class="sub">Read-only view of the latest approved plan persisted by the <code>ExitPlanMode</code> hook. The terminal prompt in <code>claudeflow-build</code> phase-0 is the only place you can ACT on this — the panel is informational.</p>
|
|
1243
|
+
<div class="card">
|
|
1244
|
+
\${row('Path', p.path || '(inline plan, no file path)')}
|
|
1245
|
+
\${row('Source', p.source || 'unknown')}
|
|
1246
|
+
\${row('Approved', age + ' · ' + (p.capturedAt || 'unknown timestamp'))}
|
|
1247
|
+
\${row('Session id', p.sessionId || '(unknown)')}
|
|
1248
|
+
\${row('Sha256', sha)}
|
|
1249
|
+
\${row('Size', fmtBytes(p.contentBytes || 0))}
|
|
1250
|
+
</div>
|
|
1251
|
+
<p class="sub" style="margin-top:14px">Preview</p>
|
|
1252
|
+
<pre style="max-height:120px">\${preview}</pre>
|
|
1253
|
+
<details data-detail-id="active-plan-full"><summary>Full plan content</summary><div id="active-plan-body" class="muted">Click to load…</div></details>\`;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1152
1256
|
function renderMcp() {
|
|
1153
1257
|
const m = state.mcp;
|
|
1154
1258
|
if (!m.configFound) return '<h2>MCP & observer</h2><p class="muted">.mcp.json not found</p>';
|
|
@@ -1313,13 +1417,35 @@ document.addEventListener('change', (e) => {
|
|
|
1313
1417
|
}
|
|
1314
1418
|
});
|
|
1315
1419
|
|
|
1420
|
+
let activePlanCache = null;
|
|
1421
|
+
async function loadActivePlanPreview() {
|
|
1422
|
+
const target = document.getElementById('active-plan-body');
|
|
1423
|
+
if (!target) return;
|
|
1424
|
+
if (target.dataset.loaded === '1') return;
|
|
1425
|
+
target.dataset.loaded = '1';
|
|
1426
|
+
target.textContent = 'Loading…';
|
|
1427
|
+
try {
|
|
1428
|
+
if (!activePlanCache) {
|
|
1429
|
+
const r = await fetch('/api/active-plan');
|
|
1430
|
+
activePlanCache = await r.text();
|
|
1431
|
+
}
|
|
1432
|
+
const pre = document.createElement('pre');
|
|
1433
|
+
pre.textContent = activePlanCache;
|
|
1434
|
+
target.innerHTML = '';
|
|
1435
|
+
target.appendChild(pre);
|
|
1436
|
+
} catch (e) {
|
|
1437
|
+
target.textContent = 'Failed to load: ' + e.message;
|
|
1438
|
+
target.dataset.loaded = '';
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1316
1442
|
// Lazy-load CLAUDE.md preview when the user opens the details element.
|
|
1317
1443
|
// The "toggle" event does not bubble by default — capture phase keeps delegation working.
|
|
1318
1444
|
document.addEventListener('toggle', (e) => {
|
|
1319
1445
|
const t = e.target;
|
|
1320
|
-
if (t
|
|
1321
|
-
|
|
1322
|
-
|
|
1446
|
+
if (!t || t.tagName !== 'DETAILS' || !t.open) return;
|
|
1447
|
+
if (t.dataset.detailId === 'claude-md-preview') loadClaudeMdPreview();
|
|
1448
|
+
else if (t.dataset.detailId === 'active-plan-full') loadActivePlanPreview();
|
|
1323
1449
|
}, true);
|
|
1324
1450
|
|
|
1325
1451
|
async function toggleEnforcement(disable) {
|
|
@@ -1402,6 +1528,15 @@ function handler(cwd) {
|
|
|
1402
1528
|
if (route === '/' || route === '/index.html') return send(200, renderHtml(), 'text/html; charset=utf-8');
|
|
1403
1529
|
if (route === '/api/status') return send(200, JSON.stringify(collectStatus(cwd)), 'application/json');
|
|
1404
1530
|
if (route === '/api/claude-md') return send(200, readClaudeMdSafe(cwd), 'text/plain; charset=utf-8');
|
|
1531
|
+
if (route === '/api/active-plan') {
|
|
1532
|
+
const p = path.join(cwd, '.claudeflow', 'state', 'active-plan.json');
|
|
1533
|
+
try {
|
|
1534
|
+
const r = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
1535
|
+
return send(200, r.content || '', 'text/plain; charset=utf-8');
|
|
1536
|
+
} catch {
|
|
1537
|
+
return send(404, '', 'text/plain');
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1405
1540
|
if (route === '/api/logs') {
|
|
1406
1541
|
const limit = Math.min(parseInt(url.searchParams.get('limit'), 10) || 100, 500);
|
|
1407
1542
|
const before = url.searchParams.get('before') || null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axiomatic-labs/claudeflow",
|
|
3
|
-
"version": "2.13.
|
|
3
|
+
"version": "2.13.36",
|
|
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"
|