@hyperdrive.bot/bmad-workflow 1.0.18 → 1.0.19

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 (98) hide show
  1. package/dist/commands/config/show.js +8 -2
  2. package/dist/commands/decompose.js +26 -5
  3. package/dist/commands/epics/create.d.ts +1 -0
  4. package/dist/commands/mcp/add.d.ts +16 -0
  5. package/dist/commands/mcp/add.js +77 -0
  6. package/dist/commands/mcp/credential/get.d.ts +14 -0
  7. package/dist/commands/mcp/credential/get.js +35 -0
  8. package/dist/commands/mcp/credential/list.d.ts +17 -0
  9. package/dist/commands/mcp/credential/list.js +67 -0
  10. package/dist/commands/mcp/credential/remove.d.ts +18 -0
  11. package/dist/commands/mcp/credential/remove.js +84 -0
  12. package/dist/commands/mcp/credential/set.d.ts +16 -0
  13. package/dist/commands/mcp/credential/set.js +41 -0
  14. package/dist/commands/mcp/credential/validate.d.ts +12 -0
  15. package/dist/commands/mcp/credential/validate.js +150 -0
  16. package/dist/commands/mcp/list.d.ts +17 -0
  17. package/dist/commands/mcp/list.js +80 -0
  18. package/dist/commands/mcp/logs.d.ts +15 -0
  19. package/dist/commands/mcp/logs.js +64 -0
  20. package/dist/commands/mcp/preset.d.ts +15 -0
  21. package/dist/commands/mcp/preset.js +84 -0
  22. package/dist/commands/mcp/remove.d.ts +14 -0
  23. package/dist/commands/mcp/remove.js +36 -0
  24. package/dist/commands/mcp/start.d.ts +12 -0
  25. package/dist/commands/mcp/start.js +80 -0
  26. package/dist/commands/mcp/status.d.ts +30 -0
  27. package/dist/commands/mcp/status.js +180 -0
  28. package/dist/commands/mcp/stop.d.ts +12 -0
  29. package/dist/commands/mcp/stop.js +47 -0
  30. package/dist/commands/stories/create.d.ts +1 -0
  31. package/dist/commands/stories/develop.d.ts +1 -0
  32. package/dist/commands/stories/qa.js +5 -2
  33. package/dist/commands/stories/review.d.ts +124 -0
  34. package/dist/commands/stories/review.js +516 -0
  35. package/dist/commands/workflow.d.ts +8 -0
  36. package/dist/commands/workflow.js +110 -2
  37. package/dist/mcp/types.d.ts +99 -0
  38. package/dist/mcp/types.js +7 -0
  39. package/dist/mcp/utils/docker-utils.d.ts +56 -0
  40. package/dist/mcp/utils/docker-utils.js +108 -0
  41. package/dist/mcp/utils/template-loader.d.ts +21 -0
  42. package/dist/mcp/utils/template-loader.js +60 -0
  43. package/dist/models/agent-options.d.ts +10 -1
  44. package/dist/models/workflow-config.d.ts +77 -0
  45. package/dist/models/workflow-result.d.ts +7 -0
  46. package/dist/services/agents/claude-agent-runner.js +19 -3
  47. package/dist/services/file-system/path-resolver.d.ts +10 -0
  48. package/dist/services/file-system/path-resolver.js +12 -0
  49. package/dist/services/mcp/mcp-config-manager.d.ts +54 -0
  50. package/dist/services/mcp/mcp-config-manager.js +146 -0
  51. package/dist/services/mcp/mcp-context-injector.d.ts +92 -0
  52. package/dist/services/mcp/mcp-context-injector.js +168 -0
  53. package/dist/services/mcp/mcp-credential-manager.d.ts +48 -0
  54. package/dist/services/mcp/mcp-credential-manager.js +124 -0
  55. package/dist/services/mcp/mcp-health-checker.d.ts +56 -0
  56. package/dist/services/mcp/mcp-health-checker.js +162 -0
  57. package/dist/services/mcp/types/health-types.d.ts +31 -0
  58. package/dist/services/mcp/types/health-types.js +7 -0
  59. package/dist/services/orchestration/dependency-graph-executor.js +1 -1
  60. package/dist/services/orchestration/task-decomposition-service.d.ts +2 -1
  61. package/dist/services/orchestration/task-decomposition-service.js +90 -36
  62. package/dist/services/orchestration/workflow-orchestrator.d.ts +54 -2
  63. package/dist/services/orchestration/workflow-orchestrator.js +303 -17
  64. package/dist/services/review/ai-review-scanner.d.ts +66 -0
  65. package/dist/services/review/ai-review-scanner.js +142 -0
  66. package/dist/services/review/coderabbit-scanner.d.ts +25 -0
  67. package/dist/services/review/coderabbit-scanner.js +31 -0
  68. package/dist/services/review/index.d.ts +20 -0
  69. package/dist/services/review/index.js +15 -0
  70. package/dist/services/review/lint-scanner.d.ts +46 -0
  71. package/dist/services/review/lint-scanner.js +172 -0
  72. package/dist/services/review/review-config.d.ts +62 -0
  73. package/dist/services/review/review-config.js +91 -0
  74. package/dist/services/review/review-phase-executor.d.ts +69 -0
  75. package/dist/services/review/review-phase-executor.js +152 -0
  76. package/dist/services/review/review-queue.d.ts +98 -0
  77. package/dist/services/review/review-queue.js +174 -0
  78. package/dist/services/review/review-reporter.d.ts +94 -0
  79. package/dist/services/review/review-reporter.js +386 -0
  80. package/dist/services/review/scanner-factory.d.ts +42 -0
  81. package/dist/services/review/scanner-factory.js +60 -0
  82. package/dist/services/review/self-heal-loop.d.ts +58 -0
  83. package/dist/services/review/self-heal-loop.js +132 -0
  84. package/dist/services/review/severity-classifier.d.ts +17 -0
  85. package/dist/services/review/severity-classifier.js +314 -0
  86. package/dist/services/review/tech-debt-tracker.d.ts +52 -0
  87. package/dist/services/review/tech-debt-tracker.js +245 -0
  88. package/dist/services/review/types.d.ts +93 -0
  89. package/dist/services/review/types.js +23 -0
  90. package/dist/services/validation/config-validator.d.ts +84 -0
  91. package/dist/services/validation/config-validator.js +78 -0
  92. package/dist/utils/credential-utils.d.ts +14 -0
  93. package/dist/utils/credential-utils.js +19 -0
  94. package/dist/utils/duration.d.ts +41 -0
  95. package/dist/utils/duration.js +89 -0
  96. package/dist/utils/shared-flags.d.ts +1 -0
  97. package/dist/utils/shared-flags.js +11 -2
  98. package/package.json +4 -2
