@axiomatic-labs/claudeflow 2.13.62 → 2.13.64

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 +254 -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,115 @@ 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
+ // Clear button — only meaningful when the daemon is running AND there's
1456
+ // either at least one unresolved incident or some browser snapshots to wipe.
1457
+ const clearable = daemonAlive && (totals.unresolved > 0 || (o.browser && o.browser.snapshots_received > 0) || o.recent_incidents.length > 0);
1458
+ const clearBtn = clearable
1459
+ ? '<button id="clear-observer" class="logs-clear-btn" style="margin-left:14px;" title="Resolves all active incidents, clears per-server recent_log buffers, and resets browser snapshot counter. The state file keeps incident history (marked resolved). Future events repopulate as usual.">Clear observer state</button>'
1460
+ : '';
1461
+
1462
+ return \`<h2>Observer</h2>
1463
+ <p class="sub">Daemon: \${statusBadge}\${restartBtn}\${clearBtn}</p>
1464
+ <p class="sub">Incidents: \${totalsBadge} \${summaryStatus}</p>
1465
+ <h3 style="margin-top:18px;font-size:14px;">Servers (\${o.servers.length})</h3>
1466
+ <div class="card" style="padding:0;">\${serversTable}</div>
1467
+ <h3 style="margin-top:18px;font-size:14px;">Browser channel</h3>
1468
+ \${browserBlock}
1469
+ <h3 style="margin-top:18px;font-size:14px;">Incidents (most recent \${o.recent_incidents.length})</h3>
1470
+ <div class="card" style="padding:0;">\${incidentsBlock}</div>\`;
1471
+ }
1472
+
1473
+ function formatRelativeTime(iso) {
1474
+ if (!iso) return 'never';
1475
+ try {
1476
+ const t = new Date(iso).getTime();
1477
+ if (Number.isNaN(t)) return 'unknown';
1478
+ const diff = Date.now() - t;
1479
+ if (diff < 0) return 'in the future';
1480
+ if (diff < 60_000) return Math.floor(diff / 1000) + 's ago';
1481
+ if (diff < 3_600_000) return Math.floor(diff / 60_000) + 'm ago';
1482
+ if (diff < 86_400_000) return Math.floor(diff / 3_600_000) + 'h ago';
1483
+ return Math.floor(diff / 86_400_000) + 'd ago';
1484
+ } catch { return 'unknown'; }
1485
+ }
1486
+
1298
1487
  function renderMcp() {
1299
1488
  const m = state.mcp;
1300
1489
  if (!m.configFound) return '<h2>MCP & observer</h2><p class="muted">.mcp.json not found</p>';
@@ -1425,8 +1614,33 @@ document.addEventListener('click', (e) => {
1425
1614
  if (t.id === 'fix-cdp-port') {
1426
1615
  return fixCdpPortAction(t);
1427
1616
  }
1617
+ if (t.id === 'clear-observer') {
1618
+ return clearObserverAction(t);
1619
+ }
1428
1620
  });
1429
1621
 
1622
+ async function clearObserverAction(btn) {
1623
+ if (!confirm('Resolve all active observer incidents and clear server logs?\\n\\nIncident history is preserved in state.incidents (marked resolved). Future events repopulate normally. This cannot be undone.')) return;
1624
+ const originalLabel = btn.textContent;
1625
+ btn.disabled = true;
1626
+ btn.textContent = 'Clearing…';
1627
+ try {
1628
+ const r = await fetch('/api/observer/clear', { method: 'POST' });
1629
+ const result = await r.json();
1630
+ if (r.ok && result.ok) {
1631
+ showToast('Observer state cleared at ' + (result.reset_at || 'now'), 'ok');
1632
+ } else {
1633
+ showToast('Clear failed: ' + (result.error || 'unknown'), 'err');
1634
+ }
1635
+ } catch (e) {
1636
+ showToast('Network error: ' + e.message, 'err');
1637
+ } finally {
1638
+ btn.disabled = false;
1639
+ btn.textContent = originalLabel;
1640
+ await refresh();
1641
+ }
1642
+ }
1643
+
1430
1644
  async function fixCdpPortAction(btn) {
1431
1645
  const originalLabel = btn.textContent;
1432
1646
  btn.disabled = true;
@@ -1754,6 +1968,42 @@ function handler(cwd) {
1754
1968
  }), 'application/json');
1755
1969
  }
1756
1970
 
1971
+ if (req.method === 'POST' && route === '/api/observer/clear') {
1972
+ // Forwards to the observer daemon's /reset-session endpoint, which
1973
+ // resolves all active browser + server incidents, clears the per-server
1974
+ // recent_log buffers, and resets the browser snapshot counter. The
1975
+ // history is preserved in state.incidents (marked resolved), so this
1976
+ // is a soft clear: future polls won't see the old incidents as active.
1977
+ const observer = readObserverState(cwd);
1978
+ if (!observer.running) {
1979
+ return send(409, JSON.stringify({ ok: false, error: 'observer is not running — start it first, then clear.' }), 'application/json');
1980
+ }
1981
+ const { request: httpRequest } = require('node:http');
1982
+ const result = await new Promise((resolve) => {
1983
+ const req2 = httpRequest({
1984
+ hostname: '127.0.0.1',
1985
+ port: observer.port,
1986
+ path: '/reset-session',
1987
+ method: 'POST',
1988
+ headers: { 'Content-Type': 'application/json', 'Content-Length': 2 },
1989
+ }, (r) => {
1990
+ let body = '';
1991
+ r.on('data', (c) => { body += c; });
1992
+ r.on('end', () => {
1993
+ try { resolve({ status: r.statusCode, body: JSON.parse(body) }); }
1994
+ catch { resolve({ status: r.statusCode, body: { raw: body } }); }
1995
+ });
1996
+ });
1997
+ req2.on('error', (err) => resolve({ status: 0, body: { error: err.message } }));
1998
+ req2.write('{}');
1999
+ req2.end();
2000
+ });
2001
+ if (result.status === 200 && result.body && result.body.ok) {
2002
+ return send(200, JSON.stringify({ ok: true, reset_at: result.body.reset_at }), 'application/json');
2003
+ }
2004
+ return send(502, JSON.stringify({ ok: false, error: 'observer returned ' + result.status, daemon_response: result.body }), 'application/json');
2005
+ }
2006
+
1757
2007
  if (req.method === 'POST' && route === '/api/observer/restart') {
1758
2008
  // Re-run the SessionStart/browser-error-daemon.js hook script
1759
2009
  // synchronously. The script calls ensureObserver() which spawns
@@ -1922,6 +2172,7 @@ module.exports = run;
1922
2172
  module.exports.start = start;
1923
2173
  module.exports.createPanelServer = handler;
1924
2174
  module.exports.derivePanelPort = derivePanelPort;
2175
+ module.exports.getObserverInfo = getObserverInfo;
1925
2176
  module.exports.collectStatus = collectStatus;
1926
2177
  module.exports.getClaudeMdInfo = getClaudeMdInfo;
1927
2178
  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.64",
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"