@axiomatic-labs/claudeflow 2.13.62 → 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.
Files changed (2) hide show
  1. package/lib/panel.js +186 -3
  2. 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.62",
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"