@@ -7,6 +7,7 @@
7
7
  import { Command, Flags } from '@oclif/core';
8
8
  import { dump, load } from 'js-yaml';
9
9
  import { FileManager } from '../../services/file-system/file-manager.js';
10
+ import { buildReviewConfig } from '../../services/review/review-config.js';
10
11
  import * as colors from '../../utils/colors.js';
11
12
  import { ConfigurationError, FileSystemError } from '../../utils/errors.js';
12
13
  import { createLogger } from '../../utils/logger.js';
@@ -76,16 +77,21 @@ export default class Show extends Command {
76
77
  this.error(colors.error(errorMsg), { exit: 1 });
77
78
  return;
78
79
  }
80
+ // Ensure review section is always present with defaults for display
81
+ const configObj = (config || {});
82
+ if (!configObj.review) {
83
+ configObj.review = buildReviewConfig();
84
+ }
79
85
  // Display configuration in requested format
80
86
  if (flags.json) {
81
87
  // JSON output - machine-readable
82
- this.log(JSON.stringify(config, null, 2));
88
+ this.log(JSON.stringify(configObj, null, 2));
83
89
  }
84
90
  else {
85
91
  // YAML output - human-readable with formatting
86
92
  this.log(colors.bold('Configuration (.bmad-core/core-config.yaml):'));
87
93
  this.log('');
88
- this.log(dump(config, { indent: 2, lineWidth: 120 }));
94
+ this.log(dump(configObj, { indent: 2, lineWidth: 120 }));
89
95
  }
