create-byan-agent 2.2.1 → 2.3.0

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.
@@ -426,6 +426,12 @@ async function install() {
426
426
  name: 'quality',
427
427
  message: '10. Niveau qualité?',
428
428
  choices: ['mvp (speed)', 'balanced', 'production', 'critical']
429
+ },
430
+ {
431
+ type: 'confirm',
432
+ name: 'costOptimizer',
433
+ message: '11. Optimiser coûts LLM automatiquement? (Économise ~54%)',
434
+ default: true
429
435
  }
430
436
  ]);
431
437
 
@@ -858,6 +864,24 @@ async function install() {
858
864
  copySpinner.warn(`⚠ Agent source not found: ${agentsSource}`);
859
865
  }
860
866
 
867
+ // Copy cost optimizer worker if enabled
868
+ if (interviewAnswers && interviewAnswers.costOptimizer) {
869
+ const workersDir = path.join(byanDir, 'workers');
870
+ await fs.ensureDir(workersDir);
871
+
872
+ const workerSource = path.join(templateDir, 'workers', 'cost-optimizer.js');
873
+ const workerDest = path.join(workersDir, 'cost-optimizer.js');
874
+
875
+ if (await fs.pathExists(workerSource)) {
876
+ await fs.copy(workerSource, workerDest, { overwrite: true });
877
+ copySpinner.text = 'Copied cost optimizer worker...';
878
+ console.log(chalk.green(` ✓ Cost Optimizer: ${workerSource} → ${workerDest}`));
879
+ console.log(chalk.cyan(' 💰 Automatic LLM cost optimization enabled (~54% savings)'));
880
+ } else {
881
+ copySpinner.warn(`⚠ Cost optimizer source not found: ${workerSource}`);
882
+ }
883
+ }
884
+
861
885
  // Copy workflow files
862
886
  const workflowsSource = path.join(templateDir, '_byan', 'bmb', 'workflows', 'byan');
863
887
  const workflowsDest = path.join(bmbDir, 'workflows', 'byan');
@@ -2,6 +2,7 @@
2
2
  * Claude Code Platform Support
3
3
  *
4
4
  * Detects and installs MCP server config for Claude Code.
5
+ * Uses agent Claude for native integration via MCP protocol.
5
6
  *
6
7
  * @module platforms/claude-code
7
8
  */
@@ -9,8 +10,10 @@
9
10
  const path = require('path');
10
11
  const os = require('os');
11
12
  const fileUtils = require('../utils/file-utils');
13
+ const logger = require('../utils/logger');
12
14
 
13
15
  const PLATFORM_NAME = 'Claude Code';
16
+ const MCP_SERVER_FILENAME = 'byan-mcp-server.js';
14
17
 
15
18
  /**
16
19
  * Get config path for current platform
@@ -48,24 +51,131 @@ async function detect() {
48
51
  /**
49
52
  * Install MCP server config for Claude Code
50
53
  *
54
+ * Delegates to agent Claude for native integration.
55
+ * Falls back to basic JSON update if agent unavailable.
56
+ *
51
57
  * @param {string} projectRoot - Project root directory
52
58
  * @param {string[]} agents - Agent names to install
53
59
  * @param {Object} config - Installation config
54
- * @returns {Promise<{success: boolean, installed: number}>}
60
+ * @param {Object} [options] - Installation options
61
+ * @param {string} [options.specialist] - Specialist agent to use (e.g., 'claude')
62
+ * @param {boolean} [options.useAgent] - Use specialist agent if available (default: true)
63
+ * @returns {Promise<{success: boolean, installed: number, method: string}>}
55
64
  */
