@axiomatic-labs/claudeflow 2.13.15 → 2.13.17

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/lib/install.js CHANGED
@@ -15,6 +15,8 @@ const REQUIRED_TEMPLATE_RULES = [];
15
15
  const LEGACY_TEMPLATE_RULES = ['claudeflow-implementation.md', 'example-rules.md'];
16
16
  const SHARED_APPEND_PROMPT_TEMPLATE = path.join('.claudeflow', 'templates', 'claudeflow-core-system-prompt.md');
17
17
  const SHARED_APPEND_PROMPT_OUTPUT = 'append-system-prompt.md';
18
+ const DEFAULT_CLAUDE_MD_TEMPLATE = path.join('.claudeflow', 'templates', 'claudeflow-default-claude-md.md');
19
+ const DEFAULT_CLAUDE_MD_OUTPUT = 'CLAUDE.md';
18
20
  const TEMPLATE_MANAGED_SETTINGS_KEYS = ['$schema', 'enabledMcpjsonServers', 'env', 'permissions', 'hooks'];
19
21
  const SERENA_REPO = 'git+https://github.com/oraios/serena';
20
22
  const TEMPLATE_SEEDED_SETTINGS_KEYS = new Set(['statusLine']);
@@ -152,6 +154,7 @@ async function run() {
152
154
  copyDirSync(srcTemplates, dstTemplates);
153
155
  }
154
156
  materializeSharedAppendPrompt(cwd);
157
+ materializeDefaultClaudeMd(cwd);
155
158
  propagateTemplateRules(cwd, srcTemplates);
156
159
 
157
160
  // Copy template agents (only template-managed, preserve user agents)
@@ -988,6 +991,19 @@ function materializeSharedAppendPrompt(projectRoot) {
988
991
  fs.copyFileSync(templatePath, outputPath);
989
992
  }
990
993
 
994
+ // Materialize the default CLAUDE.md only when the project has none.
995
+ // CLAUDE.md is user-owned: customizations made by the user (or by other
996
+ // frameworks) must survive `claudeflow update`. We therefore never
997
+ // overwrite an existing file.
998
+ function materializeDefaultClaudeMd(projectRoot) {
999
+ const templatePath = path.join(projectRoot, DEFAULT_CLAUDE_MD_TEMPLATE);
1000
+ if (!fs.existsSync(templatePath)) return false;
1001
+ const outputPath = path.join(projectRoot, DEFAULT_CLAUDE_MD_OUTPUT);
1002
+ if (fs.existsSync(outputPath)) return false;
1003
+ fs.copyFileSync(templatePath, outputPath);
1004
+ return true;
1005
+ }
1006
+
991
1007
  function isTemplateManagedAgent(agentName) {
992
1008
  return agentName.startsWith('claudeflow-');
993
1009
  }
@@ -1020,6 +1036,7 @@ function copyDirSync(src, dst, skipNames) {
1020
1036
  module.exports = Object.assign(run, {
1021
1037
  assertRequiredTemplateRules,
1022
1038
  materializeSharedAppendPrompt,
1039
+ materializeDefaultClaudeMd,
1023
1040
  isTemplateManagedAgent,
1024
1041
  listTemplateManagedAgents,
1025
1042
  mergeClaudeSettings,
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.17",
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"