@axiomatic-labs/claudeflow 2.13.61 → 2.13.63
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 +186 -3
- package/package.json +1 -1
package/lib/panel.js
CHANGED
|
@@ -285,6 +285,76 @@ function getEnforcementInfo(cwd) {
|
|
|
285
285
|
};
|
|
286
286
|
}
|
|
287
287
|
|
|
288
|
+
function getObserverInfo(cwd) {
|
|
289
|
+
// Read both the canonical state file (full snapshot) AND the summary file
|
|
290
|
+
// (which is what hooks consume — has the incident filter applied).
|
|
291
|
+
const tmpDir = path.join(cwd, '.claudeflow', 'tmp');
|
|
292
|
+
const statePath = path.join(tmpDir, 'error-observer-state.json');
|
|
293
|
+
const summaryPath = path.join(tmpDir, 'error-observer-summary.json');
|
|
294
|
+
let state;
|
|
295
|
+
let summary;
|
|
296
|
+
try { state = JSON.parse(fs.readFileSync(statePath, 'utf8')); } catch { state = null; }
|
|
297
|
+
try { summary = JSON.parse(fs.readFileSync(summaryPath, 'utf8')); } catch { summary = null; }
|
|
298
|
+
|
|
299
|
+
// Daemon liveness reuses doctor.js's helper (same source of truth).
|
|
300
|
+
const daemon = readObserverState(cwd);
|
|
301
|
+
|
|
302
|
+
// Servers table — flatten into UI-friendly rows.
|
|
303
|
+
const servers = state && state.servers && typeof state.servers === 'object'
|
|
304
|
+
? Object.entries(state.servers).map(([label, srv]) => ({
|
|
305
|
+
label,
|
|
306
|
+
port: srv.port || null,
|
|
307
|
+
status: srv.status || 'unknown',
|
|
308
|
+
supervised: !!srv.supervised,
|
|
309
|
+
observed_external: !!srv.observed_external,
|
|
310
|
+
health_ok: srv.health_ok !== false,
|
|
311
|
+
last_ready_at: srv.last_ready_at || null,
|
|
312
|
+
last_issue_at: srv.last_issue_at || null,
|
|
313
|
+
last_exit_at: srv.last_exit_at || null,
|
|
314
|
+
last_exit_code: srv.last_exit_code,
|
|
315
|
+
last_exit_signal: srv.last_exit_signal,
|
|
316
|
+
}))
|
|
317
|
+
: [];
|
|
318
|
+
|
|
319
|
+
// Browser section.
|
|
320
|
+
const browser = state && state.browser ? {
|
|
321
|
+
last_snapshot_at: state.browser.last_snapshot_at || null,
|
|
322
|
+
latest_url: state.browser.latest_url || null,
|
|
323
|
+
latest_route: state.browser.latest_route || null,
|
|
324
|
+
latest_origin: state.browser.latest_origin || null,
|
|
325
|
+
snapshots_received: state.browser.snapshots_received || 0,
|
|
326
|
+
} : null;
|
|
327
|
+
|
|
328
|
+
// Unresolved incidents — already filtered in summary, but expose state's
|
|
329
|
+
// full list too so the UI can show recent (incl. resolved) history.
|
|
330
|
+
const unresolvedIncidents = summary && Array.isArray(summary.unresolved_incidents)
|
|
331
|
+
? summary.unresolved_incidents
|
|
332
|
+
: [];
|
|
333
|
+
const allIncidents = state && state.incidents && typeof state.incidents === 'object'
|
|
334
|
+
? Object.values(state.incidents)
|
|
335
|
+
.sort((a, b) => String(b.last_seen_at || '').localeCompare(String(a.last_seen_at || '')))
|
|
336
|
+
.slice(0, 50)
|
|
337
|
+
: [];
|
|
338
|
+
|
|
339
|
+
return {
|
|
340
|
+
daemon,
|
|
341
|
+
summary_status: summary ? summary.status : null,
|
|
342
|
+
summary_reason: summary ? summary.reason : null,
|
|
343
|
+
summary_checked_at: summary ? summary.checked_at : null,
|
|
344
|
+
started_at: state ? state.started_at : null,
|
|
345
|
+
updated_at: state ? state.updated_at : null,
|
|
346
|
+
servers,
|
|
347
|
+
browser,
|
|
348
|
+
unresolved_incidents: unresolvedIncidents,
|
|
349
|
+
recent_incidents: allIncidents,
|
|
350
|
+
totals: summary ? {
|
|
351
|
+
unresolved: summary.unresolved_incidents_total || 0,
|
|
352
|
+
browser: summary.browser_incidents_total || 0,
|
|
353
|
+
server: summary.server_incidents_total || 0,
|
|
354
|
+
} : null,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
288
358
|
function getRunToCompletionInfo(cwd) {
|
|
289
359
|
const overrides = readOverrides(cwd);
|
|
290
360
|
return {
|
|
@@ -548,6 +618,7 @@ function collectStatus(cwd) {
|
|
|
548
618
|
reminders: getRemindersInfo(cwd),
|
|
549
619
|
enforcement: getEnforcementInfo(cwd),
|
|
550
620
|
runToCompletion: getRunToCompletionInfo(cwd),
|
|
621
|
+
observer: getObserverInfo(cwd),
|
|
551
622
|
activePlan: getActivePlanInfo(cwd),
|
|
552
623
|
logs: getLogsInfo(cwd),
|
|
553
624
|
doctor: getDoctorInfo(cwd),
|
|
@@ -748,6 +819,7 @@ const SECTIONS = [
|
|
|
748
819
|
{ id: 'hooks', label: 'Hooks' },
|
|
749
820
|
{ id: 'reminders', label: 'Reminders' },
|
|
750
821
|
{ id: 'logs', label: 'Logs' },
|
|
822
|
+
{ id: 'observer', label: 'Observer' },
|
|
751
823
|
{ id: 'activePlan', label: 'Active plan' },
|
|
752
824
|
{ id: 'mcp', label: 'MCP & observer' },
|
|
753
825
|
{ id: 'setupContext', label: 'Setup context' },
|
|
@@ -880,12 +952,19 @@ function severityFor(id) {
|
|
|
880
952
|
case 'mcp': {
|
|
881
953
|
if (!s.mcp.configFound) return 'info';
|
|
882
954
|
const playwrightOk = s.mcp.playwright.match && s.mcp.staleLockfiles.length === 0;
|
|
883
|
-
// Observer stopped while Playwright is configured = warn. Console errors
|
|
884
|
-
// and network failures from headed sessions are silently lost when the
|
|
885
|
-
// observer is down.
|
|
886
955
|
const observerOk = !s.mcp.playwright.match || (s.mcp.observer && s.mcp.observer.running);
|
|
887
956
|
return playwrightOk && observerOk ? 'ok' : 'warn';
|
|
888
957
|
}
|
|
958
|
+
case 'observer': {
|
|
959
|
+
if (!s.observer) return 'info';
|
|
960
|
+
// Daemon down → warn (not err, since the observer is supportive infra).
|
|
961
|
+
if (!s.observer.daemon || !s.observer.daemon.running) return 'warn';
|
|
962
|
+
// Any unresolved incidents → warn so the user sees the badge.
|
|
963
|
+
if (s.observer.totals && s.observer.totals.unresolved > 0) return 'warn';
|
|
964
|
+
// Any supervised server in unhealthy/exited → warn.
|
|
965
|
+
if (s.observer.servers && s.observer.servers.some((sr) => sr.status === 'unhealthy' || sr.status === 'exited')) return 'warn';
|
|
966
|
+
return 'ok';
|
|
967
|
+
}
|
|
889
968
|
case 'setupContext': return !s.setupContext.exists ? 'err' : (s.setupContext.toolingComplete ? 'ok' : 'warn');
|
|
890
969
|
case 'activeRun': return s.activeRun.active ? 'info' : 'info';
|
|
891
970
|
case 'doctor': return s.doctor.issueCount > 0 ? 'warn' : 'ok';
|
|
@@ -930,6 +1009,7 @@ function renderContent() {
|
|
|
930
1009
|
hooks: renderHooks,
|
|
931
1010
|
reminders: renderReminders,
|
|
932
1011
|
logs: renderLogs,
|
|
1012
|
+
observer: renderObserver,
|
|
933
1013
|
activePlan: renderActivePlan,
|
|
934
1014
|
mcp: renderMcp,
|
|
935
1015
|
setupContext: renderSetup,
|
|
@@ -1295,6 +1375,108 @@ function renderActivePlan() {
|
|
|
1295
1375
|
<details data-detail-id="active-plan-full"><summary>Full plan content</summary><div id="active-plan-body" class="muted">Click to load…</div></details>\`;
|
|
1296
1376
|
}
|
|
1297
1377
|
|
|
1378
|
+
function renderObserver() {
|
|
1379
|
+
const o = state.observer;
|
|
1380
|
+
if (!o) return '<h2>Observer</h2><p class="muted">No observer info available.</p>';
|
|
1381
|
+
const daemonAlive = o.daemon && o.daemon.running;
|
|
1382
|
+
const statusBadge = daemonAlive
|
|
1383
|
+
? \`<span class="badge ok">✓ running</span> <span class="muted">pid \${o.daemon.pid} • port \${o.daemon.port} • detected via \${o.daemon.source}</span>\`
|
|
1384
|
+
: \`<span class="badge warn">⚠ stopped</span> <span class="muted">no listener detected</span>\`;
|
|
1385
|
+
const restartBtn = !daemonAlive
|
|
1386
|
+
? '<button id="restart-observer" class="btn-warn" style="margin-left:14px;">Restart observer</button>'
|
|
1387
|
+
: '';
|
|
1388
|
+
const totals = o.totals || { unresolved: 0, browser: 0, server: 0 };
|
|
1389
|
+
const totalsBadge = totals.unresolved > 0
|
|
1390
|
+
? \`<span class="badge warn">\${totals.unresolved} unresolved</span> <span class="muted">(browser: \${totals.browser}, server: \${totals.server})</span>\`
|
|
1391
|
+
: '<span class="badge ok">✓ no unresolved incidents</span>';
|
|
1392
|
+
const summaryStatus = o.summary_status ? \`<span class="muted">summary status: <code>\${escapeHtml(o.summary_status)}</code></span>\` : '';
|
|
1393
|
+
|
|
1394
|
+
// Servers table
|
|
1395
|
+
const serversTable = o.servers.length === 0
|
|
1396
|
+
? '<p class="muted">No servers configured in setup-context.json.servers.</p>'
|
|
1397
|
+
: '<table class="kv-table" style="width:100%;border-collapse:collapse;font-size:13px;">' +
|
|
1398
|
+
'<thead><tr><th style="text-align:left;padding:6px 8px;">Label</th><th style="text-align:left;padding:6px 8px;">Port</th><th style="text-align:left;padding:6px 8px;">Status</th><th style="text-align:left;padding:6px 8px;">Health</th><th style="text-align:left;padding:6px 8px;">Last ready</th><th style="text-align:left;padding:6px 8px;">Last issue</th></tr></thead>' +
|
|
1399
|
+
'<tbody>' +
|
|
1400
|
+
o.servers.map((srv) => {
|
|
1401
|
+
const statusColor = srv.status === 'running' || srv.status === 'external' ? 'ok' : (srv.status === 'unhealthy' || srv.status === 'exited' ? 'err' : 'info');
|
|
1402
|
+
const healthIcon = srv.health_ok ? '<span class="badge ok">✓</span>' : '<span class="badge err">✗</span>';
|
|
1403
|
+
const exitInfo = srv.last_exit_code !== null && srv.last_exit_code !== undefined
|
|
1404
|
+
? \` <span class="muted">(exit \${srv.last_exit_code}\${srv.last_exit_signal ? '/' + srv.last_exit_signal : ''})</span>\`
|
|
1405
|
+
: '';
|
|
1406
|
+
return \`<tr style="border-top:1px solid var(--border);">
|
|
1407
|
+
<td style="padding:6px 8px;"><strong>\${escapeHtml(srv.label)}</strong></td>
|
|
1408
|
+
<td style="padding:6px 8px;"><code>\${srv.port || '?'}</code></td>
|
|
1409
|
+
<td style="padding:6px 8px;"><span class="badge \${statusColor}">\${escapeHtml(srv.status)}</span>\${exitInfo}</td>
|
|
1410
|
+
<td style="padding:6px 8px;">\${healthIcon}</td>
|
|
1411
|
+
<td style="padding:6px 8px;" class="muted">\${escapeHtml(formatRelativeTime(srv.last_ready_at))}</td>
|
|
1412
|
+
<td style="padding:6px 8px;" class="muted">\${escapeHtml(formatRelativeTime(srv.last_issue_at))}</td>
|
|
1413
|
+
</tr>\`;
|
|
1414
|
+
}).join('') +
|
|
1415
|
+
'</tbody></table>';
|
|
1416
|
+
|
|
1417
|
+
// Browser section
|
|
1418
|
+
const browserBlock = o.browser && o.browser.last_snapshot_at
|
|
1419
|
+
? \`<div class="card" style="padding:10px 14px;font-size:13px;">
|
|
1420
|
+
\${row('Latest URL', o.browser.latest_url ? '<code>' + escapeHtml(o.browser.latest_url) + '</code>' : '<span class="muted">none</span>')}
|
|
1421
|
+
\${row('Latest route', o.browser.latest_route ? '<code>' + escapeHtml(o.browser.latest_route) + '</code>' : '<span class="muted">none</span>')}
|
|
1422
|
+
\${row('Snapshots received', String(o.browser.snapshots_received))}
|
|
1423
|
+
\${row('Last snapshot', formatRelativeTime(o.browser.last_snapshot_at))}
|
|
1424
|
+
</div>\`
|
|
1425
|
+
: '<p class="muted">No browser snapshots received yet. (The observer collects when the agent navigates with mcp__claude-in-chrome__*.)</p>';
|
|
1426
|
+
|
|
1427
|
+
// Incidents feed
|
|
1428
|
+
const incidentsBlock = o.recent_incidents.length === 0
|
|
1429
|
+
? '<p class="muted">No incidents recorded.</p>'
|
|
1430
|
+
: '<div class="incidents-feed">' +
|
|
1431
|
+
o.recent_incidents.map((inc) => {
|
|
1432
|
+
const sevClass = inc.severity === 'error' || inc.severity === 'critical' ? 'err' : (inc.severity === 'warning' ? 'warn' : 'info');
|
|
1433
|
+
const resolvedBadge = inc.resolved_at
|
|
1434
|
+
? '<span class="badge ok" style="margin-left:6px;">resolved</span>'
|
|
1435
|
+
: '<span class="badge warn" style="margin-left:6px;">active</span>';
|
|
1436
|
+
const surfaceLabel = inc.source === 'server' && inc.metadata && inc.metadata.label
|
|
1437
|
+
? inc.metadata.label
|
|
1438
|
+
: inc.surface || inc.source;
|
|
1439
|
+
return \`<div class="incident-row" style="padding:10px 14px;border-top:1px solid var(--border);">
|
|
1440
|
+
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:4px;">
|
|
1441
|
+
<span class="badge \${sevClass}">\${escapeHtml(inc.severity || 'info')}</span>
|
|
1442
|
+
<span class="muted">\${escapeHtml(inc.source || 'unknown')}</span>
|
|
1443
|
+
<span class="muted">→</span>
|
|
1444
|
+
<strong>\${escapeHtml(surfaceLabel)}</strong>
|
|
1445
|
+
<span class="muted">\${escapeHtml(inc.type || '')}</span>
|
|
1446
|
+
\${resolvedBadge}
|
|
1447
|
+
<span class="muted" style="margin-left:auto;font-size:11px;">\${escapeHtml(formatRelativeTime(inc.last_seen_at))}</span>
|
|
1448
|
+
</div>
|
|
1449
|
+
<div style="font-size:12px;white-space:pre-wrap;">\${escapeHtml((inc.message || '').slice(0, 600))}\${(inc.message || '').length > 600 ? '…' : ''}</div>
|
|
1450
|
+
\${inc.metadata && inc.metadata.url ? '<div class="muted" style="font-size:11px;margin-top:4px;">at <code>' + escapeHtml(inc.metadata.url) + '</code></div>' : ''}
|
|
1451
|
+
</div>\`;
|
|
1452
|
+
}).join('') +
|
|
1453
|
+
'</div>';
|
|
1454
|
+
|
|
1455
|
+
return \`<h2>Observer</h2>
|
|
1456
|
+
<p class="sub">Daemon: \${statusBadge}\${restartBtn}</p>
|
|
1457
|
+
<p class="sub">Incidents: \${totalsBadge} \${summaryStatus}</p>
|
|
1458
|
+
<h3 style="margin-top:18px;font-size:14px;">Servers (\${o.servers.length})</h3>
|
|
1459
|
+
<div class="card" style="padding:0;">\${serversTable}</div>
|
|
1460
|
+
<h3 style="margin-top:18px;font-size:14px;">Browser channel</h3>
|
|
1461
|
+
\${browserBlock}
|
|
1462
|
+
<h3 style="margin-top:18px;font-size:14px;">Incidents (most recent \${o.recent_incidents.length})</h3>
|
|
1463
|
+
<div class="card" style="padding:0;">\${incidentsBlock}</div>\`;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
function formatRelativeTime(iso) {
|
|
1467
|
+
if (!iso) return 'never';
|
|
1468
|
+
try {
|
|
1469
|
+
const t = new Date(iso).getTime();
|
|
1470
|
+
if (Number.isNaN(t)) return 'unknown';
|
|
1471
|
+
const diff = Date.now() - t;
|
|
1472
|
+
if (diff < 0) return 'in the future';
|
|
1473
|
+
if (diff < 60_000) return Math.floor(diff / 1000) + 's ago';
|
|
1474
|
+
if (diff < 3_600_000) return Math.floor(diff / 60_000) + 'm ago';
|
|
1475
|
+
if (diff < 86_400_000) return Math.floor(diff / 3_600_000) + 'h ago';
|
|
1476
|
+
return Math.floor(diff / 86_400_000) + 'd ago';
|
|
1477
|
+
} catch { return 'unknown'; }
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1298
1480
|
function renderMcp() {
|
|
1299
1481
|
const m = state.mcp;
|
|
1300
1482
|
if (!m.configFound) return '<h2>MCP & observer</h2><p class="muted">.mcp.json not found</p>';
|
|
@@ -1922,6 +2104,7 @@ module.exports = run;
|
|
|
1922
2104
|
module.exports.start = start;
|
|
1923
2105
|
module.exports.createPanelServer = handler;
|
|
1924
2106
|
module.exports.derivePanelPort = derivePanelPort;
|
|
2107
|
+
module.exports.getObserverInfo = getObserverInfo;
|
|
1925
2108
|
module.exports.collectStatus = collectStatus;
|
|
1926
2109
|
module.exports.getClaudeMdInfo = getClaudeMdInfo;
|
|
1927
2110
|
module.exports.getHooksInfo = getHooksInfo;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axiomatic-labs/claudeflow",
|
|
3
|
-
"version": "2.13.
|
|
3
|
+
"version": "2.13.63",
|
|
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"
|