@axiomatic-labs/claudeflow 2.13.57 → 2.13.59
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 +67 -16
- package/lib/panel.js +99 -6
- package/package.json +1 -1
package/lib/doctor.js
CHANGED
|
@@ -129,21 +129,66 @@ function checkStaleLockfiles(cwd) {
|
|
|
129
129
|
};
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
// Read the browser-error-daemon observer state
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
132
|
+
// Read the browser-error-daemon observer state. The daemon is the Node
|
|
133
|
+
// process that hosts the HTTP server at error-observer.json's `port` and
|
|
134
|
+
// receives error/network reports from Chrome.
|
|
135
|
+
//
|
|
136
|
+
// CAVEAT — this is NOT the same as Chrome's CDP pidfile:
|
|
137
|
+
// - `.browser-cdp-{cdpPort}.pid` → Chrome browser process (CDP port)
|
|
138
|
+
// - `error-observer.pid` / json → Node daemon process (observer port)
|
|
139
|
+
// These are two distinct processes. Previous versions of this function
|
|
140
|
+
// looked at the Chrome pidfile and reported the observer "stopped" even
|
|
141
|
+
// when the daemon was alive, because the cdp-port pidfile may not be
|
|
142
|
+
// present when Chrome was started outside of claudeflow's openBrowser().
|
|
143
|
+
//
|
|
144
|
+
// Detection strategy (in order of preference):
|
|
145
|
+
// 1. error-observer.json exists → read its `port` + `pid_path`.
|
|
146
|
+
// 2. If pidfile exists and PID is alive → running=true.
|
|
147
|
+
// 3. Else, TCP probe the observer port via `lsof -i :PORT -sTCP:LISTEN -t`.
|
|
148
|
+
// The pidfile may be missing (race) but if SOMETHING is listening on
|
|
149
|
+
// the configured port, the daemon is up.
|
|
150
|
+
function readObserverState(cwd) {
|
|
151
|
+
const observerJsonPath = path.join(cwd, '.claudeflow', 'tmp', 'error-observer.json');
|
|
152
|
+
let meta;
|
|
138
153
|
try {
|
|
139
|
-
|
|
154
|
+
meta = JSON.parse(fs.readFileSync(observerJsonPath, 'utf8'));
|
|
140
155
|
} catch {
|
|
141
|
-
return { running: false, pid: null,
|
|
156
|
+
return { running: false, pid: null, port: null, source: 'no-observer-json', observerJsonPath };
|
|
157
|
+
}
|
|
158
|
+
if (!meta || typeof meta.port !== 'number') {
|
|
159
|
+
return { running: false, pid: null, port: null, source: 'observer-json-malformed', observerJsonPath };
|
|
160
|
+
}
|
|
161
|
+
const port = meta.port;
|
|
162
|
+
const pidPath = typeof meta.pid_path === 'string' ? meta.pid_path : null;
|
|
163
|
+
|
|
164
|
+
// Try the pidfile first — fastest path.
|
|
165
|
+
let pid = null;
|
|
166
|
+
if (pidPath) {
|
|
167
|
+
try {
|
|
168
|
+
const p = parseInt(fs.readFileSync(pidPath, 'utf8').trim(), 10);
|
|
169
|
+
if (Number.isFinite(p) && p > 0) pid = p;
|
|
170
|
+
} catch {}
|
|
171
|
+
}
|
|
172
|
+
if (pid !== null) {
|
|
173
|
+
let alive = false;
|
|
174
|
+
try { process.kill(pid, 0); alive = true; } catch (e) { alive = e.code === 'EPERM'; }
|
|
175
|
+
if (alive) return { running: true, pid, port, source: 'pidfile', observerJsonPath };
|
|
142
176
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
177
|
+
|
|
178
|
+
// Fallback: TCP probe via lsof. Catches the case where pidfile is missing
|
|
179
|
+
// but the daemon process is still listening on the port.
|
|
180
|
+
try {
|
|
181
|
+
const { spawnSync } = require('child_process');
|
|
182
|
+
const r = spawnSync('lsof', ['-i', `:${port}`, '-sTCP:LISTEN', '-t'], { encoding: 'utf8', timeout: 1500 });
|
|
183
|
+
if (r.status === 0 && r.stdout && r.stdout.trim()) {
|
|
184
|
+
const tcpPid = parseInt(r.stdout.trim().split('\n')[0], 10);
|
|
185
|
+
if (Number.isFinite(tcpPid) && tcpPid > 0) {
|
|
186
|
+
return { running: true, pid: tcpPid, port, source: 'tcp-probe', observerJsonPath };
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
} catch {}
|
|
190
|
+
|
|
191
|
+
return { running: false, pid: pid, port, source: 'no-listener', observerJsonPath };
|
|
147
192
|
}
|
|
148
193
|
|
|
149
194
|
// Surface as a doctor/panel issue when the browser-error-daemon observer
|
|
@@ -161,19 +206,25 @@ function checkObserverState(cwd) {
|
|
|
161
206
|
if (!hasPlaywright) {
|
|
162
207
|
return { id: 'observer-state', severity: 'info', message: 'Playwright not configured — observer not applicable.' };
|
|
163
208
|
}
|
|
164
|
-
const observer = readObserverState(cwd
|
|
209
|
+
const observer = readObserverState(cwd);
|
|
165
210
|
if (observer.running) {
|
|
166
211
|
return {
|
|
167
212
|
id: 'observer-state',
|
|
168
213
|
severity: 'ok',
|
|
169
|
-
message: `browser-error-daemon running (pid ${observer.pid}).`,
|
|
214
|
+
message: `browser-error-daemon running on port ${observer.port} (pid ${observer.pid}, detected via ${observer.source}).`,
|
|
170
215
|
};
|
|
171
216
|
}
|
|
217
|
+
// Distinguish "never started" (no observer JSON) from "started then died".
|
|
218
|
+
const detailMsg = observer.source === 'no-observer-json'
|
|
219
|
+
? 'no `.claudeflow/tmp/error-observer.json` — the daemon never started this session. Check `.claude/hooks/SessionStart/browser-error-daemon.js` for failures.'
|
|
220
|
+
: observer.source === 'observer-json-malformed'
|
|
221
|
+
? '`.claudeflow/tmp/error-observer.json` is malformed.'
|
|
222
|
+
: 'observer JSON exists (port ' + observer.port + ') but no process is listening — the daemon crashed after startup. Tail `.claude/tmp/hooks.log` for the most recent SessionStart entry.';
|
|
172
223
|
return {
|
|
173
224
|
id: 'observer-state',
|
|
174
225
|
severity: 'mismatch',
|
|
175
|
-
message:
|
|
176
|
-
detail: [{
|
|
226
|
+
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. Diagnosis: ${detailMsg}`,
|
|
227
|
+
detail: [{ port: observer.port, source: observer.source, observerJsonPath: observer.observerJsonPath }],
|
|
177
228
|
};
|
|
178
229
|
}
|
|
179
230
|
|
package/lib/panel.js
CHANGED
|
@@ -22,6 +22,7 @@ const {
|
|
|
22
22
|
checkObserverState,
|
|
23
23
|
readObserverState: readObserverStateFromDoctor,
|
|
24
24
|
readPlaywrightCdpEndpoint,
|
|
25
|
+
applyCdpPortFix,
|
|
25
26
|
} = require('./doctor.js');
|
|
26
27
|
const {
|
|
27
28
|
readOverrides,
|
|
@@ -195,7 +196,7 @@ function getMcpInfo(cwd) {
|
|
|
195
196
|
const servers = Object.keys(cfg.mcpServers || {});
|
|
196
197
|
const cdp = checkCdpPortMismatch(cwd);
|
|
197
198
|
const reading = readPlaywrightCdpEndpoint(mcpPath);
|
|
198
|
-
const observer = readObserverState(cwd
|
|
199
|
+
const observer = readObserverState(cwd);
|
|
199
200
|
const lockfiles = checkStaleLockfiles(cwd);
|
|
200
201
|
|
|
201
202
|
return {
|
|
@@ -205,6 +206,10 @@ function getMcpInfo(cwd) {
|
|
|
205
206
|
configured: reading.state === 'ok' ? reading.port : null,
|
|
206
207
|
computed: deriveCdpPort(cwd),
|
|
207
208
|
match: cdp.severity === 'ok',
|
|
209
|
+
// `fixable` is true only for the "wrong port" mismatch case — the
|
|
210
|
+
// applyCdpPortFix routine can rewrite the args entry. Other cases
|
|
211
|
+
// (invalid JSON, unparseable value) can't be auto-fixed by the panel.
|
|
212
|
+
fixable: cdp.severity === 'mismatch',
|
|
208
213
|
state: reading.state,
|
|
209
214
|
message: cdp.message,
|
|
210
215
|
},
|
|
@@ -1302,6 +1307,7 @@ function renderMcp() {
|
|
|
1302
1307
|
\${row('Playwright --cdp-endpoint port', String(m.playwright.configured || 'n/a'))}
|
|
1303
1308
|
\${row('Computed port (from path)', String(m.playwright.computed))}
|
|
1304
1309
|
\${row('Match', m.playwright.match ? 'YES' : 'NO', { kind: m.playwright.match ? 'ok' : 'err', text: m.playwright.match ? '✓' : '✗' })}
|
|
1310
|
+
\${m.playwright.fixable ? '<div style="padding:10px 14px;border-top:1px solid var(--border);"><button id="fix-cdp-port" class="btn-warn">Fix CDP port</button> <span class="muted" style="margin-left:10px;font-size:12px;">Rewrites .mcp.json --cdp-endpoint to localhost:'+m.playwright.computed+' (the port this machine derives from the cwd).</span></div>' : ''}
|
|
1305
1311
|
\${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
1312
|
\${!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>' : ''}
|
|
1307
1313
|
</div>
|
|
@@ -1416,8 +1422,32 @@ document.addEventListener('click', (e) => {
|
|
|
1416
1422
|
if (t.id === 'restart-observer') {
|
|
1417
1423
|
return restartObserverAction(t);
|
|
1418
1424
|
}
|
|
1425
|
+
if (t.id === 'fix-cdp-port') {
|
|
1426
|
+
return fixCdpPortAction(t);
|
|
1427
|
+
}
|
|
1419
1428
|
});
|
|
1420
1429
|
|
|
1430
|
+
async function fixCdpPortAction(btn) {
|
|
1431
|
+
const originalLabel = btn.textContent;
|
|
1432
|
+
btn.disabled = true;
|
|
1433
|
+
btn.textContent = 'Fixing…';
|
|
1434
|
+
try {
|
|
1435
|
+
const r = await fetch('/api/cdp-port/fix', { method: 'POST' });
|
|
1436
|
+
const result = await r.json();
|
|
1437
|
+
if (r.ok && result.ok) {
|
|
1438
|
+
showToast('CDP port fixed — .mcp.json rewritten. ' + (result.after && result.after.message || ''), 'ok');
|
|
1439
|
+
} else {
|
|
1440
|
+
showToast('Fix failed: ' + (result.error || 'unknown'), 'err');
|
|
1441
|
+
}
|
|
1442
|
+
} catch (e) {
|
|
1443
|
+
showToast('Network error: ' + e.message, 'err');
|
|
1444
|
+
} finally {
|
|
1445
|
+
btn.disabled = false;
|
|
1446
|
+
btn.textContent = originalLabel;
|
|
1447
|
+
await refresh();
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1421
1451
|
async function restartObserverAction(btn) {
|
|
1422
1452
|
const originalLabel = btn.textContent;
|
|
1423
1453
|
btn.disabled = true;
|
|
@@ -1428,7 +1458,16 @@ async function restartObserverAction(btn) {
|
|
|
1428
1458
|
if (r.ok && result.ok) {
|
|
1429
1459
|
showToast('Observer restarted — pid ' + (result.observer && result.observer.pid) + '. Refreshing…', 'ok');
|
|
1430
1460
|
} else {
|
|
1431
|
-
|
|
1461
|
+
// The daemon often exits 0 even on failure (it logs the reason to
|
|
1462
|
+
// hooks.log instead of stderr). Surface the richest signal we have.
|
|
1463
|
+
const detail = result.error
|
|
1464
|
+
|| (result.recent_log_entry ? result.recent_log_entry.split('\\n').slice(-3).join(' | ') : null)
|
|
1465
|
+
|| (result.script_stderr && result.script_stderr.trim())
|
|
1466
|
+
|| (result.script_stdout && result.script_stdout.trim())
|
|
1467
|
+
|| 'unknown — check .claude/tmp/hooks.log for the most recent SessionStart:browser-error-daemon entry';
|
|
1468
|
+
showToast('Restart failed: ' + detail, 'err');
|
|
1469
|
+
// Also log to browser console for inspection.
|
|
1470
|
+
console.error('[observer-restart] full response:', result);
|
|
1432
1471
|
}
|
|
1433
1472
|
} catch (e) {
|
|
1434
1473
|
showToast('Network error: ' + e.message, 'err');
|
|
@@ -1689,11 +1728,38 @@ function handler(cwd) {
|
|
|
1689
1728
|
return send(status, JSON.stringify(result), 'application/json');
|
|
1690
1729
|
}
|
|
1691
1730
|
|
|
1731
|
+
if (req.method === 'POST' && route === '/api/cdp-port/fix') {
|
|
1732
|
+
// Re-run checkCdpPortMismatch to get fresh state (the file may have
|
|
1733
|
+
// changed since the panel last fetched). Only apply the fix if the
|
|
1734
|
+
// check still reports severity='mismatch' (i.e. the wrong-port case
|
|
1735
|
+
// — other failure modes like invalid JSON cannot be auto-fixed).
|
|
1736
|
+
const check = checkCdpPortMismatch(cwd);
|
|
1737
|
+
if (check.severity !== 'mismatch') {
|
|
1738
|
+
return send(409, JSON.stringify({
|
|
1739
|
+
ok: false,
|
|
1740
|
+
error: `cdp-port check is "${check.severity}" — only "mismatch" is auto-fixable. Current message: ${check.message}`,
|
|
1741
|
+
check_severity: check.severity,
|
|
1742
|
+
}), 'application/json');
|
|
1743
|
+
}
|
|
1744
|
+
try {
|
|
1745
|
+
applyCdpPortFix(check);
|
|
1746
|
+
} catch (e) {
|
|
1747
|
+
return send(500, JSON.stringify({ ok: false, error: `applyCdpPortFix failed: ${e.message}` }), 'application/json');
|
|
1748
|
+
}
|
|
1749
|
+
const after = checkCdpPortMismatch(cwd);
|
|
1750
|
+
return send(200, JSON.stringify({
|
|
1751
|
+
ok: after.severity === 'ok',
|
|
1752
|
+
before: { severity: check.severity, message: check.message },
|
|
1753
|
+
after: { severity: after.severity, message: after.message },
|
|
1754
|
+
}), 'application/json');
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1692
1757
|
if (req.method === 'POST' && route === '/api/observer/restart') {
|
|
1693
1758
|
// Re-run the SessionStart/browser-error-daemon.js hook script
|
|
1694
1759
|
// synchronously. The script calls ensureObserver() which spawns
|
|
1695
1760
|
// Chrome with --cdp-endpoint as a detached process. Once the
|
|
1696
|
-
// script exits, the daemon is up (or the script logged the failure
|
|
1761
|
+
// script exits, the daemon is up (or the script logged the failure
|
|
1762
|
+
// to hooks.log even though it exits 0).
|
|
1697
1763
|
const daemonScript = path.join(cwd, '.claude', 'hooks', 'SessionStart', 'browser-error-daemon.js');
|
|
1698
1764
|
if (!fs.existsSync(daemonScript)) {
|
|
1699
1765
|
return send(404, JSON.stringify({ ok: false, error: 'daemon script not found at .claude/hooks/SessionStart/browser-error-daemon.js' }), 'application/json');
|
|
@@ -1703,15 +1769,42 @@ function handler(cwd) {
|
|
|
1703
1769
|
cwd,
|
|
1704
1770
|
env: { ...process.env, CLAUDE_PROJECT_DIR: cwd },
|
|
1705
1771
|
encoding: 'utf8',
|
|
1706
|
-
|
|
1772
|
+
// Chrome can take 5-10s to spawn cleanly; allow headroom + network probe.
|
|
1773
|
+
timeout: 30000,
|
|
1707
1774
|
});
|
|
1708
|
-
const observerState = readObserverState(cwd
|
|
1775
|
+
const observerState = readObserverState(cwd);
|
|
1776
|
+
// Tail the most recent SessionStart entry from hooks.log — the
|
|
1777
|
+
// daemon writes the failure reason there even when it exits 0.
|
|
1778
|
+
let recentLogEntry = null;
|
|
1779
|
+
try {
|
|
1780
|
+
const logPath = path.join(cwd, '.claude', 'tmp', 'hooks.log');
|
|
1781
|
+
const raw = fs.readFileSync(logPath, 'utf8');
|
|
1782
|
+
const lines = raw.split('\n');
|
|
1783
|
+
// Walk backwards, find the most recent block whose hook_file
|
|
1784
|
+
// mentions browser-error-daemon.js.
|
|
1785
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
1786
|
+
if (/browser-error-daemon\.js/.test(lines[i])) {
|
|
1787
|
+
recentLogEntry = lines.slice(Math.max(0, i - 1), Math.min(lines.length, i + 5)).join('\n');
|
|
1788
|
+
break;
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
} catch {}
|
|
1709
1792
|
const ok = observerState.running;
|
|
1793
|
+
const reason = !ok
|
|
1794
|
+
? (r.error ? `spawn error: ${r.error.message}` :
|
|
1795
|
+
r.signal ? `script killed by signal ${r.signal} (timed out?)` :
|
|
1796
|
+
r.status !== 0 ? `script exit ${r.status}` :
|
|
1797
|
+
'script exited 0 but observer pid still not alive — the daemon failed to bind Chrome to the CDP port. See script_stdout + recent_log_entry below.')
|
|
1798
|
+
: null;
|
|
1710
1799
|
return send(ok ? 200 : 500, JSON.stringify({
|
|
1711
1800
|
ok,
|
|
1712
1801
|
observer: observerState,
|
|
1713
1802
|
script_exit: r.status,
|
|
1714
|
-
|
|
1803
|
+
script_signal: r.signal || null,
|
|
1804
|
+
script_stdout: (r.stdout || '').slice(0, 2000),
|
|
1805
|
+
script_stderr: (r.stderr || '').slice(0, 2000),
|
|
1806
|
+
recent_log_entry: recentLogEntry,
|
|
1807
|
+
error: reason,
|
|
1715
1808
|
}), 'application/json');
|
|
1716
1809
|
}
|
|
1717
1810
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axiomatic-labs/claudeflow",
|
|
3
|
-
"version": "2.13.
|
|
3
|
+
"version": "2.13.59",
|
|
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"
|