56
- async function install(projectRoot, agents, config) {
65
+ async function install(projectRoot, agents, config, options = {}) {
57
66
  const configPath = getConfigPath();
58
67
 
59
68
  if (!configPath) {
60
69
  throw new Error(`Unsupported platform: ${os.platform()}`);
61
70
  }
62
71
 
63
- // TODO: Update claude_desktop_config.json to add MCP server
64
- // MCP server will expose BYAN agents
72
+ const useAgent = options.useAgent !== false && options.specialist === 'claude';
73
+
74
+ if (useAgent) {
75
+ logger.info('Using agent Claude for native MCP integration...');
76
+ return await installViaCopilotAgent(projectRoot, agents, config);
77
+ } else {
78
+ logger.info('Using direct MCP configuration...');
79
+ return await installDirectMCP(projectRoot, agents, config);
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Install via agent Claude (native integration)
85
+ *
86
+ * @param {string} projectRoot
87
+ * @param {string[]} agents
88
+ * @param {Object} config
89
+ * @returns {Promise<{success: boolean, installed: number, method: string}>}
90
+ */
91
+ async function installViaCopilotAgent(projectRoot, agents, config) {
92
+ const agentLauncher = require('../yanstaller/agent-launcher');
93
+
94
+ // Check if native launch is available
95
+ if (agentLauncher.supportsNativeLaunch('claude')) {
96
+ logger.info('\n🚀 Launching agent Claude for MCP integration...');
97
+
98
+ // Launch agent Claude with create-mcp-server action
99
+ const result = await agentLauncher.launch({
100
+ agent: 'claude',
101
+ platform: 'claude',
102
+ prompt: 'create-mcp-server'
103
+ });
104
+
105
+ if (result.success) {
106
+ return {
107
+ success: true,
108
+ installed: agents.length,
109
+ method: 'agent-claude-native'
110
+ };
111
+ } else {
112
+ logger.warn(`Native launch failed: ${result.error}`);
113
+ logger.info('Falling back to manual instructions...');
114
+ }
115
+ }
116
+
117
+ // Fallback: Manual instructions
118
+ logger.info('\n📝 To complete Claude Code integration:');
119
+ logger.info(' 1. Run: @bmad-agent-claude');
120
+ logger.info(' 2. Select option 1: Create MCP server for BYAN agents');
121
+ logger.info(' 3. Follow the guided setup\n');
122
+
123
+ return {
124
+ success: true,
125
+ installed: agents.length,
126
+ method: 'agent-claude-guided'
127
+ };
128
+ }
129
+
130
+ /**
131
+ * Install via direct MCP config update
132
+ *
133
+ * @param {string} projectRoot
134
+ * @param {string[]} agents
135
+ * @param {Object} config
136
+ * @returns {Promise<{success: boolean, installed: number, method: string}>}
137
+ */
138
+ async function installDirectMCP(projectRoot, agents, config) {
139
+ const configPath = getConfigPath();
140
+ const mcpServerPath = path.join(projectRoot, MCP_SERVER_FILENAME);
141
+
142
+ // Check if MCP server exists
143
+ if (!await fileUtils.exists(mcpServerPath)) {
144
+ logger.warn(`MCP server not found at: ${mcpServerPath}`);
145
+ logger.warn('Run @bmad-agent-claude to generate MCP server first.');
146
+
147
+ return {
148
+ success: false,
149
+ installed: 0,
150
+ method: 'direct-mcp-failed'
151
+ };
152
+ }
153
+
154
+ // Backup existing config
155
+ const backupPath = `${configPath}.backup`;
156
+ await fileUtils.copy(configPath, backupPath);
157
+ logger.info(`Backed up config to: ${backupPath}`);
158
+
159
+ // Read and update config
160
+ const existingConfig = await fileUtils.readJson(configPath);
161
+ existingConfig.mcpServers = existingConfig.mcpServers || {};
162
+
163
+ existingConfig.mcpServers.byan = {
164
+ command: 'node',
165
+ args: [mcpServerPath],
166
+ env: {
167
+ PROJECT_ROOT: projectRoot
168
+ }
169
+ };
170
+
171
+ // Write updated config
172
+ await fileUtils.writeJson(configPath, existingConfig, { spaces: 2 });
173
+ logger.info(`Updated MCP config: ${configPath}`);
65
174
 
66
175
  return {
67
176
  success: true,
68
- installed: agents.length
177
+ installed: agents.length,
178
+ method: 'direct-mcp'
69
179
  };
70
180
  }
71
181
 
@@ -0,0 +1,348 @@
1
+ /**
2
+ * AGENT LAUNCHER Module
3
+ *
4
+ * Launches specialist agents using native platform commands.
5
+ * Each platform has its own invocation syntax.
6
+ *
7
+ * @module yanstaller/agent-launcher
8
+ */
9
+
10
+ const { execSync, spawn } = require('child_process');
11
+ const logger = require('../utils/logger');
12
+
13
+ /**
14
+ * @typedef {Object} LaunchOptions
15
+ * @property {string} agent - Agent name (e.g., 'claude', 'marc')
16
+ * @property {string} platform - Platform ID (e.g., 'copilot-cli', 'claude')
17
+ * @property {string} [prompt] - Initial prompt/action
18
+ * @property {string} [model] - Model to use
19
+ * @property {Object} [config] - Additional config
20
+ */
21
+
22
+ /**
23
+ * @typedef {Object} LaunchResult
24
+ * @property {boolean} success
25
+ * @property {string} method - How agent was launched
26
+ * @property {string} [output] - Command output
27
+ * @property {string} [error] - Error message if failed
28
+ */
29
+
30
+ /**
31
+ * Platform-specific launch configurations
32
+ */
33
+ const LAUNCH_CONFIGS = {
34
+ 'copilot-cli': {
35
+ command: 'gh',
36
+ args: (agent, options) => {
37
+ const args = ['copilot'];
38
+
39
+ // Use @agent syntax if available
40
+ if (agent) {
41
+ args.push(`@bmad-agent-${agent}`);
42
+ }
43
+
44
+ if (options.prompt) {
45
+ args.push(options.prompt);
46
+ }
47
+
48
+ return args;
49
+ },
50
+ checkAvailable: () => {
51
+ try {
52
+ execSync('which gh', { stdio: 'ignore' });
53
+ return true;
54
+ } catch {
55
+ return false;
56
+ }
57
+ }
58
+ },
59
+
60
+ 'claude': {
61
+ command: 'claude',
62
+ args: (agent, options = {}) => {
63
+ const args = [];
64
+
65
+ // Agent specification
66
+ if (agent) {
67
+ args.push('--agent', agent);
68
+ }
69
+
70
+ // Model selection
71
+ if (options.model) {
72
+ args.push('--model', options.model);
73
+ }
74
+
75
+ // System prompt for context (before positional prompt)
76
+ if (options.systemPrompt) {
77
+ args.push('--system-prompt', options.systemPrompt);
78
+ }
79
+
80
+ // MCP config if needed
81
+ if (options.mcpConfig) {
82
+ args.push('--mcp-config', options.mcpConfig);
83
+ }
84
+
85
+ // Prompt as POSITIONAL argument (not --prompt)
86
+ // Must come AFTER all flags
87
+ if (options.prompt) {
88
+ args.push(options.prompt);
89
+ }
90
+
91
+ return args;
92
+ },
93
+ checkAvailable: () => {
94
+ try {
95
+ execSync('which claude', { stdio: 'ignore' });
96
+ return true;
97
+ } catch {
98
+ return false;
99
+ }
100
+ }
101
+ },
102
+
103
+ 'codex': {
104
+ command: 'codex',
105
+ args: (agent, options = {}) => {
106
+ const args = [];
107
+
108
+ // Codex uses "skills" not "agents"
109
+ // Format: codex skill <skill-name> [prompt]
110
+
111
+ if (agent) {
112
+ args.push('skill', `bmad-${agent}`);
113
+ }
114
+
115
+ // Prompt as positional argument
116
+ if (options.prompt) {
117
+ args.push(options.prompt);
118
+ }
119
+
120
+ // Model selection (if Codex supports it)
121
+ if (options.model) {
122
+ args.push('--model', options.model);
123
+ }
124
+
125
+ return args;
126
+ },
127
+ checkAvailable: () => {
128
+ try {
129
+ execSync('which codex', { stdio: 'ignore' });
130
+ return true;
131
+ } catch {
132
+ return false;
133
+ }
134
+ }
135
+ }
136
+ };
137
+
138
+ /**
139
+ * Launch agent using native platform command
140
+ *
141
+ * @param {LaunchOptions} options - Launch options
142
+ * @returns {Promise<LaunchResult>}
143
+ */
144
+ async function launch(options) {
145
+ const { agent, platform, prompt, model, config } = options;
146
+
147
+ const platformConfig = LAUNCH_CONFIGS[platform];
148
+
149
+ if (!platformConfig) {
150
+ return {
151
+ success: false,
152
+ method: 'unsupported',
153
+ error: `Platform ${platform} not supported for native launch`
154
+ };
155
+ }
156
+
157
+ // Check if platform command is available
158
+ if (!platformConfig.checkAvailable()) {
159
+ return {
160
+ success: false,
161
+ method: 'command-not-found',
162
+ error: `Command '${platformConfig.command}' not found in PATH`
163
+ };
164
+ }
165
+
166
+ // Build command arguments
167
+ const args = platformConfig.args(agent, {
168
+ prompt,
169
+ model,
170
+ systemPrompt: config?.systemPrompt,
171
+ mcpConfig: config?.mcpConfig
172
+ });
173
+
174
+ const fullCommand = `${platformConfig.command} ${args.join(' ')}`;
175
+
176
+ logger.info(`Launching agent via: ${fullCommand}`);
177
+
178
+ try {
179
+ // Launch in interactive mode (inherit stdio)
180
+ const result = spawn(platformConfig.command, args, {
181
+ stdio: 'inherit',
182
+ shell: true
183
+ });
184
+
185
+ return new Promise((resolve) => {
186
+ result.on('close', (code) => {
187
+ if (code === 0) {
188
+ resolve({
189
+ success: true,
190
+ method: 'native-interactive',
191
+ output: `Agent launched successfully via ${platform}`
192
+ });
193
+ } else {
194
+ resolve({
195
+ success: false,
196
+ method: 'native-interactive',
197
+ error: `Agent exited with code ${code}`
198
+ });
199
+ }
200
+ });
201
+
202
+ result.on('error', (error) => {
203
+ resolve({
204
+ success: false,
205
+ method: 'native-interactive',
206
+ error: error.message
207
+ });
208
+ });
209
+ });
210
+ } catch (error) {
211
+ return {
212
+ success: false,
213
+ method: 'native-interactive',
214
+ error: error.message
215
+ };
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Launch agent with specific prompt (non-interactive)
221
+ *
222
+ * Useful for automation or scripted workflows.
223
+ *
224
+ * @param {LaunchOptions} options - Launch options
225
+ * @returns {Promise<LaunchResult>}
226
+ */
227
+ async function launchWithPrompt(options) {
228
+ const { agent, platform, prompt, model, config } = options;
229
+
230
+ if (!prompt) {
231
+ throw new Error('Prompt required for non-interactive launch');
232
+ }
233
+
234
+ const platformConfig = LAUNCH_CONFIGS[platform];
235
+
236
+ if (!platformConfig) {
237
+ return {
238
+ success: false,
239
+ method: 'unsupported',
240
+ error: `Platform ${platform} not supported`
241
+ };
242
+ }
243
+
244
+ if (!platformConfig.checkAvailable()) {
245
+ return {
246
+ success: false,
247
+ method: 'command-not-found',
248
+ error: `Command '${platformConfig.command}' not found`
249
+ };
250
+ }
251
+
252
+ const args = platformConfig.args(agent, {
253
+ prompt,
254
+ model,
255
+ systemPrompt: config?.systemPrompt
256
+ });
257
+
258
+ // Add --print flag for Claude to get output
259
+ if (platform === 'claude') {
260
+ args.unshift('--print');
261
+ }
262
+
263
+ const fullCommand = `${platformConfig.command} ${args.join(' ')}`;
264
+
265
+ logger.info(`Executing: ${fullCommand}`);
266
+
267
+ try {
268
+ const output = execSync(fullCommand, {
269
+ encoding: 'utf8',
270
+ maxBuffer: 10 * 1024 * 1024 // 10MB
271
+ });
272
+
273
+ return {
274
+ success: true,
275
+ method: 'native-print',
276
+ output: output.trim()
277
+ };
278
+ } catch (error) {
279
+ return {
280
+ success: false,
281
+ method: 'native-print',
282
+ error: error.message
283
+ };
284
+ }
285
+ }
286
+
287
+ /**
288
+ * Generate launch instructions for manual invocation
289
+ *
290
+ * Used as fallback when native launch isn't possible.
291
+ *
292
+ * @param {LaunchOptions} options - Launch options
293
+ * @returns {string} - Human-readable instructions
294
+ */
295
+ function getLaunchInstructions(options) {
296
+ const { agent, platform, prompt, model } = options;
297
+
298
+ const platformConfig = LAUNCH_CONFIGS[platform];
299
+
300
+ if (!platformConfig) {
301
+ return `Platform ${platform} not yet supported for automated launch.\nPlease activate the agent manually.`;
302
+ }
303
+
304
+ const args = platformConfig.args(agent, { prompt, model });
305
+ const command = `${platformConfig.command} ${args.join(' ')}`;
306
+
307
+ return `
308
+ To activate the agent, run:
309
+
310
+ ${command}
311
+
312
+ Or in interactive mode:
313
+ ${platformConfig.command}
314
+ Then: @bmad-agent-${agent}
315
+ `;
316
+ }
317
+
318
+ /**
319
+ * Check if platform supports native agent launch
320
+ *
321
+ * @param {string} platform - Platform ID
322
+ * @returns {boolean}
323
+ */
324
+ function supportsNativeLaunch(platform) {
325
+ const config = LAUNCH_CONFIGS[platform];
326
+ if (!config) return false;
327
+ return config.checkAvailable();
328
+ }
329
+
330
+ /**
331
+ * Get available platforms for native launch
332
+ *
333
+ * @returns {string[]} - List of platform IDs
334
+ */
335
+ function getAvailablePlatforms() {
336
+ return Object.keys(LAUNCH_CONFIGS).filter(platform => {
337
+ const config = LAUNCH_CONFIGS[platform];
338
+ return config.checkAvailable();
339
+ });
340
+ }
341
+
342
+ module.exports = {
343
+ launch,
344
+ launchWithPrompt,
345
+ getLaunchInstructions,
346
+ supportsNativeLaunch,
347
+ getAvailablePlatforms
348
+ };
@@ -14,12 +14,14 @@ const troubleshooter = require('./troubleshooter');
14
14
  const interviewer = require('./interviewer');
15
15
  const backuper = require('./backuper');
16
16
  const wizard = require('./wizard');
17
+ const platformSelector = require('./platform-selector');
17
18
  const logger = require('../utils/logger');
18
19
 
19
20
  /**
20
21
  * @typedef {Object} YanInstallerOptions
21
22
  * @property {boolean} [yes] - Skip confirmations (--yes flag)
22
23
  * @property {string} [mode] - Installation mode: 'full' | 'minimal' | 'custom'
24
+ * @property {string[]} [platforms] - Target platforms (override detection)
23
25
  * @property {boolean} [verbose] - Verbose output
24
26
  * @property {boolean} [quiet] - Minimal output
25
27
  */
@@ -34,16 +36,57 @@ async function install(options = {}) {
34
36
  let backupPath = null;
35
37
 
36
38
  try {
37
- // TODO: Implement Phase 1-8 orchestration
38
- // 1. Detect environment
39
- // 2. Validate Node version (FAIL FAST)
40
- // 3. Recommend configuration
41
- // 4. Run interview (unless --yes)
42
- // 5. Backup existing installation
39
+ // Phase 1: Detect environment
40
+ logger.info('🔍 Detecting environment...');
41
+ const detection = await detector.detect();
42
+
43
+ // Phase 2: Validate Node version (FAIL FAST)
44
+ if (!detector.isNodeVersionValid(detection.nodeVersion, '18.0.0')) {
45
+ throw new Error(`Node.js >= 18.0.0 required. Found: ${detection.nodeVersion}`);
46
+ }
47
+
48
+ // Phase 3: Platform Selection
49
+ let platformSelection;
50
+ if (options.platforms) {
51
+ // CLI override
52
+ platformSelection = {
53
+ platforms: options.platforms,
54
+ mode: 'manual'
55
+ };
56
+ } else if (options.yes) {
57
+ // Auto mode
58
+ platformSelection = {
59
+ platforms: detection.platforms.filter(p => p.detected).map(p => p.name),
60
+ mode: 'auto'
61
+ };
62
+ } else {
63
+ // Interactive selection
64
+ platformSelection = await platformSelector.select(detection);
65
+ }
66
+
67
+ logger.info(`\n✓ Selected ${platformSelection.platforms.length} platform(s)`);
68
+ logger.info(` Mode: ${platformSelection.mode}`);
69
+ if (platformSelection.specialist) {
70
+ logger.info(` Specialist: @bmad-agent-${platformSelection.specialist}`);
71
+ }
72
+
73
+ // Phase 4: Recommend configuration
74
+ // TODO: Implement
75
+
76
+ // Phase 5: Run interview (unless --yes)
77
+ // TODO: Implement
78
+
79
+ // Phase 6: Backup existing installation
43
80
  // backupPath = await backuper.backup('_bmad');
44
- // 6. Install agents
45
- // 7. Validate installation
46
- // 8. Show post-install wizard
81
+
82
+ // Phase 7: Install agents
83
+ // TODO: Implement
84
+
85
+ // Phase 8: Validate installation
86
+ // TODO: Implement
87
+
88
+ // Phase 9: Show post-install wizard
89
+ // TODO: Implement
47
90
  } catch (error) {
48
91
  // ROLLBACK STRATEGY: Leave partial state + clear message
49
92
  // Rationale (Mantra #37 Ockham's Razor):
@@ -89,5 +132,8 @@ async function update(version) {
89
132
  module.exports = {
90
133
  install,
91
134
  uninstall,
92
- update
135
+ update,
136
+ // Expose for testing
137
+ detector,
138
+ platformSelector
93
139
  };