@sylphx/flow 3.19.0 → 3.20.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/assets/agents/builder.md +12 -3
  3. package/package.json +1 -3
  4. package/src/commands/flow/execute-v2.ts +126 -128
  5. package/src/commands/flow-command.ts +52 -42
  6. package/src/commands/hook-command.ts +161 -20
  7. package/src/config/index.ts +0 -20
  8. package/src/core/agent-loader.ts +2 -2
  9. package/src/core/attach-manager.ts +5 -1
  10. package/src/core/cleanup-handler.ts +20 -16
  11. package/src/core/flow-executor.ts +93 -62
  12. package/src/core/functional/index.ts +0 -11
  13. package/src/core/index.ts +1 -1
  14. package/src/core/project-manager.ts +14 -29
  15. package/src/core/secrets-manager.ts +15 -18
  16. package/src/core/session-manager.ts +4 -8
  17. package/src/core/target-manager.ts +6 -3
  18. package/src/core/upgrade-manager.ts +1 -1
  19. package/src/index.ts +1 -1
  20. package/src/services/auto-upgrade.ts +6 -14
  21. package/src/services/config-service.ts +7 -23
  22. package/src/services/index.ts +1 -1
  23. package/src/targets/claude-code.ts +14 -8
  24. package/src/targets/functional/claude-code-logic.ts +11 -7
  25. package/src/targets/opencode.ts +61 -39
  26. package/src/targets/shared/mcp-transforms.ts +20 -43
  27. package/src/types/agent.types.ts +5 -3
  28. package/src/types/mcp.types.ts +38 -1
  29. package/src/types.ts +4 -0
  30. package/src/utils/agent-enhancer.ts +1 -1
  31. package/src/utils/errors.ts +13 -0
  32. package/src/utils/files/file-operations.ts +16 -0
  33. package/src/utils/index.ts +1 -1
  34. package/src/core/error-handling.ts +0 -482
  35. package/src/core/functional/async.ts +0 -101
  36. package/src/core/functional/either.ts +0 -109
  37. package/src/core/functional/error-handler.ts +0 -135
  38. package/src/core/functional/pipe.ts +0 -189
  39. package/src/core/functional/validation.ts +0 -138
  40. package/src/types/mcp-config.types.ts +0 -448
  41. package/src/utils/error-handler.ts +0 -53
@@ -12,6 +12,7 @@ import os from 'node:os';
12
12
  import path from 'node:path';
13
13
  import { promisify } from 'node:util';
14
14
  import type { Target } from '../types/target.types.js';
15
+ import { readJsonFileSafe } from '../utils/files/file-operations.js';
15
16
  import { targetManager } from './target-manager.js';
16
17
 
17
18
  const execAsync = promisify(exec);
@@ -109,16 +110,11 @@ export class ProjectManager {
109
110
  projectHash: string
110
111
  ): Promise<'claude-code' | 'opencode' | undefined> {
111
112
  const prefsPath = path.join(this.flowHomeDir, 'project-preferences.json');
112
- if (!existsSync(prefsPath)) {
113
- return undefined;
114
- }
115
-
116
- try {
117
- const prefs = JSON.parse(await fs.readFile(prefsPath, 'utf-8'));
118
- return prefs.projects?.[projectHash]?.target;
119
- } catch {
120
- return undefined;
121
- }
113
+ const prefs = await readJsonFileSafe<{ projects?: Record<string, { target?: string }> }>(
114
+ prefsPath,
115
+ {}
116
+ );
117
+ return prefs.projects?.[projectHash]?.target as 'claude-code' | 'opencode' | undefined;
122
118
  }
123
119
 
124
120
  /**
@@ -126,15 +122,10 @@ export class ProjectManager {
126
122
  */
