@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 +7 -1
- package/lib/doctor.js +207 -0
- package/lib/install.js +17 -0
- package/package.json +1 -1
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.
|
|
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"
|