90
96
  logger.info('Configuration displayed successfully');
91
97
  }
@@ -14,6 +14,7 @@ import { DependencyGraphExecutor } from '../services/orchestration/dependency-gr
14
14
  import { TaskDecompositionService } from '../services/orchestration/task-decomposition-service.js';
15
15
  import { DecomposeSessionScaffolder } from '../services/scaffolding/decompose-session-scaffolder.js';
16
16
  import * as colors from '../utils/colors.js';
17
+ import { parseDuration } from '../utils/duration.js';
17
18
  import { formatBox, formatTable } from '../utils/formatters.js';
18
19
  import { createLogger } from '../utils/logger.js';
19
20
  /**
@@ -100,9 +101,11 @@ export default class Decompose extends Command {
100
101
  'story-prefix': Flags.string({
101
102
  description: 'Project prefix for story IDs (e.g., "MIGRATE", "REFACTOR") - used with --story-format',
102
103
  }),
103
- 'task-timeout': Flags.integer({
104
+ 'task-timeout': Flags.custom({
105
+ parse: async (input) => parseDuration(input),
106
+ })({
104
107
  default: 1_800_000, // 30 minutes
105
- description: 'Timeout per task in milliseconds',
108
+ description: 'Timeout per task accepts durations like 30s, 5m, 1h, 90m, or raw milliseconds (default: 30m)',
106
109
  }),
107
110
  verbose: Flags.boolean({
108
111
  char: 'v',
@@ -119,6 +122,7 @@ export default class Decompose extends Command {
119
122
  */
