@axiomatic-labs/claudeflow 2.13.58 → 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 +43 -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
|
@@ -196,7 +196,7 @@ function getMcpInfo(cwd) {
|
|
|
196
196
|
const servers = Object.keys(cfg.mcpServers || {});
|
|
197
197
|
const cdp = checkCdpPortMismatch(cwd);
|
|
198
198
|
const reading = readPlaywrightCdpEndpoint(mcpPath);
|
|
199
|
-
const observer = readObserverState(cwd
|
|
199
|
+
const observer = readObserverState(cwd);
|
|
200
200
|
const lockfiles = checkStaleLockfiles(cwd);
|
|
201
201
|
|
|
202
202
|
return {
|
|
@@ -1458,7 +1458,16 @@ async function restartObserverAction(btn) {
|
|
|
1458
1458
|
if (r.ok && result.ok) {
|
|
1459
1459
|
showToast('Observer restarted — pid ' + (result.observer && result.observer.pid) + '. Refreshing…', 'ok');
|
|
1460
1460
|
} else {
|
|
1461
|
-
|
|
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);
|
|
1462
1471
|
}
|
|
1463
1472
|
} catch (e) {
|
|
1464
1473
|
showToast('Network error: ' + e.message, 'err');
|
|
@@ -1749,7 +1758,8 @@ function handler(cwd) {
|
|
|
1749
1758
|
// Re-run the SessionStart/browser-error-daemon.js hook script
|
|
1750
1759
|
// synchronously. The script calls ensureObserver() which spawns
|
|
1751
1760
|
// Chrome with --cdp-endpoint as a detached process. Once the
|
|
1752
|
-
// 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).
|
|
1753
1763
|
const daemonScript = path.join(cwd, '.claude', 'hooks', 'SessionStart', 'browser-error-daemon.js');
|
|
1754
1764
|
if (!fs.existsSync(daemonScript)) {
|
|
1755
1765
|
return send(404, JSON.stringify({ ok: false, error: 'daemon script not found at .claude/hooks/SessionStart/browser-error-daemon.js' }), 'application/json');
|
|
@@ -1759,15 +1769,42 @@ function handler(cwd) {
|
|
|
1759
1769
|
cwd,
|
|
1760
1770
|
env: { ...process.env, CLAUDE_PROJECT_DIR: cwd },
|
|
1761
1771
|
encoding: 'utf8',
|
|
1762
|
-
|
|
1772
|
+
// Chrome can take 5-10s to spawn cleanly; allow headroom + network probe.
|
|
1773
|
+
timeout: 30000,
|
|
1763
1774
|
});
|
|
1764
|
-
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 {}
|
|
1765
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;
|
|
1766
1799
|
return send(ok ? 200 : 500, JSON.stringify({
|
|
1767
1800
|
ok,
|
|
1768
1801
|
observer: observerState,
|
|
1769
1802
|
script_exit: r.status,
|
|
1770
|
-
|
|
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,
|
|
1771
1808
|
}), 'application/json');
|
|
1772
1809
|
}
|
|
1773
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"
|