@axiomatic-labs/claudeflow 2.33.1 → 2.33.2

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 (3) hide show
  1. package/package.json +8 -3
  2. package/lib/doctor.js +0 -376
  3. package/lib/panel.js +0 -2628
package/package.json CHANGED
@@ -1,13 +1,18 @@
1
1
  {
2
2
  "name": "@axiomatic-labs/claudeflow",
3
- "version": "2.33.1",
3
+ "version": "2.33.2",
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"
7
7
  },
8
8
  "files": [
9
- "bin/",
10
- "lib/",
9
+ "bin/cli.js",
10
+ "lib/auth.js",
11
+ "lib/claude-passthrough.js",
12
+ "lib/download.js",
13
+ "lib/install.js",
14
+ "lib/ui.js",
15
+ "lib/version.js",
11
16
  "README.md"
12
17
  ],
13
18
  "keywords": [
package/lib/doctor.js DELETED
@@ -1,376 +0,0 @@
1
- // `claudeflow doctor` — diagnose and optionally repair common local-machine
2
- // issues that don't surface until something silently fails.
3
- //
4
- // Scope (intentionally narrow):
5
- // 1. CDP port mismatch — `.mcp.json` ships a `--cdp-endpoint` derived from
6
- // the path of whoever ran `claudeflow init`. After a clone, the local
7
- // observer derives a different port from the new path, so the Playwright
8
- // MCP can't reach the observer's Chrome.
9
- // 2. Stale browser PID lockfile — `openBrowser()` writes
10
- // `.claudeflow/tmp/.browser-cdp-<port>.pid` and skips relaunch if the PID
11
- // is alive. If Chrome is killed externally (pkill) the lockfile lingers
12
- // and blocks subsequent launches.
13
- //
14
- // Read-only by default. `--fix` applies repairs after reporting them.
15
-
16
- const fs = require('fs');
17
- const path = require('path');
18
- const crypto = require('crypto');
19
-
20
- const ui = require('./ui.js');
21
-
22
- function deriveCdpPort(projectPath) {
23
- const hash = crypto.createHash('sha1').update(projectPath).digest();
24
- return 9200 + (hash.readUInt16BE(2) % 100);
25
- }
26
-
27
- function readPlaywrightCdpEndpoint(mcpPath) {
28
- let raw;
29
- try {
30
- raw = fs.readFileSync(mcpPath, 'utf8');
31
- } catch {
32
- return { state: 'missing-file' };
33
- }
34
- let parsed;
35
- try {
36
- parsed = JSON.parse(raw);
37
- } catch (err) {
38
- return { state: 'invalid-json', error: err.message };
39
- }
40
- const entry = parsed && parsed.mcpServers && parsed.mcpServers.playwright;
41
- if (!entry || !Array.isArray(entry.args)) return { state: 'no-playwright' };
42
- const flagIdx = entry.args.indexOf('--cdp-endpoint');
43
- if (flagIdx === -1) return { state: 'no-flag', parsed };
44
- const value = entry.args[flagIdx + 1];
45
- // Accept localhost OR 127.0.0.1 (v2.13.72+ writes the latter explicitly to
46
- // dodge the IPv4/IPv6 dual-daemon trap — see observer-loopback lint test).
47
- const match = /(?:localhost|127\.0\.0\.1):(\d+)/.exec(value || '');
48
- if (!match) return { state: 'unparseable', value, parsed };
49
- return { state: 'ok', port: Number(match[1]), flagIdx, parsed };
50
- }
51
-
52
- function checkCdpPortMismatch(cwd) {
53
- const mcpPath = path.join(cwd, '.mcp.json');
54
- const expected = deriveCdpPort(cwd);
55
- const reading = readPlaywrightCdpEndpoint(mcpPath);
56
-
57
- switch (reading.state) {
58
- case 'missing-file':
59
- return { id: 'cdp-port', severity: 'info', message: 'No .mcp.json in cwd — skipping.' };
60
- case 'invalid-json':
61
- return { id: 'cdp-port', severity: 'error', message: `.mcp.json is invalid JSON: ${reading.error}` };
62
- case 'no-playwright':
63
- return { id: 'cdp-port', severity: 'info', message: '.mcp.json has no `playwright` entry — skipping.' };
64
- case 'no-flag':
65
- return { id: 'cdp-port', severity: 'info', message: 'Playwright MCP has no `--cdp-endpoint` flag — observer-MCP wiring not in use.' };
66
- case 'unparseable':
67
- return { id: 'cdp-port', severity: 'error', message: `Unparseable --cdp-endpoint value: ${reading.value}` };
68
- case 'ok':
69
- if (reading.port === expected) {
70
- return { id: 'cdp-port', severity: 'ok', message: `CDP port matches (${expected}).` };
71
- }
72
- return {
73
- id: 'cdp-port',
74
- severity: 'mismatch',
75
- message: `CDP port mismatch — .mcp.json has ${reading.port}, this machine derives ${expected} from ${cwd}.`,
76
- fix: { mcpPath, expected, parsed: reading.parsed, flagIdx: reading.flagIdx },
77
- };
78
- }
79
- }
80
-
81
- function applyCdpPortFix(check) {
82
- const { mcpPath, expected, parsed, flagIdx } = check.fix;
83
- parsed.mcpServers.playwright.args[flagIdx + 1] = `http://127.0.0.1:${expected}`;
84
- fs.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + '\n');
85
- }
86
-
87
- function listStaleLockfiles(cwd) {
88
- const dir = path.join(cwd, '.claudeflow', 'tmp');
89
- let entries;
90
- try {
91
- entries = fs.readdirSync(dir);
92
- } catch {
93
- return [];
94
- }
95
- const stale = [];
96
- for (const name of entries) {
97
- if (!/^\.browser-cdp-\d+\.pid$/.test(name)) continue;
98
- const full = path.join(dir, name);
99
- let pid;
100
- try {
101
- pid = parseInt(fs.readFileSync(full, 'utf8').trim(), 10);
102
- } catch {
103
- stale.push({ file: full, reason: 'unreadable' });
104
- continue;
105
- }
106
- if (!Number.isFinite(pid) || pid <= 0) {
107
- stale.push({ file: full, reason: 'invalid-pid', pid });
108
- continue;
109
- }
110
- try {
111
- process.kill(pid, 0);
112
- } catch (err) {
113
- if (err.code === 'ESRCH') stale.push({ file: full, reason: 'dead-pid', pid });
114
- // EPERM means the PID exists but we can't signal it — treat as alive.
115
- }
116
- }
117
- return stale;
118
- }
119
-
120
- function checkStaleLockfiles(cwd) {
121
- const stale = listStaleLockfiles(cwd);
122
- if (stale.length === 0) {
123
- return { id: 'stale-lockfile', severity: 'ok', message: 'No stale browser lockfiles.' };
124
- }
125
- return {
126
- id: 'stale-lockfile',
127
- severity: 'mismatch',
128
- message: `${stale.length} stale browser lockfile(s) — Chrome PIDs no longer alive.`,
129
- detail: stale,
130
- fix: { stale },
131
- };
132
- }
133
-
134
- // Read the browser-error-daemon observer state. The daemon is the Node
135
- // process that hosts the HTTP server at error-observer.json's `port` and
136
- // receives error/network reports from Chrome.
137
- //
138
- // CAVEAT — this is NOT the same as Chrome's CDP pidfile:
139
- // - `.browser-cdp-{cdpPort}.pid` → Chrome browser process (CDP port)
140
- // - `error-observer.pid` / json → Node daemon process (observer port)
141
- // These are two distinct processes. Previous versions of this function
142
- // looked at the Chrome pidfile and reported the observer "stopped" even
143
- // when the daemon was alive, because the cdp-port pidfile may not be
144
- // present when Chrome was started outside of claudeflow's openBrowser().
145
- //
146
- // Detection strategy (in order of preference):
147
- // 1. error-observer.json exists → read its `port` + `pid_path`.
148
- // 2. If pidfile exists and PID is alive → running=true.
149
- // 3. Else, TCP probe the observer port via `lsof -i :PORT -sTCP:LISTEN -t`.
150
- // The pidfile may be missing (race) but if SOMETHING is listening on
151
- // the configured port, the daemon is up.
152
- function readObserverState(cwd) {
153
- const observerJsonPath = path.join(cwd, '.claudeflow', 'tmp', 'error-observer.json');
154
- // Defense-in-depth retry: even though the daemon now writes atomically
155
- // (rename-after-write), older daemon versions and any future non-atomic
156
- // writer would expose a 0-byte window during overwrite. We retry up to
157
- // 3 times with a tiny pause between attempts so a transient empty read
158
- // doesn't flip the panel UI between running/stopped every refresh.
159
- let meta = null;
160
- let lastErr = null;
161
- for (let attempt = 0; attempt < 3; attempt += 1) {
162
- try {
163
- const raw = fs.readFileSync(observerJsonPath, 'utf8');
164
- if (raw.trim()) {
165
- meta = JSON.parse(raw);
166
- break;
167
- }
168
- lastErr = new Error('empty file');
169
- } catch (e) {
170
- lastErr = e;
171
- if (e.code === 'ENOENT') break; // file genuinely missing — no retry helps
172
- }
173
- if (attempt < 2) {
174
- // Sync sleep (~30ms) via spawnSync — sufficient to outlast a writeFile race.
175
- const { spawnSync } = require('child_process');
176
- spawnSync('sleep', ['0.03']);
177
- }
178
- }
179
- if (!meta) {
180
- const source = (lastErr && lastErr.code === 'ENOENT') ? 'no-observer-json' : 'observer-json-malformed';
181
- return { running: false, pid: null, port: null, source, observerJsonPath };
182
- }
183
- if (typeof meta.port !== 'number') {
184
- return { running: false, pid: null, port: null, source: 'observer-json-malformed', observerJsonPath };
185
- }
186
- const port = meta.port;
187
- const pidPath = typeof meta.pid_path === 'string' ? meta.pid_path : null;
188
-
189
- // Try the pidfile first — fastest path.
190
- let pid = null;
191
- if (pidPath) {
192
- try {
193
- const p = parseInt(fs.readFileSync(pidPath, 'utf8').trim(), 10);
194
- if (Number.isFinite(p) && p > 0) pid = p;
195
- } catch {}
196
- }
197
- if (pid !== null) {
198
- let alive = false;
199
- try { process.kill(pid, 0); alive = true; } catch (e) { alive = e.code === 'EPERM'; }
200
- if (alive) return { running: true, pid, port, source: 'pidfile', observerJsonPath };
201
- }
202
-
203
- // Fallback: TCP probe via lsof. Catches the case where pidfile is missing
204
- // but the daemon process is still listening on the port.
205
- try {
206
- const { spawnSync } = require('child_process');
207
- const r = spawnSync('lsof', ['-i', `:${port}`, '-sTCP:LISTEN', '-t'], { encoding: 'utf8', timeout: 1500 });
208
- if (r.status === 0 && r.stdout && r.stdout.trim()) {
209
- const tcpPid = parseInt(r.stdout.trim().split('\n')[0], 10);
210
- if (Number.isFinite(tcpPid) && tcpPid > 0) {
211
- return { running: true, pid: tcpPid, port, source: 'tcp-probe', observerJsonPath };
212
- }
213
- }
214
- } catch {}
215
-
216
- return { running: false, pid: pid, port, source: 'no-listener', observerJsonPath };
217
- }
218
-
219
- // Surface as a doctor/panel issue when the browser-error-daemon observer
220
- // is stopped while Playwright is configured. The observer collects console
221
- // errors + network failures during headed browser verification — when it's
222
- // down, those signals are silently lost, so the agent may verify a flow
223
- // that has console errors and not know it.
224
- function checkObserverState(cwd) {
225
- const mcpPath = path.join(cwd, '.mcp.json');
226
- let cfg;
227
- try { cfg = JSON.parse(fs.readFileSync(mcpPath, 'utf8')); }
228
- catch { return { id: 'observer-state', severity: 'info', message: 'No .mcp.json — observer not applicable.' }; }
229
- const hasPlaywright = cfg.mcpServers
230
- && Object.keys(cfg.mcpServers).some((s) => s.toLowerCase().includes('playwright'));
231
- if (!hasPlaywright) {
232
- return { id: 'observer-state', severity: 'info', message: 'Playwright not configured — observer not applicable.' };
233
- }
234
- const observer = readObserverState(cwd);
235
- if (observer.running) {
236
- return {
237
- id: 'observer-state',
238
- severity: 'ok',
239
- message: `browser-error-daemon running on port ${observer.port} (pid ${observer.pid}, detected via ${observer.source}).`,
240
- };
241
- }
242
- // Distinguish "never started" (no observer JSON) from "started then died".
243
- const detailMsg = observer.source === 'no-observer-json'
244
- ? 'no `.claudeflow/tmp/error-observer.json` — the daemon never started this session. Check `.claude/hooks/SessionStart/browser-error-daemon.js` for failures.'
245
- : observer.source === 'observer-json-malformed'
246
- ? '`.claudeflow/tmp/error-observer.json` is malformed.'
247
- : '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.';
248
- return {
249
- id: 'observer-state',
250
- severity: 'mismatch',
251
- 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}`,
252
- detail: [{ port: observer.port, source: observer.source, observerJsonPath: observer.observerJsonPath }],
253
- };
254
- }
255
-
256
- // Detects more than one process listening on the observer port. macOS allows
257
- // distinct sockets on the same port for IPv4 and IPv6, so a stale daemon
258
- // bound to "localhost" (which old code resolved to ::1) could coexist with a
259
- // fresh daemon bound to 127.0.0.1. Snapshots routed via `localhost` then
260
- // landed on the stale daemon, which carried the pre-v2.13.71 auto-resolve
261
- // bug — panel saw the new daemon's empty state, hook saw 1 incident. This
262
- // check surfaces that condition explicitly.
263
- function checkObserverDualBind(cwd) {
264
- const observer = readObserverState(cwd);
265
- if (!observer.port) {
266
- return { id: 'observer-dual-bind', severity: 'info', message: 'No observer port known — skipping dual-bind check.' };
267
- }
268
- const port = observer.port;
269
- let listeners = [];
270
- try {
271
- const { spawnSync } = require('child_process');
272
- const r = spawnSync('lsof', [`-iTCP:${port}`, '-sTCP:LISTEN', '-P', '-n', '-F', 'pn'], { encoding: 'utf8', timeout: 1500 });
273
- if (r.status === 0 && r.stdout) {
274
- let currentPid = null;
275
- for (const line of r.stdout.split('\n')) {
276
- if (line.startsWith('p')) currentPid = Number(line.slice(1));
277
- else if (line.startsWith('n') && currentPid) listeners.push({ pid: currentPid, name: line.slice(1) });
278
- }
279
- }
280
- } catch {
281
- return { id: 'observer-dual-bind', severity: 'info', message: 'lsof unavailable — cannot verify single-bind on observer port.' };
282
- }
283
- const uniquePids = new Set(listeners.map((l) => l.pid));
284
- if (uniquePids.size <= 1) {
285
- return { id: 'observer-dual-bind', severity: 'ok', message: `Observer port ${port} has a single listener.` };
286
- }
287
- return {
288
- id: 'observer-dual-bind',
289
- severity: 'error',
290
- message: `Observer port ${port} has ${uniquePids.size} distinct processes listening (PIDs: ${[...uniquePids].join(', ')}). One is likely a stale daemon from a previous version. Browser snapshots may route to the wrong instance, causing the panel and the Stop hook to disagree. Fix: stop the older PID (\`kill <pid>\`) and let the SessionStart hook respawn a single daemon.`,
291
- detail: listeners.map((l) => ({ file: `pid ${l.pid}`, reason: l.name })),
292
- fix: { kind: 'dual-bind', listeners },
293
- };
294
- }
295
-
296
- function applyStaleLockfileFix(check) {
297
- for (const { file } of check.fix.stale) {
298
- try { fs.unlinkSync(file); } catch {}
299
- }
300
- }
301
-
302
- const SEVERITY_TAG = {
303
- ok: `${ui.GREEN}✓${ui.RESET}`,
304
- info: `${ui.DIM}·${ui.RESET}`,
305
- mismatch: `${ui.YELLOW}!${ui.RESET}`,
306
- error: `${ui.RED}✗${ui.RESET}`,
307
- };
308
-
309
- function printCheck(check) {
310
- const tag = SEVERITY_TAG[check.severity] || '?';
311
- console.log(` ${tag} [${check.id}] ${check.message}`);
312
- if (check.detail && Array.isArray(check.detail)) {
313
- for (const d of check.detail) {
314
- console.log(` ${ui.DIM}- ${d.file} (${d.reason}${d.pid ? `, pid=${d.pid}` : ''})${ui.RESET}`);
315
- }
316
- }
317
- }
318
-
319
- async function run(argv = []) {
320
- const fixMode = argv.includes('--fix');
321
- const cwd = process.cwd();
322
-
323
- ui.banner();
324
- console.log(` Diagnosing ${ui.CYAN}${cwd}${ui.RESET}${fixMode ? ` ${ui.YELLOW}(--fix)${ui.RESET}` : ''}`);
325
- console.log('');
326
-
327
- const checks = [
328
- checkCdpPortMismatch(cwd),
329
- checkStaleLockfiles(cwd),
330
- checkObserverState(cwd),
331
- checkObserverDualBind(cwd),
332
- ];
333
-
334
- for (const check of checks) printCheck(check);
335
-
336
- const fixable = checks.filter(c => c.fix);
337
- if (fixable.length === 0) {
338
- console.log('');
339
- console.log(` ${ui.GREEN}All checks passed.${ui.RESET}`);
340
- return 0;
341
- }
342
-
343
- if (!fixMode) {
344
- console.log('');
345
- console.log(` ${ui.YELLOW}${fixable.length} issue(s) detected.${ui.RESET} Re-run with ${ui.CYAN}claudeflow doctor --fix${ui.RESET} to repair.`);
346
- return 1;
347
- }
348
-
349
- console.log('');
350
- console.log(` ${ui.CYAN}Applying fixes...${ui.RESET}`);
351
- for (const check of fixable) {
352
- if (check.id === 'cdp-port') {
353
- applyCdpPortFix(check);
354
- console.log(` ${ui.GREEN}✓${ui.RESET} Rewrote .mcp.json playwright --cdp-endpoint → ${check.fix.expected}`);
355
- console.log(` ${ui.YELLOW}Do NOT commit this change${ui.RESET} — the port is local to this machine's path.`);
356
- console.log(` Consider: ${ui.CYAN}git update-index --skip-worktree .mcp.json${ui.RESET}`);
357
- } else if (check.id === 'stale-lockfile') {
358
- applyStaleLockfileFix(check);
359
- console.log(` ${ui.GREEN}✓${ui.RESET} Removed ${check.fix.stale.length} stale lockfile(s).`);
360
- }
361
- }
362
- console.log('');
363
- console.log(` ${ui.GREEN}Done.${ui.RESET} Reconnect MCP (${ui.CYAN}/mcp${ui.RESET} in Claude Code) to pick up changes.`);
364
- return 0;
365
- }
366
-
367
- module.exports = run;
368
- module.exports.deriveCdpPort = deriveCdpPort;
369
- module.exports.checkCdpPortMismatch = checkCdpPortMismatch;
370
- module.exports.checkStaleLockfiles = checkStaleLockfiles;
371
- module.exports.checkObserverState = checkObserverState;
372
- module.exports.checkObserverDualBind = checkObserverDualBind;
373
- module.exports.readObserverState = readObserverState;
374
- module.exports.readPlaywrightCdpEndpoint = readPlaywrightCdpEndpoint;
375
- module.exports.applyCdpPortFix = applyCdpPortFix;
376
- module.exports.applyStaleLockfileFix = applyStaleLockfileFix;