120
123
  async run() {
121
124
  const { args, flags } = await this.parse(Decompose);
125
+ let sessionDir;
122
126
  try {
123
127
  // Validate provider
124
128
  if (!isProviderSupported(flags.provider)) {
@@ -160,14 +164,17 @@ export default class Decompose extends Command {
160
164
  }
161
165
  // Create session directory
162
166
  const timestamp = new Date().toISOString().replaceAll(/[:.]/g, '-').split('.')[0];
163
- const sessionDir = join(flags['output-dir'], `session-${timestamp}`);
167
+ sessionDir = join(flags['output-dir'], `session-${timestamp}`);
164
168
  await this.fileManager.createDirectory(sessionDir);
165
169
  this.log(colors.info(`📁 Session directory: ${sessionDir}\n`));
166
170
  // Phase 1: Decompose goal into task graph
167
- this.log(colors.bold('⚙️ Phase 1: Decomposing goal into task graph...\n'));
171
+ this.log(colors.bold('⚙️ Phase 1: Decomposing goal into task graph...'));
172
+ this.log(colors.dim(' (sending goal to AI agent — this typically takes 2-5 minutes)\n'));
173
+ const startPhase1 = Date.now();
168
174
  const decompositionService = new TaskDecompositionService(createAgentRunner(flags.provider, this.logger), this.fileManager, new GlobMatcher(this.fileManager, this.logger), this.logger);
169
175
  const taskGraph = await decompositionService.decomposeGoal(options, sessionDir);
170
- this.log(colors.success('✓ Task graph generated!\n'));
176
+ const phase1Duration = ((Date.now() - startPhase1) / 1000).toFixed(1);
177
+ this.log(colors.success(`✓ Task graph generated in ${phase1Duration}s!\n`));
171
178
  this.displayTaskGraphSummary(taskGraph);
172
179
  // Phase 2: Scaffold session structure
173
180
  this.log(colors.bold('\n⚙️ Phase 2: Creating session structure...\n'));
@@ -229,6 +236,20 @@ export default class Decompose extends Command {
229
236
  this.logger.error({ error: error.message }, 'Decompose command failed');
230
237
  this.log('\n' + colors.error('✗ Decompose failed:'));
231
238
  this.log(colors.error(` ${error.message}`));
239
+ // Clean up empty session directories left by failed runs
240
+ if (sessionDir) {
241
+ try {
242
+ const { readdir, rmdir } = await import('node:fs/promises');
243
+ const contents = await readdir(sessionDir);
244
+ if (contents.length === 0) {
245
+ await rmdir(sessionDir);
246
+ this.logger.debug({ sessionDir }, 'Cleaned up empty session directory');
247
+ }
248
+ }
249
+ catch {
250
+ // Ignore cleanup errors
251
+ }
252
+ }
232
253
  this.error('Execution failed', { exit: 1 });
233
254
  }
234
255
  }
@@ -41,6 +41,7 @@ export default class EpicsCreate extends Command {
41
41
  provider: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
42
42
  task: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
43
43
  timeout: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
44
+ 'review-timeout': import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
44
45
  'max-retries': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
45
46
  'retry-backoff': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
46
47
  };
@@ -0,0 +1,16 @@
1
+ /**
2
+ * MCP Add Command
3
+ *
4
+ * Adds an MCP server to the active configuration by loading its template
5
+ * from server-templates/ and persisting it via McpConfigManager.
6
+ * Prompts for API key if required and not already configured.
7
+ */
8
+ import { Command } from '@oclif/core';
9
+ export default class McpAdd extends Command {
10
+ static args: {
11
+ server: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
12
+ };
13
+ static description: string;
14
+ static examples: string[];
15
+ run(): Promise<void>;
16
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * MCP Add Command
3
+ *
4
+ * Adds an MCP server to the active configuration by loading its template
5
+ * from server-templates/ and persisting it via McpConfigManager.
6
+ * Prompts for API key if required and not already configured.
7
+ */
8
+ import { Args, Command } from '@oclif/core';
9
+ import chalk from 'chalk';
10
+ import { listAvailableTemplates, loadServerTemplate } from '../../mcp/utils/template-loader.js';
11
+ import { McpConfigManager } from '../../services/mcp/mcp-config-manager.js';
12
+ import { McpCredentialManager } from '../../services/mcp/mcp-credential-manager.js';
13
+ import { createLogger } from '../../utils/logger.js';
14
+ const logger = createLogger({ namespace: 'commands:mcp:add' });
15
+ export default class McpAdd extends Command {
16
+ static args = {
17
+ server: Args.string({
18
+ description: 'Name of the MCP server template to add',
19
+ required: true,
20
+ }),
21
+ };
22
+ static description = 'Add an MCP server to the active gateway configuration';
23
+ static examples = [
24
+ '<%= config.bin %> mcp add context7',
25
+ '<%= config.bin %> mcp add exa',
26
+ '<%= config.bin %> mcp add playwright',
27
+ ];
28
+ async run() {
29
+ const { args } = await this.parse(McpAdd);
30
+ const serverName = args.server;
31
+ logger.info('Adding MCP server: %s', serverName);
32
+ // 1. Load the server template
33
+ let template;
34
+ try {
35
+ template = loadServerTemplate(serverName);
36
+ }
37
+ catch (error) {
38
+ const available = listAvailableTemplates();
39
+ this.error(`Server template '${serverName}' not found.\n\nAvailable templates: ${available.join(', ')}`, { exit: 1 });
40
+ return;
41
+ }
42
+ // 2. Add to config
43
+ const configManager = new McpConfigManager();
44
+ configManager.addServer(template);
45
+ this.log(chalk.green(`Server '${serverName}' added to configuration`));
46
+ // 3. Check credential requirements
47
+ if (template.api_key_required && template.env) {
48
+ const credentialManager = new McpCredentialManager();
49
+ const envVarNames = Object.keys(template.env);
50
+ for (const envVar of envVarNames) {
51
+ const existing = credentialManager.resolve(envVar);
52
+ if (existing) {
53
+ this.log(chalk.dim(` Credential ${envVar}: already configured`));
54
+ continue;
55
+ }
56
+ // Prompt for API key if running interactively
57
+ if (process.stdin.isTTY) {
58
+ const { password } = await import('@inquirer/prompts');
59
+ const value = await password({
60
+ mask: '*',
61
+ message: `Enter value for ${envVar}:`,
62
+ });
63
+ if (value) {
64
+ credentialManager.set(envVar, value);
65
+ this.log(chalk.green(` Credential set for ${envVar}`));
66
+ }
67
+ else {
68
+ this.log(chalk.yellow(` Warning: ${envVar} not set. Server may not work without it.`));
69
+ }
70
+ }
71
+ else {
72
+ this.log(chalk.yellow(` Warning: ${envVar} is required but not set. Run in interactive mode or set the environment variable.`));
73
+ }
74
+ }
75
+ }
76
+ }
77
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * MCP Credential Get Command
3
+ *
4
+ * Retrieves and displays a stored credential value in masked format.
5
+ */
6
+ import { Command } from '@oclif/core';
7
+ export default class CredentialGet extends Command {
8
+ static args: {
9
+ key: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
10
+ };
11
+ static description: string;
12
+ static examples: string[];
13
+ run(): Promise<void>;
14
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * MCP Credential Get Command
3
+ *
4
+ * Retrieves and displays a stored credential value in masked format.
5
+ */
6
+ import { Args, Command } from '@oclif/core';
7
+ import chalk from 'chalk';
8
+ import { McpCredentialManager } from '../../../services/mcp/mcp-credential-manager.js';
9
+ import { createLogger } from '../../../utils/logger.js';
10
+ import { maskValue } from '../../../utils/credential-utils.js';
11
+ const logger = createLogger({ namespace: 'commands:mcp:credential:get' });
12
+ export default class CredentialGet extends Command {
13
+ static args = {
14
+ key: Args.string({
15
+ description: 'Credential key name (e.g., EXA_API_KEY)',
16
+ required: true,
17
+ }),
18
+ };
19
+ static description = 'Get an MCP server credential (masked display)';
20
+ static examples = [
21
+ '<%= config.bin %> mcp credential get EXA_API_KEY',
22
+ ];
23
+ async run() {
24
+ const { args } = await this.parse(CredentialGet);
25
+ const { key } = args;
26
+ logger.info('Getting credential: %s', key);
27
+ const credentialManager = new McpCredentialManager();
28
+ const value = credentialManager.resolve(key);
29
+ if (!value) {
30
+ this.error(chalk.red(`Credential '${key}' not found`), { exit: 1 });
31
+ return;
32
+ }
33
+ this.log(`${chalk.bold(key)}: ${maskValue(value)}`);
34
+ }
35
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * MCP Credential List Command
3
+ *
4
+ * Displays all stored credential keys with masked values, indicating
5
+ * which are required by currently enabled servers.
6
+ */
7
+ import { Command } from '@oclif/core';
8
+ export default class CredentialList extends Command {
9
+ static description: string;
10
+ static examples: string[];
11
+ run(): Promise<void>;
12
+ /**
13
+ * Map server name to known env var keys.
14
+ * This is a simple lookup based on known server templates.
15
+ */
16
+ private getServerEnvKeys;
17
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * MCP Credential List Command
3
+ *
4
+ * Displays all stored credential keys with masked values, indicating
5
+ * which are required by currently enabled servers.
6
+ */
7
+ import { Command } from '@oclif/core';
8
+ import chalk from 'chalk';
9
+ import { McpConfigManager } from '../../../services/mcp/mcp-config-manager.js';
10
+ import { McpCredentialManager } from '../../../services/mcp/mcp-credential-manager.js';
11
+ import { createLogger } from '../../../utils/logger.js';
12
+ import { maskValue } from '../../../utils/credential-utils.js';
13
+ const logger = createLogger({ namespace: 'commands:mcp:credential:list' });
14
+ export default class CredentialList extends Command {
15
+ static description = 'List all stored MCP server credentials';
16
+ static examples = [
17
+ '<%= config.bin %> mcp credential list',
18
+ ];
19
+ async run() {
20
+ logger.info('Listing credentials');
21
+ const credentialManager = new McpCredentialManager();
22
+ const configManager = new McpConfigManager();
23
+ const entries = credentialManager.list();
24
+ if (entries.length === 0) {
25
+ this.log(chalk.dim('No credentials stored. Use "bmad-workflow mcp credential set <key> <value>" to add one.'));
26
+ return;
27
+ }
28
+ // Determine which env var keys are required by enabled servers
29
+ const enabledServers = configManager.getEnabledServers();
30
+ const requiredKeys = new Set();
31
+ // We need to check the full server templates to get env var names
32
+ // For now, servers with apiKeyRequired=true imply their env keys are needed
33
+ for (const server of enabledServers) {
34
+ if (server.apiKeyRequired && server.enabled) {
35
+ // Map server name to known env keys via template
36
+ const envKeys = this.getServerEnvKeys(server.name);
37
+ for (const k of envKeys) {
38
+ requiredKeys.add(k);
39
+ }
40
+ }
41
+ }
42
+ this.log(chalk.bold('\nMCP Credentials\n'));
43
+ this.log(`${chalk.dim('Key'.padEnd(25))} ${chalk.dim('Value'.padEnd(25))} ${chalk.dim('Status')}`);
44
+ this.log(chalk.dim('─'.repeat(70)));
45
+ for (const entry of entries) {
46
+ const value = credentialManager.resolve(entry.key);
47
+ const masked = value ? maskValue(value) : chalk.dim('not set');
48
+ const required = requiredKeys.has(entry.key);
49
+ const status = entry.configured
50
+ ? (required ? chalk.green('configured (required)') : chalk.green('configured'))
51
+ : (required ? chalk.yellow('missing (required)') : chalk.dim('not set'));
52
+ this.log(`${entry.key.padEnd(25)} ${masked.padEnd(25)} ${status}`);
53
+ }
54
+ this.log('');
55
+ }
56
+ /**
57
+ * Map server name to known env var keys.
58
+ * This is a simple lookup based on known server templates.
59
+ */
60
+ getServerEnvKeys(serverName) {
61
+ const envMapping = {
62
+ apify: ['APIFY_TOKEN'],
63
+ exa: ['EXA_API_KEY'],
64
+ };
65
+ return envMapping[serverName] || [];
66
+ }
67
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * MCP Credential Remove Command
3
+ *
4
+ * Removes a credential entry from the stored credentials file.
5
+ * Warns if the key is required by an enabled server before proceeding.
6
+ */
7
+ import { Command } from '@oclif/core';
8
+ export default class CredentialRemove extends Command {
9
+ static args: {
10
+ key: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
11
+ };
12
+ static description: string;
13
+ static examples: string[];
14
+ static flags: {
15
+ force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
16
+ };
17
+ run(): Promise<void>;
18
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * MCP Credential Remove Command
3
+ *
4
+ * Removes a credential entry from the stored credentials file.
5
+ * Warns if the key is required by an enabled server before proceeding.
6
+ */
7
+ import { Args, Command, Flags } from '@oclif/core';
8
+ import chalk from 'chalk';
9
+ import { McpConfigManager } from '../../../services/mcp/mcp-config-manager.js';
10
+ import { McpCredentialManager } from '../../../services/mcp/mcp-credential-manager.js';
11
+ import { createLogger } from '../../../utils/logger.js';
12
+ const logger = createLogger({ namespace: 'commands:mcp:credential:remove' });
13
+ /**
14
+ * Map server name to known env var keys.
15
+ */
16
+ function getServerEnvKeys(serverName) {
17
+ const envMapping = {
18
+ apify: ['APIFY_TOKEN'],
19
+ exa: ['EXA_API_KEY'],
20
+ };
21
+ return envMapping[serverName] || [];
22
+ }
23
+ export default class CredentialRemove extends Command {
24
+ static args = {
25
+ key: Args.string({
26
+ description: 'Credential key name to remove',
27
+ required: true,
28
+ }),
29
+ };
30
+ static description = 'Remove an MCP server credential';
31
+ static examples = [
32
+ '<%= config.bin %> mcp credential remove EXA_API_KEY',
33
+ '<%= config.bin %> mcp credential remove APIFY_TOKEN --force',
34
+ ];
35
+ static flags = {
36
+ force: Flags.boolean({
37
+ char: 'f',
38
+ default: false,
39
+ description: 'Skip confirmation when key is required by an enabled server',
40
+ }),
41
+ };
42
+ async run() {
43
+ const { args, flags } = await this.parse(CredentialRemove);
44
+ const { key } = args;
45
+ logger.info('Removing credential: %s', key);
46
+ const credentialManager = new McpCredentialManager();
47
+ const configManager = new McpConfigManager();
48
+ // Check if key is required by any enabled server
49
+ const enabledServers = configManager.getEnabledServers();
50
+ const dependentServers = [];
51
+ for (const server of enabledServers) {
52
+ if (server.apiKeyRequired && server.enabled) {
53
+ const envKeys = getServerEnvKeys(server.name);
54
+ if (envKeys.includes(key)) {
55
+ dependentServers.push(server.name);
56
+ }
57
+ }
58
+ }
59
+ if (dependentServers.length > 0 && !flags.force) {
60
+ this.log(chalk.yellow(`Warning: '${key}' is required by enabled server(s): ${dependentServers.join(', ')}`));
61
+ this.log(chalk.yellow('Removing this credential may break those servers.'));
62
+ this.log(chalk.dim('Use --force to skip this warning.'));
63
+ if (process.stdin.isTTY) {
64
+ const { confirm } = await import('@inquirer/prompts');
65
+ const proceed = await confirm({ default: false, message: 'Proceed with removal?' });
66
+ if (!proceed) {
67
+ this.log(chalk.dim('Removal cancelled.'));
68
+ return;
69
+ }
70
+ }
71
+ else {
72
+ this.error(chalk.red(`Credential '${key}' is required by enabled servers. Use --force to remove.`), { exit: 1 });
73
+ return;
74
+ }
75
+ }
76
+ const removed = credentialManager.remove(key);
77
+ if (removed) {
78
+ this.log(chalk.green(`Credential '${key}' removed successfully`));
79
+ }
80
+ else {
81
+ this.log(chalk.yellow(`Credential '${key}' not found in store`));
82
+ }
83
+ }
84
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * MCP Credential Set Command
3
+ *
4
+ * Stores an API credential key-value pair to ~/.bmad/credentials/mcp-keys.yaml
5
+ * with file permissions 600. Rejects empty values.
6
+ */
7
+ import { Command } from '@oclif/core';
8
+ export default class CredentialSet extends Command {
9
+ static args: {
10
+ key: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
11
+ value: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
12
+ };
13
+ static description: string;
14
+ static examples: string[];
15
+ run(): Promise<void>;
16
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * MCP Credential Set Command
3
+ *
4
+ * Stores an API credential key-value pair to ~/.bmad/credentials/mcp-keys.yaml
5
+ * with file permissions 600. Rejects empty values.
6
+ */
7
+ import { Args, Command } from '@oclif/core';
8
+ import chalk from 'chalk';
9
+ import { McpCredentialManager } from '../../../services/mcp/mcp-credential-manager.js';
10
+ import { createLogger } from '../../../utils/logger.js';
11
+ import { maskValue } from '../../../utils/credential-utils.js';
12
+ const logger = createLogger({ namespace: 'commands:mcp:credential:set' });
13
+ export default class CredentialSet extends Command {
14
+ static args = {
15
+ key: Args.string({
16
+ description: 'Credential key name (e.g., EXA_API_KEY)',
17
+ required: true,
18
+ }),
19
+ value: Args.string({
20
+ description: 'Credential value',
21
+ required: true,
22
+ }),
23
+ };
24
+ static description = 'Set an MCP server credential';
25
+ static examples = [
26
+ '<%= config.bin %> mcp credential set EXA_API_KEY sk-exa-abc123',
27
+ '<%= config.bin %> mcp credential set APIFY_TOKEN apify_api_xyz',
28
+ ];
29
+ async run() {
30
+ const { args } = await this.parse(CredentialSet);
31
+ const { key, value } = args;
32
+ logger.info('Setting credential: %s', key);
33
+ if (!value || value.trim().length === 0) {
34
+ this.error(chalk.red('Value cannot be empty'), { exit: 1 });
35
+ return;
36
+ }
37
+ const credentialManager = new McpCredentialManager();
38
+ credentialManager.set(key, value);
39
+ this.log(chalk.green(`Credential '${key}' set successfully (${maskValue(value)})`));
40
+ }
41
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * MCP Credential Validate Command
3
+ *
4
+ * Validates all credentials required by enabled MCP servers by resolving them
5
+ * via the credential resolution chain and testing against live health checks.
6
+ */
7
+ import { Command } from '@oclif/core';
8
+ export default class CredentialValidate extends Command {
9
+ static description: string;
10
+ static examples: string[];
11
+ run(): Promise<void>;
12
+ }