@axiomatic-labs/claudeflow 2.13.55 → 2.13.57
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/doctor.js +51 -0
- package/lib/panel.js +75 -16
- package/package.json +1 -1
package/lib/doctor.js
CHANGED
|
@@ -129,6 +129,54 @@ function checkStaleLockfiles(cwd) {
|
|
|
129
129
|
};
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
// Read the browser-error-daemon observer state from its pidfile.
|
|
133
|
+
// Returns { running, pid, lockfile } — running=true only when the pid is
|
|
134
|
+
// alive AND the lockfile exists. Used by both doctor and panel.
|
|
135
|
+
function readObserverState(cwd, cdpPort) {
|
|
136
|
+
const lockfile = path.join(cwd, '.claudeflow', 'tmp', `.browser-cdp-${cdpPort}.pid`);
|
|
137
|
+
let pid = null;
|
|
138
|
+
try {
|
|
139
|
+
pid = parseInt(fs.readFileSync(lockfile, 'utf8').trim(), 10);
|
|
140
|
+
} catch {
|
|
141
|
+
return { running: false, pid: null, lockfile };
|
|
142
|
+
}
|
|
143
|
+
if (!Number.isFinite(pid) || pid <= 0) return { running: false, pid, lockfile };
|
|
144
|
+
let alive = false;
|
|
145
|
+
try { process.kill(pid, 0); alive = true; } catch (e) { alive = e.code === 'EPERM'; }
|
|
146
|
+
return { running: alive, pid, lockfile };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Surface as a doctor/panel issue when the browser-error-daemon observer
|
|
150
|
+
// is stopped while Playwright is configured. The observer collects console
|
|
151
|
+
// errors + network failures during headed browser verification — when it's
|
|
152
|
+
// down, those signals are silently lost, so the agent may verify a flow
|
|
153
|
+
// that has console errors and not know it.
|
|
154
|
+
function checkObserverState(cwd) {
|
|
155
|
+
const mcpPath = path.join(cwd, '.mcp.json');
|
|
156
|
+
let cfg;
|
|
157
|
+
try { cfg = JSON.parse(fs.readFileSync(mcpPath, 'utf8')); }
|
|
158
|
+
catch { return { id: 'observer-state', severity: 'info', message: 'No .mcp.json — observer not applicable.' }; }
|
|
159
|
+
const hasPlaywright = cfg.mcpServers
|
|
160
|
+
&& Object.keys(cfg.mcpServers).some((s) => s.toLowerCase().includes('playwright'));
|
|
161
|
+
if (!hasPlaywright) {
|
|
162
|
+
return { id: 'observer-state', severity: 'info', message: 'Playwright not configured — observer not applicable.' };
|
|
163
|
+
}
|
|
164
|
+
const observer = readObserverState(cwd, deriveCdpPort(cwd));
|
|
165
|
+
if (observer.running) {
|
|
166
|
+
return {
|
|
167
|
+
id: 'observer-state',
|
|
168
|
+
severity: 'ok',
|
|
169
|
+
message: `browser-error-daemon running (pid ${observer.pid}).`,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
id: 'observer-state',
|
|
174
|
+
severity: 'mismatch',
|
|
175
|
+
message: 'browser-error-daemon is STOPPED while Playwright is configured. Console errors / network failures from headed sessions are NOT being collected — the agent may verify a flow with console errors and miss them. The observer should auto-start at SessionStart via `.claude/hooks/SessionStart/browser-error-daemon.js`; if it keeps stopping, inspect that hook for crashes or check the pidfile at `.claudeflow/tmp/.browser-cdp-<port>.pid`.',
|
|
176
|
+
detail: [{ lockfile: observer.lockfile, pid: observer.pid }],
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
132
180
|
function applyStaleLockfileFix(check) {
|
|
133
181
|
for (const { file } of check.fix.stale) {
|
|
134
182
|
try { fs.unlinkSync(file); } catch {}
|
|
@@ -163,6 +211,7 @@ async function run(argv = []) {
|
|
|
163
211
|
const checks = [
|
|
164
212
|
checkCdpPortMismatch(cwd),
|
|
165
213
|
checkStaleLockfiles(cwd),
|
|
214
|
+
checkObserverState(cwd),
|
|
166
215
|
];
|
|
167
216
|
|
|
168
217
|
for (const check of checks) printCheck(check);
|
|
@@ -202,6 +251,8 @@ module.exports = run;
|
|
|
202
251
|
module.exports.deriveCdpPort = deriveCdpPort;
|
|
203
252
|
module.exports.checkCdpPortMismatch = checkCdpPortMismatch;
|
|
204
253
|
module.exports.checkStaleLockfiles = checkStaleLockfiles;
|
|
254
|
+
module.exports.checkObserverState = checkObserverState;
|
|
255
|
+
module.exports.readObserverState = readObserverState;
|
|
205
256
|
module.exports.readPlaywrightCdpEndpoint = readPlaywrightCdpEndpoint;
|
|
206
257
|
module.exports.applyCdpPortFix = applyCdpPortFix;
|
|
207
258
|
module.exports.applyStaleLockfileFix = applyStaleLockfileFix;
|
package/lib/panel.js
CHANGED
|
@@ -19,6 +19,8 @@ const {
|
|
|
19
19
|
deriveCdpPort,
|
|
20
20
|
checkCdpPortMismatch,
|
|
21
21
|
checkStaleLockfiles,
|
|
22
|
+
checkObserverState,
|
|
23
|
+
readObserverState: readObserverStateFromDoctor,
|
|
22
24
|
readPlaywrightCdpEndpoint,
|
|
23
25
|
} = require('./doctor.js');
|
|
24
26
|
const {
|
|
@@ -211,19 +213,10 @@ function getMcpInfo(cwd) {
|
|
|
211
213
|
};
|
|
212
214
|
}
|
|
213
215
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
pid = parseInt(fs.readFileSync(lockfile, 'utf8').trim(), 10);
|
|
219
|
-
} catch {
|
|
220
|
-
return { running: false, pid: null, lockfile };
|
|
221
|
-
}
|
|
222
|
-
if (!Number.isFinite(pid) || pid <= 0) return { running: false, pid, lockfile };
|
|
223
|
-
let alive = false;
|
|
224
|
-
try { process.kill(pid, 0); alive = true; } catch (e) { alive = e.code === 'EPERM'; }
|
|
225
|
-
return { running: alive, pid, lockfile };
|
|
226
|
-
}
|
|
216
|
+
// Re-export the canonical helper from doctor.js so panel callers keep the
|
|
217
|
+
// same name. The implementation moved to doctor.js so both surfaces (CLI
|
|
218
|
+
// doctor + panel) share a single source of truth.
|
|
219
|
+
const readObserverState = readObserverStateFromDoctor;
|
|
227
220
|
|
|
228
221
|
function getSetupContextInfo(cwd) {
|
|
229
222
|
const p = path.join(cwd, '.claudeflow', 'config', 'setup-context.json');
|
|
@@ -524,7 +517,11 @@ function getLogsInfo(cwd) {
|
|
|
524
517
|
}
|
|
525
518
|
|
|
526
519
|
function getDoctorInfo(cwd) {
|
|
527
|
-
const checks = [
|
|
520
|
+
const checks = [
|
|
521
|
+
checkCdpPortMismatch(cwd),
|
|
522
|
+
checkStaleLockfiles(cwd),
|
|
523
|
+
checkObserverState(cwd),
|
|
524
|
+
];
|
|
528
525
|
const issues = checks.filter((c) => c.severity !== 'ok' && c.severity !== 'info');
|
|
529
526
|
return {
|
|
530
527
|
issueCount: issues.length,
|
|
@@ -661,6 +658,9 @@ details[open] summary { padding-bottom: 8px; border-bottom: 1px solid var(--bord
|
|
|
661
658
|
.logs-controls { display: flex; gap: 12px; align-items: center; margin-bottom: 14px; }
|
|
662
659
|
.logs-controls select, .logs-controls button { background: var(--panel); border: 1px solid var(--border); color: var(--fg); padding: 6px 10px; border-radius: 6px; font: inherit; }
|
|
663
660
|
.logs-controls button:hover { border-color: var(--accent); cursor: pointer; }
|
|
661
|
+
.btn-warn { background: var(--panel); border: 1px solid rgba(245,194,52,0.5); color: var(--warn, #f5c234); padding: 6px 12px; border-radius: 6px; cursor: pointer; font: inherit; }
|
|
662
|
+
.btn-warn:hover { border-color: var(--warn, #f5c234); background: rgba(245,194,52,0.08); }
|
|
663
|
+
.btn-warn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
664
664
|
.logs-clear-btn { margin-left: auto; color: var(--err); border-color: rgba(248,81,73,0.4); }
|
|
665
665
|
.logs-clear-btn:hover { border-color: var(--err); background: rgba(248,81,73,0.08); }
|
|
666
666
|
.logs-clear-btn:disabled { opacity: 0.4; cursor: not-allowed; color: var(--muted); border-color: var(--border); }
|
|
@@ -872,7 +872,15 @@ function severityFor(id) {
|
|
|
872
872
|
if (s.logs.latestReminderStatus === 'heavy') return 'warn';
|
|
873
873
|
return s.logs.total > 0 ? 'ok' : 'info';
|
|
874
874
|
}
|
|
875
|
-
case 'mcp':
|
|
875
|
+
case 'mcp': {
|
|
876
|
+
if (!s.mcp.configFound) return 'info';
|
|
877
|
+
const playwrightOk = s.mcp.playwright.match && s.mcp.staleLockfiles.length === 0;
|
|
878
|
+
// Observer stopped while Playwright is configured = warn. Console errors
|
|
879
|
+
// and network failures from headed sessions are silently lost when the
|
|
880
|
+
// observer is down.
|
|
881
|
+
const observerOk = !s.mcp.playwright.match || (s.mcp.observer && s.mcp.observer.running);
|
|
882
|
+
return playwrightOk && observerOk ? 'ok' : 'warn';
|
|
883
|
+
}
|
|
876
884
|
case 'setupContext': return !s.setupContext.exists ? 'err' : (s.setupContext.toolingComplete ? 'ok' : 'warn');
|
|
877
885
|
case 'activeRun': return s.activeRun.active ? 'info' : 'info';
|
|
878
886
|
case 'doctor': return s.doctor.issueCount > 0 ? 'warn' : 'ok';
|
|
@@ -1294,7 +1302,8 @@ function renderMcp() {
|
|
|
1294
1302
|
\${row('Playwright --cdp-endpoint port', String(m.playwright.configured || 'n/a'))}
|
|
1295
1303
|
\${row('Computed port (from path)', String(m.playwright.computed))}
|
|
1296
1304
|
\${row('Match', m.playwright.match ? 'YES' : 'NO', { kind: m.playwright.match ? 'ok' : 'err', text: m.playwright.match ? '✓' : '✗' })}
|
|
1297
|
-
\${row('Observer', m.observer.running ? 'running (pid '+m.observer.pid+')' : '
|
|
1305
|
+
\${row('Observer', m.observer.running ? 'running (pid '+m.observer.pid+')' : 'STOPPED — browser errors NOT being collected', { kind: m.observer.running ? 'ok' : (m.playwright.match ? 'warn' : 'info'), text: m.observer.running ? '✓' : (m.playwright.match ? '⚠' : '·') })}
|
|
1306
|
+
\${!m.observer.running && m.playwright.match ? '<div style="padding:10px 14px;border-top:1px solid var(--border);"><button id="restart-observer" class="btn-warn">Restart observer</button> <span class="muted" style="margin-left:10px;font-size:12px;">Re-runs SessionStart/browser-error-daemon.js — spawns Chrome with --cdp-endpoint and registers the daemon pidfile.</span></div>' : ''}
|
|
1298
1307
|
</div>
|
|
1299
1308
|
<h2 style="margin-top:18px">Stale lockfiles</h2>
|
|
1300
1309
|
\${lockfiles}\`;
|
|
@@ -1404,8 +1413,32 @@ document.addEventListener('click', (e) => {
|
|
|
1404
1413
|
if (!confirm('Delete ' + total + ' captured log event(s) and truncate .claude/tmp/hooks.log?\\n\\nThis cannot be undone. (Future events will be captured normally.)')) return;
|
|
1405
1414
|
return clearLogsAction();
|
|
1406
1415
|
}
|
|
1416
|
+
if (t.id === 'restart-observer') {
|
|
1417
|
+
return restartObserverAction(t);
|
|
1418
|
+
}
|
|
1407
1419
|
});
|
|
1408
1420
|
|
|
1421
|
+
async function restartObserverAction(btn) {
|
|
1422
|
+
const originalLabel = btn.textContent;
|
|
1423
|
+
btn.disabled = true;
|
|
1424
|
+
btn.textContent = 'Restarting…';
|
|
1425
|
+
try {
|
|
1426
|
+
const r = await fetch('/api/observer/restart', { method: 'POST' });
|
|
1427
|
+
const result = await r.json();
|
|
1428
|
+
if (r.ok && result.ok) {
|
|
1429
|
+
showToast('Observer restarted — pid ' + (result.observer && result.observer.pid) + '. Refreshing…', 'ok');
|
|
1430
|
+
} else {
|
|
1431
|
+
showToast('Restart failed: ' + (result.error || result.script_stderr || 'unknown'), 'err');
|
|
1432
|
+
}
|
|
1433
|
+
} catch (e) {
|
|
1434
|
+
showToast('Network error: ' + e.message, 'err');
|
|
1435
|
+
} finally {
|
|
1436
|
+
btn.disabled = false;
|
|
1437
|
+
btn.textContent = originalLabel;
|
|
1438
|
+
await refresh();
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1409
1442
|
async function clearLogsAction() {
|
|
1410
1443
|
try {
|
|
1411
1444
|
const r = await fetch('/api/logs', { method: 'DELETE' });
|
|
@@ -1656,6 +1689,32 @@ function handler(cwd) {
|
|
|
1656
1689
|
return send(status, JSON.stringify(result), 'application/json');
|
|
1657
1690
|
}
|
|
1658
1691
|
|
|
1692
|
+
if (req.method === 'POST' && route === '/api/observer/restart') {
|
|
1693
|
+
// Re-run the SessionStart/browser-error-daemon.js hook script
|
|
1694
|
+
// synchronously. The script calls ensureObserver() which spawns
|
|
1695
|
+
// Chrome with --cdp-endpoint as a detached process. Once the
|
|
1696
|
+
// script exits, the daemon is up (or the script logged the failure).
|
|
1697
|
+
const daemonScript = path.join(cwd, '.claude', 'hooks', 'SessionStart', 'browser-error-daemon.js');
|
|
1698
|
+
if (!fs.existsSync(daemonScript)) {
|
|
1699
|
+
return send(404, JSON.stringify({ ok: false, error: 'daemon script not found at .claude/hooks/SessionStart/browser-error-daemon.js' }), 'application/json');
|
|
1700
|
+
}
|
|
1701
|
+
const { spawnSync } = require('child_process');
|
|
1702
|
+
const r = spawnSync(process.execPath, [daemonScript], {
|
|
1703
|
+
cwd,
|
|
1704
|
+
env: { ...process.env, CLAUDE_PROJECT_DIR: cwd },
|
|
1705
|
+
encoding: 'utf8',
|
|
1706
|
+
timeout: 15000,
|
|
1707
|
+
});
|
|
1708
|
+
const observerState = readObserverState(cwd, deriveCdpPort(cwd));
|
|
1709
|
+
const ok = observerState.running;
|
|
1710
|
+
return send(ok ? 200 : 500, JSON.stringify({
|
|
1711
|
+
ok,
|
|
1712
|
+
observer: observerState,
|
|
1713
|
+
script_exit: r.status,
|
|
1714
|
+
script_stderr: (r.stderr || '').slice(0, 500),
|
|
1715
|
+
}), 'application/json');
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1659
1718
|
if (req.method === 'POST' && route === '/api/reminders/toggle') {
|
|
1660
1719
|
const raw = await readRequestBody(req);
|
|
1661
1720
|
let payload;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axiomatic-labs/claudeflow",
|
|
3
|
-
"version": "2.13.
|
|
3
|
+
"version": "2.13.57",
|
|
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"
|