@axiomatic-labs/claudeflow 2.13.15 → 2.13.16

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/bin/cli.js CHANGED
@@ -19,7 +19,7 @@
19
19
  const fs = require('fs');
20
20
  const path = require('path');
21
21
 
22
- const SUBCOMMANDS = new Set(['install', 'version', '--version', '-v', 'help', '--help', '-h']);
22
+ const SUBCOMMANDS = new Set(['install', 'version', '--version', '-v', 'help', '--help', '-h', 'doctor']);
23
23
 
24
24
  function inClaudeflowProject(startDir) {
25
25
  let dir = path.resolve(startDir);
@@ -47,6 +47,11 @@ async function runSubcommand(command) {
47
47
  await version();
48
48
  return;
49
49
  }
50
+ case 'doctor': {
51
+ const doctor = require('../lib/doctor.js');
52
+ const code = await doctor(process.argv.slice(3));
53
+ process.exit(code || 0);
54
+ }
50
55
  case 'help':
51
56
  case '--help':
52
57
  case '-h':
@@ -57,6 +62,7 @@ async function runSubcommand(command) {
57
62
  console.log('');
58
63
  console.log(' Claudeflow commands:');
59
64
  console.log(` ${ui.CYAN}install${ui.RESET} Install or update Claudeflow in the current project`);
65
+ console.log(` ${ui.CYAN}doctor${ui.RESET} Diagnose local issues (CDP port, stale lockfiles); add --fix to repair`);
60
66
  console.log(` ${ui.CYAN}version${ui.RESET} Show version info`);
61
67
  console.log(` ${ui.CYAN}help${ui.RESET} Show this message`);
62
68
  console.log('');
package/lib/doctor.js ADDED
@@ -0,0 +1,207 @@
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
+ const match = /localhost:(\d+)/.exec(value || '');
46
+ if (!match) return { state: 'unparseable', value, parsed };
47
+ return { state: 'ok', port: Number(match[1]), flagIdx, parsed };
48
+ }
49
+
50
+ function checkCdpPortMismatch(cwd) {
51
+ const mcpPath = path.join(cwd, '.mcp.json');
52
+ const expected = deriveCdpPort(cwd);
53
+ const reading = readPlaywrightCdpEndpoint(mcpPath);
54
+
55
+ switch (reading.state) {
56
+ case 'missing-file':
57
+ return { id: 'cdp-port', severity: 'info', message: 'No .mcp.json in cwd — skipping.' };
58
+ case 'invalid-json':
59
+ return { id: 'cdp-port', severity: 'error', message: `.mcp.json is invalid JSON: ${reading.error}` };
60
+ case 'no-playwright':
61
+ return { id: 'cdp-port', severity: 'info', message: '.mcp.json has no `playwright` entry — skipping.' };
62
+ case 'no-flag':
63
+ return { id: 'cdp-port', severity: 'info', message: 'Playwright MCP has no `--cdp-endpoint` flag — observer-MCP wiring not in use.' };
64
+ case 'unparseable':
65
+ return { id: 'cdp-port', severity: 'error', message: `Unparseable --cdp-endpoint value: ${reading.value}` };
66
+ case 'ok':
67
+ if (reading.port === expected) {
68
+ return { id: 'cdp-port', severity: 'ok', message: `CDP port matches (${expected}).` };
69
+ }
70
+ return {
71
+ id: 'cdp-port',
72
+ severity: 'mismatch',
73
+ message: `CDP port mismatch — .mcp.json has ${reading.port}, this machine derives ${expected} from ${cwd}.`,
74
+ fix: { mcpPath, expected, parsed: reading.parsed, flagIdx: reading.flagIdx },
75
+ };
76
+ }
77
+ }
78
+
79
+ function applyCdpPortFix(check) {
80
+ const { mcpPath, expected, parsed, flagIdx } = check.fix;
81
+ parsed.mcpServers.playwright.args[flagIdx + 1] = `http://localhost:${expected}`;
82
+ fs.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + '\n');
83
+ }
84
+
85
+ function listStaleLockfiles(cwd) {
86
+ const dir = path.join(cwd, '.claudeflow', 'tmp');
87
+ let entries;
88
+ try {
89
+ entries = fs.readdirSync(dir);
90
+ } catch {
91
+ return [];
92
+ }
93
+ const stale = [];
94
+ for (const name of entries) {
95
+ if (!/^\.browser-cdp-\d+\.pid$/.test(name)) continue;
96
+ const full = path.join(dir, name);
97
+ let pid;
98
+ try {
99
+ pid = parseInt(fs.readFileSync(full, 'utf8').trim(), 10);
100
+ } catch {
101
+ stale.push({ file: full, reason: 'unreadable' });
102
+ continue;
103
+ }
104
+ if (!Number.isFinite(pid) || pid <= 0) {
105
+ stale.push({ file: full, reason: 'invalid-pid', pid });
106
+ continue;
107
+ }
108
+ try {
109
+ process.kill(pid, 0);
110
+ } catch (err) {
111
+ if (err.code === 'ESRCH') stale.push({ file: full, reason: 'dead-pid', pid });
112
+ // EPERM means the PID exists but we can't signal it — treat as alive.
113
+ }
114
+ }
115
+ return stale;
116
+ }
117
+
118
+ function checkStaleLockfiles(cwd) {
119
+ const stale = listStaleLockfiles(cwd);
120
+ if (stale.length === 0) {
121
+ return { id: 'stale-lockfile', severity: 'ok', message: 'No stale browser lockfiles.' };
122
+ }
123
+ return {
124
+ id: 'stale-lockfile',
125
+ severity: 'mismatch',
126
+ message: `${stale.length} stale browser lockfile(s) — Chrome PIDs no longer alive.`,
127
+ detail: stale,
128
+ fix: { stale },
129
+ };
130
+ }
131
+
132
+ function applyStaleLockfileFix(check) {
133
+ for (const { file } of check.fix.stale) {
134
+ try { fs.unlinkSync(file); } catch {}
135
+ }
136
+ }
137
+
138
+ const SEVERITY_TAG = {
139
+ ok: `${ui.GREEN}✓${ui.RESET}`,
140
+ info: `${ui.DIM}·${ui.RESET}`,
141
+ mismatch: `${ui.YELLOW}!${ui.RESET}`,
142
+ error: `${ui.RED}✗${ui.RESET}`,
143
+ };
144
+
145
+ function printCheck(check) {
146
+ const tag = SEVERITY_TAG[check.severity] || '?';
147
+ console.log(` ${tag} [${check.id}] ${check.message}`);
148
+ if (check.detail && Array.isArray(check.detail)) {
149
+ for (const d of check.detail) {
150
+ console.log(` ${ui.DIM}- ${d.file} (${d.reason}${d.pid ? `, pid=${d.pid}` : ''})${ui.RESET}`);
151
+ }
152
+ }
153
+ }
154
+
155
+ async function run(argv = []) {
156
+ const fixMode = argv.includes('--fix');
157
+ const cwd = process.cwd();
158
+
159
+ ui.banner();
160
+ console.log(` Diagnosing ${ui.CYAN}${cwd}${ui.RESET}${fixMode ? ` ${ui.YELLOW}(--fix)${ui.RESET}` : ''}`);
161
+ console.log('');
162
+
163
+ const checks = [
164
+ checkCdpPortMismatch(cwd),
165
+ checkStaleLockfiles(cwd),
166
+ ];
167
+
168
+ for (const check of checks) printCheck(check);
169
+
170
+ const fixable = checks.filter(c => c.fix);
171
+ if (fixable.length === 0) {
172
+ console.log('');
173
+ console.log(` ${ui.GREEN}All checks passed.${ui.RESET}`);
174
+ return 0;
175
+ }
176
+
177
+ if (!fixMode) {
178
+ console.log('');
179
+ console.log(` ${ui.YELLOW}${fixable.length} issue(s) detected.${ui.RESET} Re-run with ${ui.CYAN}claudeflow doctor --fix${ui.RESET} to repair.`);
180
+ return 1;
181
+ }
182
+
183
+ console.log('');
184
+ console.log(` ${ui.CYAN}Applying fixes...${ui.RESET}`);
185
+ for (const check of fixable) {
186
+ if (check.id === 'cdp-port') {
187
+ applyCdpPortFix(check);
188
+ console.log(` ${ui.GREEN}✓${ui.RESET} Rewrote .mcp.json playwright --cdp-endpoint → ${check.fix.expected}`);
189
+ console.log(` ${ui.YELLOW}Do NOT commit this change${ui.RESET} — the port is local to this machine's path.`);
190
+ console.log(` Consider: ${ui.CYAN}git update-index --skip-worktree .mcp.json${ui.RESET}`);
191
+ } else if (check.id === 'stale-lockfile') {
192
+ applyStaleLockfileFix(check);
193
+ console.log(` ${ui.GREEN}✓${ui.RESET} Removed ${check.fix.stale.length} stale lockfile(s).`);
194
+ }
195
+ }
196
+ console.log('');
197
+ console.log(` ${ui.GREEN}Done.${ui.RESET} Reconnect MCP (${ui.CYAN}/mcp${ui.RESET} in Claude Code) to pick up changes.`);
198
+ return 0;
199
+ }
200
+
201
+ module.exports = run;
202
+ module.exports.deriveCdpPort = deriveCdpPort;
203
+ module.exports.checkCdpPortMismatch = checkCdpPortMismatch;
204
+ module.exports.checkStaleLockfiles = checkStaleLockfiles;
205
+ module.exports.readPlaywrightCdpEndpoint = readPlaywrightCdpEndpoint;
206
+ module.exports.applyCdpPortFix = applyCdpPortFix;
207
+ module.exports.applyStaleLockfileFix = applyStaleLockfileFix;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axiomatic-labs/claudeflow",
3
- "version": "2.13.15",
3
+ "version": "2.13.16",
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"