@axiomatic-labs/claudeflow 2.49.18 → 2.49.20

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 (2) hide show
  1. package/lib/install.js +263 -3
  2. package/package.json +1 -1
package/lib/install.js CHANGED
@@ -2,7 +2,7 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const os = require('os');
4
4
  const crypto = require('crypto');
5
- const { execSync } = require('child_process');
5
+ const { execFileSync, execSync } = require('child_process');
6
6
  const { requireAuth } = require('./auth.js');
7
7
  const { getLatestRelease, downloadReleaseAsset } = require('./download.js');
8
8
  const { writeLocalVersion, readLocalVersion } = require('./version.js');
@@ -27,12 +27,233 @@ const PER_PROJECT_CONFIGS = ['brand.json', 'risk-signals.json', 'test-context.js
27
27
  // Paths appended to the project's .gitignore (add-if-absent). These hold TEST credentials / Playwright
28
28
  // storage state — they must NEVER be committed. test-context.json is per-project runtime state; .auth/
29
29
  // holds storageState files the orchestrator seeds for auth-gated functional verification.
30
- const GITIGNORE_LINES = ['.claudeflow/test-context.json', '.claudeflow/.auth/', '.claudeflow/telemetry/', '.claudeflow/certification/'];
30
+ const GITIGNORE_LINES = ['.claudeflow/test-context.json', '.claudeflow/.auth/', '.claudeflow/telemetry/', '.claudeflow/certification/', '.claudeflow/tools/'];
31
31
  const SHARED_CONFIGS = ['playbook-map.json', 'agent-map.json']; // always refreshed (shared system)
32
32
  const CODEX_DOCS = ['claudeflow-codex-migration.md', 'codexflow-codex-native-workflow.md', 'codexflow-local-actions.md'];
33
33
 
34
34
  function rmrf(p) { fs.rmSync(p, { recursive: true, force: true }); }
35
35
 
36
+ function findExecutable(name) {
37
+ const envPath = process.env.PATH || '';
38
+ const exts = process.platform === 'win32'
39
+ ? (process.env.PATHEXT || '.EXE;.CMD;.BAT;.COM').split(';')
40
+ : [''];
41
+ for (const dir of envPath.split(path.delimiter)) {
42
+ if (!dir) continue;
43
+ for (const ext of exts) {
44
+ const p = path.join(dir, process.platform === 'win32' ? name + ext : name);
45
+ try {
46
+ fs.accessSync(p, fs.constants.X_OK);
47
+ return p;
48
+ } catch {}
49
+ }
50
+ }
51
+ return null;
52
+ }
53
+
54
+ function optionalSerenaCommand(options = {}) {
55
+ if (options.configureSerena !== true && options.configureTools !== true) return null;
56
+ if (options.noSerena === true || process.env.CLAUDEFLOW_NO_SERENA === '1') return null;
57
+ if (options.serenaCommand) return String(options.serenaCommand);
58
+ if (process.env.CLAUDEFLOW_SERENA_COMMAND) return process.env.CLAUDEFLOW_SERENA_COMMAND;
59
+ return findExecutable('serena');
60
+ }
61
+
62
+ function serenaMcpServer(command, context) {
63
+ return {
64
+ command,
65
+ args: ['start-mcp-server', '--project-from-cwd', `--context=${context}`, '--open-web-dashboard', 'false'],
66
+ };
67
+ }
68
+
69
+ function tomlString(value) {
70
+ return JSON.stringify(String(value));
71
+ }
72
+
73
+ function serenaCodexToml(command) {
74
+ return [
75
+ '[mcp_servers.serena]',
76
+ `command = ${tomlString(command)}`,
77
+ 'args = ["start-mcp-server", "--project-from-cwd", "--context=codex", "--open-web-dashboard", "false"]',
78
+ 'startup_timeout_sec = 30',
79
+ 'tool_timeout_sec = 120',
80
+ '',
81
+ ].join('\n');
82
+ }
83
+
84
+ function exeName(name) {
85
+ return process.platform === 'win32' ? `${name}.cmd` : name;
86
+ }
87
+
88
+ function managedToolsHome(options = {}) {
89
+ return options.toolsHome || path.join(options.homeDir || os.homedir(), '.claudeflow', 'tools');
90
+ }
91
+
92
+ function projectToolsBin(cwd) {
93
+ return path.join(cwd, '.claudeflow', 'tools', 'bin');
94
+ }
95
+
96
+ function runQuiet(command, args, options = {}) {
97
+ execFileSync(command, args, {
98
+ cwd: options.cwd || process.cwd(),
99
+ env: { ...process.env, ...(options.env || {}) },
100
+ stdio: 'ignore',
101
+ timeout: options.timeout || 120000,
102
+ });
103
+ }
104
+
105
+ function executableOk(command, args = ['--help']) {
106
+ if (!command) return false;
107
+ try {
108
+ runQuiet(command, args, { timeout: 20000 });
109
+ return true;
110
+ } catch {
111
+ return false;
112
+ }
113
+ }
114
+
115
+ function writeToolWrapper(cwd, name, target) {
116
+ const bin = projectToolsBin(cwd);
117
+ fs.mkdirSync(bin, { recursive: true });
118
+ if (process.platform === 'win32') {
119
+ const wrapper = path.join(bin, `${name}.cmd`);
120
+ fs.writeFileSync(wrapper, `@echo off\r\n"${target}" %*\r\n`);
121
+ return wrapper;
122
+ }
123
+ const wrapper = path.join(bin, name);
124
+ fs.writeFileSync(wrapper, `#!/usr/bin/env sh\nexec "${target}" "$@"\n`);
125
+ fs.chmodSync(wrapper, 0o755);
126
+ return wrapper;
127
+ }
128
+
129
+ function uvToolBinDir(uvCommand) {
130
+ try {
131
+ return String(execFileSync(uvCommand, ['tool', 'dir', '--bin'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 20000 })).trim();
132
+ } catch {
133
+ return null;
134
+ }
135
+ }
136
+
137
+ function pythonVersionOk(command) {
138
+ if (!command) return false;
139
+ try {
140
+ const out = String(execFileSync(command, ['-c', 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")'], {
141
+ encoding: 'utf8',
142
+ stdio: ['ignore', 'pipe', 'ignore'],
143
+ timeout: 20000,
144
+ })).trim();
145
+ const [maj, min] = out.split('.').map(n => parseInt(n, 10) || 0);
146
+ return maj > 3 || (maj === 3 && min >= 11);
147
+ } catch {
148
+ return false;
149
+ }
150
+ }
151
+
152
+ function findPython311(options = {}) {
153
+ const candidates = [
154
+ options.pythonCommand,
155
+ process.env.CLAUDEFLOW_PYTHON_COMMAND,
156
+ findExecutable('python3.13'),
157
+ findExecutable('python3.12'),
158
+ findExecutable('python3.11'),
159
+ findExecutable('python3'),
160
+ ].filter(Boolean);
161
+ return candidates.find(pythonVersionOk) || null;
162
+ }
163
+
164
+ function venvBinPath(venv, name) {
165
+ return process.platform === 'win32'
166
+ ? path.join(venv, 'Scripts', `${name}.exe`)
167
+ : path.join(venv, 'bin', name);
168
+ }
169
+
170
+ function installSerenaWithPip(options = {}) {
171
+ const python = findPython311(options);
172
+ if (!python) return null;
173
+ const venv = path.join(managedToolsHome(options), 'serena-venv');
174
+ try {
175
+ if (!fs.existsSync(venvBinPath(venv, 'python'))) {
176
+ runQuiet(python, ['-m', 'venv', venv], { timeout: 180000 });
177
+ }
178
+ const venvPython = venvBinPath(venv, 'python');
179
+ runQuiet(venvPython, ['-m', 'pip', 'install', '-U', 'serena-agent'], { timeout: 300000 });
180
+ const command = venvBinPath(venv, 'serena');
181
+ return executableOk(command, ['--help']) ? command : null;
182
+ } catch {
183
+ return null;
184
+ }
185
+ }
186
+
187
+ function ensureSerenaTool(cwd, options = {}) {
188
+ if (options.noSerena === true || process.env.CLAUDEFLOW_NO_SERENA === '1') {
189
+ return { status: 'skipped', reason: 'CLAUDEFLOW_NO_SERENA=1' };
190
+ }
191
+
192
+ let command = options.serenaCommand || process.env.CLAUDEFLOW_SERENA_COMMAND || findExecutable('serena');
193
+ if (!executableOk(command, ['--help'])) {
194
+ const uv = options.uvCommand || findExecutable('uv');
195
+ if (uv) {
196
+ try {
197
+ runQuiet(uv, ['tool', 'install', '-p', '3.13', 'serena-agent'], { timeout: 300000 });
198
+ } catch {}
199
+ const binDir = uvToolBinDir(uv);
200
+ const installed = binDir ? path.join(binDir, exeName('serena')) : null;
201
+ command = executableOk(installed, ['--help']) ? installed : findExecutable('serena');
202
+ }
203
+ if (!executableOk(command, ['--help'])) command = installSerenaWithPip(options);
204
+ }
205
+
206
+ if (!executableOk(command, ['--help'])) {
207
+ return { status: 'failed', reason: 'serena install failed; need uv or Python 3.11+ with pip' };
208
+ }
209
+ const wrapper = writeToolWrapper(cwd, 'serena', command);
210
+ return { status: 'ready', command: wrapper, target: command };
211
+ }
212
+
213
+ function ensureAstGrepTool(cwd, options = {}) {
214
+ if (options.noAstGrep === true || process.env.CLAUDEFLOW_NO_AST_GREP === '1') {
215
+ return { status: 'skipped', reason: 'CLAUDEFLOW_NO_AST_GREP=1' };
216
+ }
217
+
218
+ const home = managedToolsHome(options);
219
+ const prefix = path.join(home, 'ast-grep');
220
+ const managed = path.join(prefix, 'node_modules', '.bin', exeName('ast-grep'));
221
+ let command = options.astGrepCommand || process.env.CLAUDEFLOW_AST_GREP_COMMAND || null;
222
+ if (!executableOk(command, ['--version'])) {
223
+ command = executableOk(managed, ['--version'])
224
+ ? managed
225
+ : (findExecutable('ast-grep') || findExecutable('sg'));
226
+ }
227
+ if (!executableOk(command, ['--version'])) {
228
+ const npm = options.npmCommand || findExecutable('npm');
229
+ if (!npm) return { status: 'failed', reason: 'npm not found; ast-grep managed install requires npm' };
230
+ try {
231
+ fs.mkdirSync(prefix, { recursive: true });
232
+ runQuiet(npm, ['install', '--prefix', prefix, '@ast-grep/cli@latest'], { timeout: 300000 });
233
+ } catch {
234
+ return { status: 'failed', reason: 'npm install @ast-grep/cli failed' };
235
+ }
236
+ command = managed;
237
+ }
238
+ if (!executableOk(command, ['--version'])) {
239
+ return { status: 'failed', reason: 'ast-grep command did not validate after install' };
240
+ }
241
+ const wrapper = writeToolWrapper(cwd, 'ast-grep', command);
242
+ // `sg` is convenient on macOS, but Linux reserves `sg`; still provide the wrapper because it is project-local.
243
+ const sgWrapper = writeToolWrapper(cwd, 'sg', command);
244
+ return { status: 'ready', command: wrapper, sgCommand: sgWrapper, target: command };
245
+ }
246
+
247
+ function ensureManagedTools(cwd, options = {}) {
248
+ if (options.configureTools !== true || process.env.CLAUDEFLOW_NO_TOOLS === '1') {
249
+ return { serena: { status: 'skipped', reason: 'tool bootstrap disabled' }, astGrep: { status: 'skipped', reason: 'tool bootstrap disabled' } };
250
+ }
251
+ return {
252
+ serena: ensureSerenaTool(cwd, options),
253
+ astGrep: ensureAstGrepTool(cwd, options),
254
+ };
255
+ }
256
+
36
257
  function copyDir(src, dst) {
37
258
  fs.mkdirSync(dst, { recursive: true });
38
259
  for (const e of fs.readdirSync(src, { withFileTypes: true })) {
@@ -393,6 +614,9 @@ function installFromPayload(src, cwd, options = {}) {
393
614
  codexPlugin: 0,
394
615
  docs: 0,
395
616
  mcp: 0,
617
+ serenaMcp: 0,
618
+ serenaCodexConfig: false,
619
+ tooling: {},
396
620
  gitignore: 0,
397
621
  claudeMd: false,
398
622
  agentsMd: false,
@@ -428,6 +652,17 @@ function installFromPayload(src, cwd, options = {}) {
428
652
  }
429
653
  });
430
654
 
655
+ // 2b) Managed tool bootstrap — installs/validates semantic-code accelerators once per machine and writes
656
+ // small project-local wrappers under .claudeflow/tools/bin. Deps stay outside the repo; the wrappers give
657
+ // agents stable commands even when their shell PATH differs from the user's interactive shell.
658
+ step('managed-tools', () => {
659
+ out.tooling = ensureManagedTools(cwd, options);
660
+ for (const name of ['serena', 'astGrep']) {
661
+ const tool = out.tooling[name];
662
+ if (tool && tool.status === 'failed') out.failed.push(`tools:${name}:${tool.reason}`);
663
+ }
664
+ });
665
+
431
666
  // 3) Scripts (hooks + scans + servers) — always refreshed. Per-file isolation so ONE bad file (a socket, a
432
667
  // permission denial) can't abort the whole copy — let alone the later settings/CLAUDE.md steps.
433
668
  step('scripts', () => {
@@ -532,6 +767,14 @@ function installFromPayload(src, cwd, options = {}) {
532
767
  }
533
768
  });
534
769
 
770
+ // 7b) Optional semantic-code MCP. Serena is useful for fresh implementers, but it must not become a hidden
771
+ // dependency. The real install auto-registers it only when a `serena` executable already exists (or the caller
772
+ // passes an explicit command); tests/direct installFromPayload stay deterministic unless configureSerena=true.
773
+ step('serena-mcp', () => {
774
+ const serena = (out.tooling.serena && out.tooling.serena.command) || optionalSerenaCommand(options);
775
+ if (serena) out.serenaMcp = mergeMcp(cwd, { serena: serenaMcpServer(serena, 'claude-code') });
776
+ });
777
+
535
778
  // 8) AGENTS.md — upsert the Codex guidance block between markers, preserving project guidance.
536
779
  step('agents-md', () => {
537
780
  out.agentsMd = syncAgentsMd(cwd, path.join(src, 'assets', 'AGENTS.snippet.md'));
@@ -546,6 +789,10 @@ function installFromPayload(src, cwd, options = {}) {
546
789
  const srcConfig = path.join(src, '.codex', 'config.toml');
547
790
  if (fs.existsSync(srcConfig)) out.codexConfig = mergeCodexConfig(cwd, fs.readFileSync(srcConfig, 'utf8'));
548
791
  });
792
+ step('serena-codex-config', () => {
793
+ const serena = (out.tooling.serena && out.tooling.serena.command) || optionalSerenaCommand(options);
794
+ if (serena) out.serenaCodexConfig = mergeCodexConfig(cwd, serenaCodexToml(serena));
795
+ });
549
796
 
550
797
  // 10) Codex marketplace + local plugin — managed codexflow entry only; user plugins are preserved.
551
798
  step('codex-marketplace', () => {
@@ -633,7 +880,11 @@ async function run() {
633
880
  execSync(`unzip -qq -o "${tmpZip}" -d "${tmp}"`, { stdio: 'ignore' });
634
881
 
635
882
  ui.step('Installing framework...');
636
- const summary = installFromPayload(tmp, cwd, { syncGlobalClaude: process.env.CLAUDEFLOW_NO_GLOBAL_SYNC !== '1' });
883
+ const summary = installFromPayload(tmp, cwd, {
884
+ syncGlobalClaude: process.env.CLAUDEFLOW_NO_GLOBAL_SYNC !== '1',
885
+ configureTools: true,
886
+ configureSerena: true,
887
+ });
637
888
  writeLocalVersion(`v${version}`);
638
889
 
639
890
  try { rmrf(tmp); fs.rmSync(tmpZip, { force: true }); } catch {}
@@ -649,6 +900,9 @@ async function run() {
649
900
  + (summary.codexAgents ? ` · ${summary.codexAgents} Codex agents` : '')
650
901
  + (summary.codexHooks ? ' · Codex hooks merged' : '')
651
902
  + (summary.codexConfig ? ' · Codex config synced' : '')
903
+ + (summary.tooling && summary.tooling.serena && summary.tooling.serena.status === 'ready' ? ' · Serena ready' : '')
904
+ + (summary.tooling && summary.tooling.astGrep && summary.tooling.astGrep.status === 'ready' ? ' · ast-grep ready' : '')
905
+ + (summary.serenaMcp || summary.serenaCodexConfig ? ' · Serena MCP configured' : '')
652
906
  + (summary.codexMarketplace ? ' · Codex plugin registered' : '')
653
907
  + (summary.mcp ? ` · ${summary.mcp} MCP server${summary.mcp > 1 ? 's' : ''} added to .mcp.json` : '')
654
908
  + (summary.claudeMd ? ' · CLAUDE.md guidance synced' : '')
@@ -670,3 +924,9 @@ module.exports.mergeGitignore = mergeGitignore;
670
924
  module.exports.mergeCodexHooks = mergeCodexHooks;
671
925
  module.exports.mergeCodexConfig = mergeCodexConfig;
672
926
  module.exports.mergeCodexMarketplace = mergeCodexMarketplace;
927
+ module.exports.optionalSerenaCommand = optionalSerenaCommand;
928
+ module.exports.serenaMcpServer = serenaMcpServer;
929
+ module.exports.serenaCodexToml = serenaCodexToml;
930
+ module.exports.ensureManagedTools = ensureManagedTools;
931
+ module.exports.ensureSerenaTool = ensureSerenaTool;
932
+ module.exports.ensureAstGrepTool = ensureAstGrepTool;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axiomatic-labs/claudeflow",
3
- "version": "2.49.18",
3
+ "version": "2.49.20",
4
4
  "description": "Claudeflow — AI-powered development toolkit for Claude Code and Codex. Skills, agents, hooks, and quality gates that ship production apps.",
5
5
  "bin": {
6
6
  "claudeflow": "./bin/cli.js"