127
123
  async saveProjectTargetPreference(projectHash: string, target: string): Promise<void> {
128
124
  const prefsPath = path.join(this.flowHomeDir, 'project-preferences.json');
129
- let prefs: { projects: Record<string, { target?: string }> } = { projects: {} };
130
-
131
- if (existsSync(prefsPath)) {
132
- try {
133
- prefs = JSON.parse(await fs.readFile(prefsPath, 'utf-8'));
134
- } catch {
135
- // Use default
136
- }
137
- }
125
+ const prefs = await readJsonFileSafe<{ projects: Record<string, { target?: string }> }>(
126
+ prefsPath,
127
+ { projects: {} }
128
+ );
138
129
 
139
130
  if (!prefs.projects) {
140
131
  prefs.projects = {};
@@ -187,16 +178,10 @@ export class ProjectManager {
187
178
  // If both are installed, use global default
188
179
  if (installed.claudeCode && installed.opencode) {
189
180
  const globalSettingsPath = path.join(this.flowHomeDir, 'settings.json');
190
- if (existsSync(globalSettingsPath)) {
191
- try {
192
- const settings = JSON.parse(await fs.readFile(globalSettingsPath, 'utf-8'));
193
- if (settings.defaultTarget) {
194
- await this.saveProjectTargetPreference(projectHash, settings.defaultTarget);
195
- return settings.defaultTarget;
196
- }
197
- } catch {
198
- // Fall through
199
- }
181
+ const settings = await readJsonFileSafe<{ defaultTarget?: string }>(globalSettingsPath, {});
182
+ if (settings.defaultTarget) {
183
+ await this.saveProjectTargetPreference(projectHash, settings.defaultTarget);
184
+ return settings.defaultTarget;
200
185
  }
201
186
 
202
187
  // Both installed, no global default, use claude-code
@@ -8,6 +8,7 @@ import { existsSync } from 'node:fs';
8
8
  import fs from 'node:fs/promises';
9
9
  import path from 'node:path';
10
10
  import type { Target } from '../types/target.types.js';
11
+ import { readJsonFileSafe } from '../utils/files/file-operations.js';
11
12
  import type { ProjectManager } from './project-manager.js';
12
13
  import { resolveTargetOrId } from './target-resolver.js';
13
14
 
@@ -60,20 +61,26 @@ export class SecretsManager {
60
61
  const mcpServers = config[mcpPath] as Record<string, unknown> | undefined;
61
62
  if (mcpServers) {
62
63
  for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
63
- const server = serverConfig as any;
64
+ if (typeof serverConfig !== 'object' || serverConfig === null) {
65
+ continue;
66
+ }
67
+ const server = serverConfig as Record<string, unknown>;
64
68
 
65
69
  // Extract env vars (sensitive) - handle both 'env' and 'environment' keys
66
- const envVars = server.env || server.environment;
67
- if (envVars && Object.keys(envVars).length > 0) {
70
+ const envVars = (server.env || server.environment) as Record<string, string> | undefined;
71
+ if (envVars && typeof envVars === 'object' && Object.keys(envVars).length > 0) {
68
72
  secrets.servers[serverName] = {
69
73
  env: envVars,
70
74
  };
71
75
  }
72
76
 
73
77
  // Extract args (may contain secrets) - handle both 'args' and 'command' array
74
- const args =
75
- server.args || (Array.isArray(server.command) ? server.command.slice(1) : undefined);
76
- if (args && Array.isArray(args) && args.length > 0) {
78
+ const args = Array.isArray(server.args)
79
+ ? server.args
80
+ : Array.isArray(server.command)
81
+ ? (server.command as string[]).slice(1)
82
+ : undefined;
83
+ if (args && args.length > 0) {
77
84
  if (!secrets.servers[serverName]) {
78
85
  secrets.servers[serverName] = {};
79
86
  }
@@ -105,20 +112,10 @@ export class SecretsManager {
105
112
  /**
106
113
  * Load secrets from storage
107
114
  */
108
- async loadSecrets(projectHash: string): Promise<MCPSecrets | null> {
115
+ loadSecrets(projectHash: string): Promise<MCPSecrets | null> {
109
116
  const paths = this.projectManager.getProjectPaths(projectHash);
110
117
  const secretsPath = path.join(paths.secretsDir, 'mcp-env.json');
111
-
112
- if (!existsSync(secretsPath)) {
113
- return null;
114
- }
115
-
116
- try {
117
- const data = await fs.readFile(secretsPath, 'utf-8');
118
- return JSON.parse(data);
119
- } catch {
120
- return null;
121
- }
118
+ return readJsonFileSafe<MCPSecrets | null>(secretsPath, null);
122
119
  }
123
120
 
124
121
  /**
@@ -8,6 +8,7 @@ import { existsSync } from 'node:fs';
8
8
  import fs from 'node:fs/promises';
9
9
  import path from 'node:path';
10
10
  import type { Target } from '../types/target.types.js';
11
+ import { readJsonFileSafe } from '../utils/files/file-operations.js';
11
12
  import type { ProjectManager } from './project-manager.js';
12
13
 
13
14
  export interface Session {
@@ -143,14 +144,9 @@ export class SessionManager {
143
144
  /**
144
145
  * Get active session for a project
145
146
  */
146
- async getActiveSession(projectHash: string): Promise<Session | null> {
147
- try {
148
- const paths = this.projectManager.getProjectPaths(projectHash);
149
- const data = await fs.readFile(paths.sessionFile, 'utf-8');
150
- return JSON.parse(data);
151
- } catch {
152
- return null;
153
- }
147
+ getActiveSession(projectHash: string): Promise<Session | null> {
148
+ const paths = this.projectManager.getProjectPaths(projectHash);
149
+ return readJsonFileSafe<Session | null>(paths.sessionFile, null);
154
150
  }
155
151
 
156
152
  /**
@@ -8,7 +8,7 @@ import {
8
8
  getTargetsWithMCPSupport,
9
9
  isTargetImplemented,
10
10
  } from '../config/targets.js';
11
- import { getOrElse, isSome } from '../core/functional/option.js';
11
+ import { isSome, match } from '../core/functional/option.js';
12
12
  import { projectSettings } from '../utils/config/settings.js';
13
13
  import { promptSelect } from '../utils/prompts/index.js';
14
14
 
@@ -85,9 +85,12 @@ export function createTargetManager(): TargetManager {
85
85
  message: 'Select target platform:',
86
86
  options: availableTargets.map((id) => {
87
87
  const targetOption = getTarget(id);
88
- const target = getOrElse({ id, name: id } as any)(targetOption);
88
+ const label = match(
89
+ (t: ReturnType<typeof getAllTargets>[number]) => t.name || id,
90
+ () => id
91
+ )(targetOption);
89
92
  return {
90
- label: target.name || id,
93
+ label,
91
94
  value: id,
92
95
  };
93
96
  }),
@@ -5,7 +5,7 @@ import { promisify } from 'node:util';
5
5
  import chalk from 'chalk';
6
6
  import { getProjectSettingsFile } from '../config/constants.js';
7
7
  import type { Target } from '../types/target.types.js';
8
- import { CLIError } from '../utils/error-handler.js';
8
+ import { CLIError } from '../utils/errors.js';
9
9
  import { detectPackageManager, getUpgradeCommand } from '../utils/package-manager-detector.js';
10
10
  import { createSpinner, log } from '../utils/prompts/index.js';
11
11
  import type { ProjectState } from './state-detector.js';
package/src/index.ts CHANGED
@@ -126,7 +126,7 @@ function handleCommandError(error: unknown): void {
126
126
 
127
127
  // Handle Commander.js specific errors
128
128
  if (error.name === 'CommanderError') {
129
- const commanderError = error as any;
129
+ const commanderError = error as Error & { code?: string; exitCode?: number };
130
130
 
131
131
  // Don't exit for help or version commands - they should already be handled
132
132
  if (commanderError.code === 'commander.help' || commanderError.code === 'commander.version') {
@@ -10,6 +10,7 @@ import os from 'node:os';
10
10
  import path from 'node:path';
11
11
  import { fileURLToPath } from 'node:url';
12
12
  import { promisify } from 'node:util';
13
+ import { readJsonFileSafe } from '../utils/files/file-operations.js';
13
14
  import { getUpgradeCommand } from '../utils/package-manager-detector.js';
14
15
 
15
16
  const __filename = fileURLToPath(import.meta.url);
@@ -91,13 +92,8 @@ export class AutoUpgrade {
91
92
  /**
92
93
  * Read version info from disk
93
94
  */
94
- private async readVersionInfo(): Promise<VersionInfo | null> {
95
- try {
96
- const data = await fs.readFile(VERSION_FILE, 'utf-8');
97
- return JSON.parse(data);
98
- } catch {
99
- return null;
100
- }
95
+ private readVersionInfo(): Promise<VersionInfo | null> {
96
+ return readJsonFileSafe<VersionInfo | null>(VERSION_FILE, null);
101
97
  }
102
98
 
103
99
  /**
@@ -127,13 +123,9 @@ export class AutoUpgrade {
127
123
  * Get current Flow version from package.json
128
124
  */
129
125
  private async getCurrentVersion(): Promise<string> {
130
- try {
131
- const packageJsonPath = path.join(__dirname, '..', '..', 'package.json');
132
- const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8'));
133
- return packageJson.version;
134
- } catch {
135
- return 'unknown';
136
- }
126
+ const packageJsonPath = path.join(__dirname, '..', '..', 'package.json');
127
+ const pkg = await readJsonFileSafe<{ version?: string }>(packageJsonPath, {});
128
+ return pkg.version ?? 'unknown';
137
129
  }
138
130
 
139
131
  /**
@@ -11,6 +11,7 @@ import {
11
11
  getProjectSettingsFile,
12
12
  USER_SETTINGS_FILE,
13
13
  } from '../config/constants.js';
14
+ import { readJsonFileSafe } from '../utils/files/file-operations.js';
14
15
 
15
16
  /**
16
17
  * User configuration (sensitive data, saved to home directory)
@@ -102,13 +103,8 @@ export class ConfigService {
102
103
  /**
103
104
  * Load user global settings (mainly for API keys)
104
105
  */
105
- static async loadHomeSettings(): Promise<UserSettings> {
106
- try {
107
- const content = await fs.readFile(USER_SETTINGS_FILE, 'utf-8');
108
- return JSON.parse(content);
109
- } catch {
110
- return {};
111
- }
106
+ static loadHomeSettings(): Promise<UserSettings> {
107
+ return readJsonFileSafe<UserSettings>(USER_SETTINGS_FILE, {});
112
108
  }
113
109
 
114
110
  /**
@@ -154,14 +150,8 @@ export class ConfigService {
154
150
  /**
155
151
  * Load project-level settings
156
152
  */
157
- static async loadProjectSettings(cwd: string = process.cwd()): Promise<ProjectSettings> {
158
- try {
159
- const configPath = getProjectSettingsFile(cwd);
160
- const content = await fs.readFile(configPath, 'utf-8');
161
- return JSON.parse(content);
162
- } catch {
163
- return {};
164
- }
153
+ static loadProjectSettings(cwd: string = process.cwd()): Promise<ProjectSettings> {
154
+ return readJsonFileSafe<ProjectSettings>(getProjectSettingsFile(cwd), {});
165
155
  }
166
156
 
167
157
  /**
@@ -186,14 +176,8 @@ export class ConfigService {
186
176
  /**
187
177
  * Load project-local settings (overrides everything)
188
178
  */
189
- static async loadLocalSettings(cwd: string = process.cwd()): Promise<RuntimeChoices> {
190
- try {
191
- const configPath = getProjectLocalSettingsFile(cwd);
192
- const content = await fs.readFile(configPath, 'utf-8');
193
- return JSON.parse(content);
194
- } catch {
195
- return {};
196
- }
179
+ static loadLocalSettings(cwd: string = process.cwd()): Promise<RuntimeChoices> {
180
+ return readJsonFileSafe<RuntimeChoices>(getProjectLocalSettingsFile(cwd), {});
197
181
  }
198
182
 
199
183
  /**
@@ -10,4 +10,4 @@ export {
10
10
  installServers,
11
11
  listAvailableServers,
12
12
  validateServerConfiguration,
13
- } from './mcp-service';
13
+ } from './mcp-service.js';
@@ -11,7 +11,7 @@ import {
11
11
  pathUtils,
12
12
  yamlUtils,
13
13
  } from '../utils/config/target-utils.js';
14
- import { CLIError } from '../utils/error-handler.js';
14
+ import { CLIError } from '../utils/errors.js';
15
15
  import { sanitize } from '../utils/security/security.js';
16
16
  import { DEFAULT_CLAUDE_CODE_ENV } from './functional/claude-code-logic.js';
17
17
  import {
@@ -37,6 +37,11 @@ interface ProcessExitError extends Error {
37
37
  code: number | null;
38
38
  }
39
39
 
40
+ /** Type guard for Node.js errors with errno/code properties */
41
+ function isNodeError(error: unknown): error is NodeJS.ErrnoException {
42
+ return error instanceof Error && 'code' in error;
43
+ }
44
+
40
45
  /**
41
46
  * Claude Code target - composition approach with all original functionality
42
47
  */
@@ -296,16 +301,18 @@ Please begin your response with a comprehensive summary of all the instructions
296
301
  });
297
302
  });
298
303
  } catch (error: unknown) {
299
- if (error instanceof Error) {
300
- const errWithCode = error as Error & { code?: string | number };
301
- if (errWithCode.code === 'ENOENT') {
304
+ if (isNodeError(error)) {
305
+ if (error.code === 'ENOENT') {
302
306
  throw new CLIError('Claude Code not found. Please install it first.', 'CLAUDE_NOT_FOUND');
303
307
  }
304
- if (errWithCode.code !== undefined) {
305
- throw new CLIError(`Claude Code exited with code ${errWithCode.code}`, 'CLAUDE_ERROR');
308
+ if (error.code !== undefined) {
309
+ throw new CLIError(`Claude Code exited with code ${error.code}`, 'CLAUDE_ERROR');
306
310
  }
307
311
  throw new CLIError(`Failed to execute Claude Code: ${error.message}`, 'CLAUDE_ERROR');
308
312
  }
313
+ if (error instanceof Error) {
314
+ throw new CLIError(`Failed to execute Claude Code: ${error.message}`, 'CLAUDE_ERROR');
315
+ }
309
316
  throw new CLIError(`Failed to execute Claude Code: ${String(error)}`, 'CLAUDE_ERROR');
310
317
  }
311
318
  },
@@ -331,8 +338,7 @@ Please begin your response with a comprehensive summary of all the instructions
331
338
  const content = await fsPromises.readFile(settingsPath, 'utf8');
332
339
  settings = JSON.parse(content);
333
340
  } catch (error: unknown) {
334
- const err = error as NodeJS.ErrnoException;
335
- if (err.code !== 'ENOENT') {
341
+ if (!isNodeError(error) || error.code !== 'ENOENT') {
336
342
  throw error;
337
343
  }
338
344
  // File doesn't exist, will create new
@@ -66,6 +66,7 @@ const DEFAULT_CLAUDE_CODE_SETTINGS: Partial<ClaudeCodeSettings> = {
66
66
 
67
67
  export interface HookConfig {
68
68
  notificationCommand?: string;
69
+ sessionStartCommand?: string;
69
70
  }
70
71
 
71
72
  /**
@@ -75,15 +76,16 @@ export interface HookConfig {
75
76
  export const generateHookCommands = async (targetId: string): Promise<HookConfig> => {
76
77
  return {
77
78
  notificationCommand: `sylphx-flow hook --type notification --target ${targetId}`,
79
+ sessionStartCommand: `sylphx-flow hook --type session-start --target ${targetId}`,
78
80
  };
79
81
  };
80
82
 
81
83
  /**
82
84
  * Default hook commands (fallback)
83
- * Simplified to only include notification hook
84
85
  */
85
86
  const DEFAULT_HOOKS: HookConfig = {
86
87
  notificationCommand: 'sylphx-flow hook --type notification --target claude-code',
88
+ sessionStartCommand: 'sylphx-flow hook --type session-start --target claude-code',
87
89
  };
88
90
 
89
91
  /**
@@ -94,17 +96,19 @@ export const processSettings = (
94
96
  hookConfig: HookConfig = DEFAULT_HOOKS
95
97
  ): Result<string, ConfigError> => {
96
98
  const notificationCommand = hookConfig.notificationCommand || DEFAULT_HOOKS.notificationCommand!;
99
+ const sessionStartCommand = hookConfig.sessionStartCommand || DEFAULT_HOOKS.sessionStartCommand!;
97
100
 
98
101
  const hookConfiguration: ClaudeCodeSettings['hooks'] = {
99
102
  Notification: [
100
103
  {
101
104
  matcher: '',
102
- hooks: [
103
- {
104
- type: 'command',
105
- command: notificationCommand,
106
- },
107
- ],
105
+ hooks: [{ type: 'command', command: notificationCommand }],
106
+ },
107
+ ],
108
+ SessionStart: [
109
+ {
110
+ matcher: '',
111
+ hooks: [{ type: 'command', command: sessionStartCommand }],
108
112
  },
109
113
  ],
110
114
  };
@@ -2,8 +2,13 @@ import chalk from 'chalk';
2
2
  import { MCP_SERVER_REGISTRY } from '../config/servers.js';
3
3
  import type { AgentMetadata } from '../types/target-config.types.js';
4
4
  import type { MCPServerConfigUnion, Target } from '../types.js';
5
- import { fileUtils, generateHelpText, yamlUtils } from '../utils/config/target-utils.js';
6
- import { CLIError } from '../utils/error-handler.js';
5
+ import {
6
+ type ConfigData,
7
+ fileUtils,
8
+ generateHelpText,
9
+ yamlUtils,
10
+ } from '../utils/config/target-utils.js';
11
+ import { CLIError } from '../utils/errors.js';
7
12
  import { secretUtils } from '../utils/security/secret-utils.js';
8
13
  import {
9
14
  detectTargetConfig,
@@ -11,6 +16,49 @@ import {
11
16
  transformMCPConfig as transformMCP,
12
17
  } from './shared/index.js';
13
18
 
19
+ /** OpenCode configuration data with MCP config */
20
+ interface OpenCodeConfigData extends ConfigData {
21
+ mcp?: Record<string, unknown>;
22
+ }
23
+
24
+ /**
25
+ * Convert secret environment variables in a single MCP server config to file references
26
+ */
27
+ async function convertServerSecrets(
28
+ cwd: string,
29
+ serverId: string,
30
+ serverConfig: Record<string, unknown>
31
+ ): Promise<void> {
32
+ if (!serverConfig || typeof serverConfig !== 'object' || !('environment' in serverConfig)) {
33
+ return;
34
+ }
35
+
36
+ const envVars = serverConfig.environment as Record<string, string>;
37
+ if (!envVars || typeof envVars !== 'object') {
38
+ return;
39
+ }
40
+
41
+ const serverDef = Object.values(MCP_SERVER_REGISTRY).find((s) => s.name === serverId);
42
+ if (!serverDef?.envVars) {
43
+ return;
44
+ }
45
+
46
+ const secretEnvVars: Record<string, string> = {};
47
+ const nonSecretEnvVars: Record<string, string> = {};
48
+
49
+ for (const [key, value] of Object.entries(envVars)) {
50
+ const envConfig = serverDef.envVars[key];
51
+ if (envConfig?.secret && value && !secretUtils.isFileReference(value)) {
52
+ secretEnvVars[key] = value;
53
+ } else {
54
+ nonSecretEnvVars[key] = value;
55
+ }
56
+ }
57
+
58
+ const convertedSecrets = await secretUtils.convertSecretsToFileReferences(cwd, secretEnvVars);
59
+ serverConfig.environment = { ...nonSecretEnvVars, ...convertedSecrets };
60
+ }
61
+
14
62
  /**
15
63
  * OpenCode target - composition approach with all original functionality
16
64
  */
@@ -92,11 +140,14 @@ export const opencodeTarget: Target = {
92
140
  /**
93
141
  * Read OpenCode configuration with structure normalization
94
142
  */
95
- async readConfig(cwd: string): Promise<any> {
143
+ async readConfig(cwd: string): Promise<OpenCodeConfigData> {
96
144
  const config = await fileUtils.readConfig(opencodeTarget.config, cwd);
97
145
 
98
146
  // Resolve any file references in the configuration
99
- const resolvedConfig = await secretUtils.resolveFileReferences(cwd, config);
147
+ const resolvedConfig = (await secretUtils.resolveFileReferences(
148
+ cwd,
149
+ config
150
+ )) as OpenCodeConfigData;
100
151
 
101
152
  // Ensure the config has the expected structure
102
153
  if (!resolvedConfig.mcp) {
@@ -110,46 +161,17 @@ export const opencodeTarget: Target = {
110
161
  * Write OpenCode configuration with structure normalization
111
162
  */
112
163
  async writeConfig(cwd: string, config: Record<string, unknown>): Promise<void> {
113
- // Ensure the config has the expected structure for OpenCode
114
164
  if (!config.mcp) {
115
165
  config.mcp = {};
116
166
  }
117
167
 
118
- // Convert secrets to file references if secret files are enabled
119
168
  if (opencodeTarget.config.installation?.useSecretFiles) {
120
- // Process each MCP server's environment variables
121
- for (const [serverId, serverConfig] of Object.entries(config.mcp || {})) {
122
- if (serverConfig && typeof serverConfig === 'object' && 'environment' in serverConfig) {
123
- const envVars = serverConfig.environment as Record<string, string>;
124
- if (envVars && typeof envVars === 'object') {
125
- // Find the corresponding server definition to get secret env vars
126
- const serverDef = Object.values(MCP_SERVER_REGISTRY).find((s) => s.name === serverId);
127
- if (serverDef?.envVars) {
128
- // Separate secret and non-secret variables
129
- const secretEnvVars: Record<string, string> = {};
130
- const nonSecretEnvVars: Record<string, string> = {};
131
-
132
- for (const [key, value] of Object.entries(envVars)) {
133
- const envConfig = serverDef.envVars[key];
134
- if (envConfig?.secret && value && !secretUtils.isFileReference(value)) {
135
- secretEnvVars[key] = value;
136
- } else {
137
- nonSecretEnvVars[key] = value;
138
- }
139
- }
140
-
141
- // Convert only secret variables
142
- const convertedSecrets = await secretUtils.convertSecretsToFileReferences(
143
- cwd,
144
- secretEnvVars
145
- );
146
-
147
- // Merge back
148
- serverConfig.environment = { ...nonSecretEnvVars, ...convertedSecrets };
149
- }
150
- }
151
- }
152
- }
169
+ const mcpServers = config.mcp as Record<string, Record<string, unknown>>;
170
+ await Promise.all(
171
+ Object.entries(mcpServers).map(([serverId, serverConfig]) =>
172
+ convertServerSecrets(cwd, serverId, serverConfig)
173
+ )
174
+ );
153
175
  }
154
176
 
155
177
  await fileUtils.writeConfig(opencodeTarget.config, cwd, config);