@sylphx/flow 2.1.3 → 2.1.4

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 (66) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +44 -0
  3. package/package.json +79 -73
  4. package/src/commands/flow/execute-v2.ts +37 -29
  5. package/src/commands/flow/prompt.ts +5 -3
  6. package/src/commands/flow/types.ts +0 -2
  7. package/src/commands/flow-command.ts +20 -13
  8. package/src/commands/hook-command.ts +1 -3
  9. package/src/commands/settings-command.ts +36 -33
  10. package/src/config/ai-config.ts +60 -41
  11. package/src/core/agent-loader.ts +11 -6
  12. package/src/core/attach-manager.ts +92 -84
  13. package/src/core/backup-manager.ts +35 -29
  14. package/src/core/cleanup-handler.ts +11 -8
  15. package/src/core/error-handling.ts +23 -30
  16. package/src/core/flow-executor.ts +58 -76
  17. package/src/core/formatting/bytes.ts +2 -4
  18. package/src/core/functional/async.ts +5 -4
  19. package/src/core/functional/error-handler.ts +2 -2
  20. package/src/core/git-stash-manager.ts +21 -10
  21. package/src/core/installers/file-installer.ts +0 -1
  22. package/src/core/installers/mcp-installer.ts +0 -1
  23. package/src/core/project-manager.ts +24 -18
  24. package/src/core/secrets-manager.ts +54 -73
  25. package/src/core/session-manager.ts +20 -22
  26. package/src/core/state-detector.ts +139 -80
  27. package/src/core/template-loader.ts +13 -31
  28. package/src/core/upgrade-manager.ts +122 -69
  29. package/src/index.ts +8 -5
  30. package/src/services/auto-upgrade.ts +1 -1
  31. package/src/services/config-service.ts +41 -29
  32. package/src/services/global-config.ts +2 -2
  33. package/src/services/target-installer.ts +9 -7
  34. package/src/targets/claude-code.ts +24 -12
  35. package/src/targets/opencode.ts +17 -6
  36. package/src/types/cli.types.ts +2 -2
  37. package/src/types/provider.types.ts +1 -7
  38. package/src/types/session.types.ts +11 -11
  39. package/src/types/target.types.ts +3 -1
  40. package/src/types/todo.types.ts +1 -1
  41. package/src/types.ts +1 -1
  42. package/src/utils/__tests__/package-manager-detector.test.ts +6 -6
  43. package/src/utils/agent-enhancer.ts +4 -4
  44. package/src/utils/config/paths.ts +3 -1
  45. package/src/utils/config/target-utils.ts +2 -2
  46. package/src/utils/display/banner.ts +2 -2
  47. package/src/utils/display/notifications.ts +58 -45
  48. package/src/utils/display/status.ts +29 -12
  49. package/src/utils/files/file-operations.ts +1 -1
  50. package/src/utils/files/sync-utils.ts +38 -41
  51. package/src/utils/index.ts +19 -27
  52. package/src/utils/package-manager-detector.ts +15 -5
  53. package/src/utils/security/security.ts +8 -4
  54. package/src/utils/target-selection.ts +5 -2
  55. package/src/utils/version.ts +4 -2
  56. package/src/commands/flow-orchestrator.ts +0 -328
  57. package/src/commands/init-command.ts +0 -92
  58. package/src/commands/init-core.ts +0 -331
  59. package/src/core/agent-manager.ts +0 -174
  60. package/src/core/loop-controller.ts +0 -200
  61. package/src/core/rule-loader.ts +0 -147
  62. package/src/core/rule-manager.ts +0 -240
  63. package/src/services/claude-config-service.ts +0 -252
  64. package/src/services/first-run-setup.ts +0 -220
  65. package/src/services/smart-config-service.ts +0 -269
  66. package/src/types/api.types.ts +0 -9
@@ -10,12 +10,15 @@
10
10
  */
11
11
 
12
12
  import fs from 'node:fs/promises';
13
- import path from 'node:path';
14
13
  import os from 'node:os';
14
+ import path from 'node:path';
15
15
  import { z } from 'zod';
16
- import { type Result, success, tryCatchAsync } from '../core/functional/result.js';
16
+ import { type Result, tryCatchAsync } from '../core/functional/result.js';
17
17
  import { getAllProviders } from '../providers/index.js';
18
- import type { ProviderId, ProviderConfigValue as ProviderConfigValueType } from '../types/provider.types.js';
18
+ import type {
19
+ ProviderConfigValue as ProviderConfigValueType,
20
+ ProviderId,
21
+ } from '../types/provider.types.js';
19
22
 
20
23
  // Re-export types for backward compatibility
21
24
  export type { ProviderId } from '../types/provider.types.js';
@@ -39,14 +42,20 @@ export type ProviderConfigValue = ProviderConfigValueType;
39
42
  * Uses generic Record for provider configs - validation happens at provider level
40
43
  */
41
44
  const aiConfigSchema = z.object({
42
- defaultProvider: z.enum(['anthropic', 'openai', 'google', 'openrouter', 'claude-code', 'zai']).optional(),
45
+ defaultProvider: z
46
+ .enum(['anthropic', 'openai', 'google', 'openrouter', 'claude-code', 'zai'])
47
+ .optional(),
43
48
  defaultModel: z.string().optional(),
44
- providers: z.record(
45
- z.string(),
46
- z.object({
47
- defaultModel: z.string().optional(),
48
- }).passthrough() // Allow additional fields defined by provider
49
- ).optional(),
49
+ providers: z
50
+ .record(
51
+ z.string(),
52
+ z
53
+ .object({
54
+ defaultModel: z.string().optional(),
55
+ })
56
+ .passthrough() // Allow additional fields defined by provider
57
+ )
58
+ .optional(),
50
59
  });
51
60
 
52
61
  export type AIConfig = z.infer<typeof aiConfigSchema>;
@@ -61,7 +70,9 @@ const LOCAL_CONFIG_FILE = '.sylphx-flow/settings.local.json';
61
70
  /**
62
71
  * Get AI config file paths in priority order
63
72
  */
64
- export const getAIConfigPaths = (cwd: string = process.cwd()): {
73
+ export const getAIConfigPaths = (
74
+ cwd: string = process.cwd()
75
+ ): {
65
76
  global: string;
66
77
  project: string;
67
78
  local: string;
@@ -79,8 +90,9 @@ const loadConfigFile = async (filePath: string): Promise<AIConfig | null> => {
79
90
  const content = await fs.readFile(filePath, 'utf8');
80
91
  const parsed = JSON.parse(content);
81
92
  return aiConfigSchema.parse(parsed);
82
- } catch (error: any) {
83
- if (error.code === 'ENOENT') {
93
+ } catch (error: unknown) {
94
+ const err = error as NodeJS.ErrnoException;
95
+ if (err.code === 'ENOENT') {
84
96
  return null; // File doesn't exist
85
97
  }
86
98
  throw error; // Re-throw other errors
@@ -97,7 +109,7 @@ const mergeConfigs = (a: AIConfig, b: AIConfig): AIConfig => {
97
109
  ...Object.keys(b.providers || {}),
98
110
  ]);
99
111
 
100
- const mergedProviders: Record<string, any> = {};
112
+ const mergedProviders: Record<string, Record<string, unknown>> = {};
101
113
  for (const providerId of allProviderIds) {
102
114
  mergedProviders[providerId] = {
103
115
  ...a.providers?.[providerId],
@@ -117,30 +129,31 @@ const mergeConfigs = (a: AIConfig, b: AIConfig): AIConfig => {
117
129
  */
118
130
  export const aiConfigExists = async (cwd: string = process.cwd()): Promise<boolean> => {
119
131
  const paths = getAIConfigPaths(cwd);
120
- try {
121
- // Check any of the config files
122
- await fs.access(paths.global).catch(() => {});
123
- return true;
124
- } catch {}
125
-
126
- try {
127
- await fs.access(paths.project);
128
- return true;
129
- } catch {}
130
132
 
131
- try {
132
- await fs.access(paths.local);
133
- return true;
134
- } catch {}
133
+ // Check if any config file exists
134
+ const checks = await Promise.all([
135
+ fs
136
+ .access(paths.global)
137
+ .then(() => true)
138
+ .catch(() => false),
139
+ fs
140
+ .access(paths.project)
141
+ .then(() => true)
142
+ .catch(() => false),
143
+ fs
144
+ .access(paths.local)
145
+ .then(() => true)
146
+ .catch(() => false),
147
+ ]);
135
148
 
136
- return false;
149
+ return checks.some(Boolean);
137
150
  };
138
151
 
139
152
  /**
140
153
  * Load AI configuration
141
154
  * Merges global, project, and local configs with priority: local > project > global
142
155
  */
143
- export const loadAIConfig = async (cwd: string = process.cwd()): Promise<Result<AIConfig, Error>> => {
156
+ export const loadAIConfig = (cwd: string = process.cwd()): Promise<Result<AIConfig, Error>> => {
144
157
  return tryCatchAsync(
145
158
  async () => {
146
159
  const paths = getAIConfigPaths(cwd);
@@ -156,13 +169,19 @@ export const loadAIConfig = async (cwd: string = process.cwd()): Promise<Result<
156
169
  let merged: AIConfig = {};
157
170
 
158
171
  // Merge in priority order: global < project < local
159
- if (globalConfig) merged = mergeConfigs(merged, globalConfig);
160
- if (projectConfig) merged = mergeConfigs(merged, projectConfig);
161
- if (localConfig) merged = mergeConfigs(merged, localConfig);
172
+ if (globalConfig) {
173
+ merged = mergeConfigs(merged, globalConfig);
174
+ }
175
+ if (projectConfig) {
176
+ merged = mergeConfigs(merged, projectConfig);
177
+ }
178
+ if (localConfig) {
179
+ merged = mergeConfigs(merged, localConfig);
180
+ }
162
181
 
163
182
  return merged;
164
183
  },
165
- (error: any) => new Error(`Failed to load AI config: ${error.message}`)
184
+ (error: unknown) => new Error(`Failed to load AI config: ${(error as Error).message}`)
166
185
  );
167
186
  };
168
187
 
@@ -171,7 +190,7 @@ export const loadAIConfig = async (cwd: string = process.cwd()): Promise<Result<
171
190
  * By default, all configuration (including API keys) goes to ~/.sylphx-flow/settings.json
172
191
  * Automatically sets default provider if not set
173
192
  */
174
- export const saveAIConfig = async (
193
+ export const saveAIConfig = (
175
194
  config: AIConfig,
176
195
  cwd: string = process.cwd()
177
196
  ): Promise<Result<void, Error>> => {
@@ -211,16 +230,16 @@ export const saveAIConfig = async (
211
230
  const validated = aiConfigSchema.parse(configToSave);
212
231
 
213
232
  // Write config
214
- await fs.writeFile(configPath, JSON.stringify(validated, null, 2) + '\n', 'utf8');
233
+ await fs.writeFile(configPath, `${JSON.stringify(validated, null, 2)}\n`, 'utf8');
215
234
  },
216
- (error: any) => new Error(`Failed to save AI config: ${error.message}`)
235
+ (error: unknown) => new Error(`Failed to save AI config: ${(error as Error).message}`)
217
236
  );
218
237
  };
219
238
 
220
239
  /**
221
240
  * Save AI configuration to a specific location
222
241
  */
223
- export const saveAIConfigTo = async (
242
+ export const saveAIConfigTo = (
224
243
  config: AIConfig,
225
244
  location: 'global' | 'project' | 'local',
226
245
  cwd: string = process.cwd()
@@ -237,9 +256,10 @@ export const saveAIConfigTo = async (
237
256
  const validated = aiConfigSchema.parse(config);
238
257
 
239
258
  // Write config
240
- await fs.writeFile(configPath, JSON.stringify(validated, null, 2) + '\n', 'utf8');
259
+ await fs.writeFile(configPath, `${JSON.stringify(validated, null, 2)}\n`, 'utf8');
241
260
  },
242
- (error: any) => new Error(`Failed to save AI config to ${location}: ${error.message}`)
261
+ (error: unknown) =>
262
+ new Error(`Failed to save AI config to ${location}: ${(error as Error).message}`)
243
263
  );
244
264
  };
245
265
 
@@ -306,4 +326,3 @@ export const getConfiguredProviders = async (
306
326
 
307
327
  return providers;
308
328
  };
309
-
@@ -3,9 +3,9 @@
3
3
  * Loads agent definitions from markdown files with front matter
4
4
  */
5
5
 
6
- import { readFile, readdir, access } from 'node:fs/promises';
7
- import { join, parse, relative, dirname } from 'node:path';
6
+ import { access, readdir, readFile } from 'node:fs/promises';
8
7
  import { homedir } from 'node:os';
8
+ import { dirname, join, parse, relative } from 'node:path';
9
9
  import { fileURLToPath } from 'node:url';
10
10
  import matter from 'gray-matter';
11
11
  import type { Agent, AgentMetadata } from '../types/agent.types.js';
@@ -52,7 +52,10 @@ export async function loadAgentFromFile(
52
52
  /**
53
53
  * Load all agents from a directory (recursively)
54
54
  */
55
- export async function loadAgentsFromDirectory(dirPath: string, isBuiltin: boolean = false): Promise<Agent[]> {
55
+ export async function loadAgentsFromDirectory(
56
+ dirPath: string,
57
+ isBuiltin: boolean = false
58
+ ): Promise<Agent[]> {
56
59
  try {
57
60
  // Read directory recursively to support subdirectories
58
61
  const files = await readdir(dirPath, { recursive: true, withFileTypes: true });
@@ -72,7 +75,7 @@ export async function loadAgentsFromDirectory(dirPath: string, isBuiltin: boolea
72
75
  );
73
76
 
74
77
  return agents.filter((agent): agent is Agent => agent !== null);
75
- } catch (error) {
78
+ } catch (_error) {
76
79
  // Directory doesn't exist or can't be read
77
80
  return [];
78
81
  }
@@ -117,7 +120,7 @@ export async function loadAllAgents(cwd: string, targetAgentDir?: string): Promi
117
120
  const systemPath = await getSystemAgentsPath();
118
121
  const [globalPath, projectPath] = getAgentSearchPaths(cwd);
119
122
 
120
- let allAgentPaths = [systemPath, globalPath, projectPath];
123
+ const allAgentPaths = [systemPath, globalPath, projectPath];
121
124
 
122
125
  // If a target-specific agent directory is provided, add it with highest priority
123
126
  if (targetAgentDir) {
@@ -126,7 +129,9 @@ export async function loadAllAgents(cwd: string, targetAgentDir?: string): Promi
126
129
  }
127
130
 
128
131
  // Load agents from all paths
129
- const loadedAgentsPromises = allAgentPaths.map(path => loadAgentsFromDirectory(path, path === systemPath));
132
+ const loadedAgentsPromises = allAgentPaths.map((path) =>
133
+ loadAgentsFromDirectory(path, path === systemPath)
134
+ );
130
135
  const loadedAgentsArrays = await Promise.all(loadedAgentsPromises);
131
136
 
132
137
  // Flatten and deduplicate
@@ -4,15 +4,17 @@
4
4
  * Strategy: Direct override with backup, restore on cleanup
5
5
  */
6
6
 
7
+ import { createHash } from 'node:crypto';
8
+ import { existsSync } from 'node:fs';
7
9
  import fs from 'node:fs/promises';
8
10
  import path from 'node:path';
9
- import { existsSync } from 'node:fs';
10
- import { createHash } from 'node:crypto';
11
11
  import chalk from 'chalk';
12
- import { ProjectManager } from './project-manager.js';
13
- import type { BackupManifest } from './backup-manager.js';
14
- import { GlobalConfigService } from '../services/global-config.js';
15
12
  import { MCP_SERVER_REGISTRY } from '../config/servers.js';
13
+ import { GlobalConfigService } from '../services/global-config.js';
14
+ import type { Target } from '../types/target.types.js';
15
+ import type { BackupManifest } from './backup-manager.js';
16
+ import type { ProjectManager } from './project-manager.js';
17
+ import { targetManager } from './target-manager.js';
16
18
 
17
19
  export interface AttachResult {
18
20
  agentsAdded: string[];
@@ -39,13 +41,12 @@ export interface FlowTemplates {
39
41
  agents: Array<{ name: string; content: string }>;
40
42
  commands: Array<{ name: string; content: string }>;
41
43
  rules?: string;
42
- mcpServers: Array<{ name: string; config: any }>;
44
+ mcpServers: Array<{ name: string; config: Record<string, unknown> }>;
43
45
  hooks: Array<{ name: string; content: string }>;
44
46
  singleFiles: Array<{ path: string; content: string }>;
45
47
  }
46
48
 
47
49
  export class AttachManager {
48
- private projectManager: ProjectManager;
49
50
  private configService: GlobalConfigService;
50
51
 
51
52
  constructor(projectManager: ProjectManager) {
@@ -66,26 +67,25 @@ export class AttachManager {
66
67
  }
67
68
 
68
69
  /**
69
- * Get target-specific directory names
70
+ * Resolve target from ID string to Target object
70
71
  */
71
- private getTargetDirs(target: 'claude-code' | 'opencode'): {
72
- agents: string;
73
- commands: string;
74
- } {
75
- return target === 'claude-code'
76
- ? { agents: 'agents', commands: 'commands' }
77
- : { agents: 'agent', commands: 'command' };
72
+ private resolveTarget(targetId: string): Target {
73
+ const targetOption = targetManager.getTarget(targetId);
74
+ if (targetOption._tag === 'None') {
75
+ throw new Error(`Unknown target: ${targetId}`);
76
+ }
77
+ return targetOption.value;
78
78
  }
79
79
 
80
80
  /**
81
81
  * Load global MCP servers from ~/.sylphx-flow/mcp-config.json
82
82
  */
83
83
  private async loadGlobalMCPServers(
84
- target: 'claude-code' | 'opencode'
85
- ): Promise<Array<{ name: string; config: any }>> {
84
+ _target: Target
85
+ ): Promise<Array<{ name: string; config: Record<string, unknown> }>> {
86
86
  try {
87
87
  const enabledServers = await this.configService.getEnabledMCPServers();
88
- const servers: Array<{ name: string; config: any }> = [];
88
+ const servers: Array<{ name: string; config: Record<string, unknown> }> = [];
89
89
 
90
90
  for (const [serverKey, serverConfig] of Object.entries(enabledServers)) {
91
91
  // Lookup server definition in registry
@@ -97,7 +97,7 @@ export class AttachManager {
97
97
  }
98
98
 
99
99
  // Clone the server config from registry
100
- let config: any = { ...serverDef.config };
100
+ const config: Record<string, unknown> = { ...serverDef.config };
101
101
 
102
102
  // Merge environment variables from global config
103
103
  if (serverConfig.env && Object.keys(serverConfig.env).length > 0) {
@@ -110,7 +110,7 @@ export class AttachManager {
110
110
  }
111
111
 
112
112
  return servers;
113
- } catch (error) {
113
+ } catch (_error) {
114
114
  // If global config doesn't exist or fails to load, return empty array
115
115
  return [];
116
116
  }
@@ -119,15 +119,21 @@ export class AttachManager {
119
119
  /**
120
120
  * Attach Flow templates to project
121
121
  * Strategy: Override with warning, backup handles restoration
122
+ * @param projectPath - Project root path
123
+ * @param _projectHash - Project hash (unused but kept for API compatibility)
124
+ * @param targetOrId - Target object or target ID string
125
+ * @param templates - Flow templates to attach
126
+ * @param manifest - Backup manifest to track changes
122
127
  */
123
128
  async attach(
124
129
  projectPath: string,
125
- projectHash: string,
126
- target: 'claude-code' | 'opencode',
130
+ _projectHash: string,
131
+ targetOrId: Target | string,
127
132
  templates: FlowTemplates,
128
133
  manifest: BackupManifest
129
134
  ): Promise<AttachResult> {
130
- const targetDir = this.projectManager.getTargetConfigDir(projectPath, target);
135
+ // Resolve target from ID if needed
136
+ const target = typeof targetOrId === 'string' ? this.resolveTarget(targetOrId) : targetOrId;
131
137
 
132
138
  const result: AttachResult = {
133
139
  agentsAdded: [],
@@ -143,18 +149,17 @@ export class AttachManager {
143
149
  conflicts: [],
144
150
  };
145
151
 
146
- // Ensure target directory exists
147
- await fs.mkdir(targetDir, { recursive: true });
152
+ // All paths are relative to projectPath, using target.config.* directly
148
153
 
149
154
  // 1. Attach agents
150
- await this.attachAgents(targetDir, target, templates.agents, result, manifest);
155
+ await this.attachAgents(projectPath, target, templates.agents, result, manifest);
151
156
 
152
157
  // 2. Attach commands
153
- await this.attachCommands(targetDir, target, templates.commands, result, manifest);
158
+ await this.attachCommands(projectPath, target, templates.commands, result, manifest);
154
159
 
155
160
  // 3. Attach rules (if applicable)
156
161
  if (templates.rules) {
157
- await this.attachRules(targetDir, target, templates.rules, result, manifest);
162
+ await this.attachRules(projectPath, target, templates.rules, result, manifest);
158
163
  }
159
164
 
160
165
  // 4. Attach MCP servers (merge global + template servers)
@@ -162,18 +167,12 @@ export class AttachManager {
162
167
  const allMCPServers = [...globalMCPServers, ...templates.mcpServers];
163
168
 
164
169
  if (allMCPServers.length > 0) {
165
- await this.attachMCPServers(
166
- targetDir,
167
- target,
168
- allMCPServers,
169
- result,
170
- manifest
171
- );
170
+ await this.attachMCPServers(projectPath, target, allMCPServers, result, manifest);
172
171
  }
173
172
 
174
173
  // 5. Attach hooks
175
174
  if (templates.hooks.length > 0) {
176
- await this.attachHooks(targetDir, templates.hooks, result, manifest);
175
+ await this.attachHooks(projectPath, target, templates.hooks, result, manifest);
177
176
  }
178
177
 
179
178
  // 6. Attach single files
@@ -191,14 +190,14 @@ export class AttachManager {
191
190
  * Attach agents (override strategy)
192
191
  */
193
192
  private async attachAgents(
194
- targetDir: string,
195
- target: 'claude-code' | 'opencode',
193
+ projectPath: string,
194
+ target: Target,
196
195
  agents: Array<{ name: string; content: string }>,
197
196
  result: AttachResult,
198
197
  manifest: BackupManifest
199
198
  ): Promise<void> {
200
- const dirs = this.getTargetDirs(target);
201
- const agentsDir = path.join(targetDir, dirs.agents);
199
+ // Use full path from target config
200
+ const agentsDir = path.join(projectPath, target.config.agentDir);
202
201
  await fs.mkdir(agentsDir, { recursive: true });
203
202
 
204
203
  for (const agent of agents) {
@@ -233,14 +232,14 @@ export class AttachManager {
233
232
  * Attach commands (override strategy)
234
233
  */
235
234
  private async attachCommands(
236
- targetDir: string,
237
- target: 'claude-code' | 'opencode',
235
+ projectPath: string,
236
+ target: Target,
238
237
  commands: Array<{ name: string; content: string }>,
239
238
  result: AttachResult,
240
239
  manifest: BackupManifest
241
240
  ): Promise<void> {
242
- const dirs = this.getTargetDirs(target);
243
- const commandsDir = path.join(targetDir, dirs.commands);
241
+ // Use full path from target config
242
+ const commandsDir = path.join(projectPath, target.config.slashCommandsDir);
244
243
  await fs.mkdir(commandsDir, { recursive: true });
245
244
 
246
245
  for (const command of commands) {
@@ -275,19 +274,18 @@ export class AttachManager {
275
274
  * Attach rules (append strategy for AGENTS.md)
276
275
  */
277
276
  private async attachRules(
278
- targetDir: string,
279
- target: 'claude-code' | 'opencode',
277
+ projectPath: string,
278
+ target: Target,
280
279
  rules: string,
281
280
  result: AttachResult,
282
281
  manifest: BackupManifest
283
282
  ): Promise<void> {
284
- // Claude Code: .claude/agents/AGENTS.md
285
- // OpenCode: .opencode/AGENTS.md
286
- const dirs = this.getTargetDirs(target);
287
- const rulesPath =
288
- target === 'claude-code'
289
- ? path.join(targetDir, dirs.agents, 'AGENTS.md')
290
- : path.join(targetDir, 'AGENTS.md');
283
+ // Use full paths from target config:
284
+ // - rulesFile defined (e.g., OpenCode): projectPath/rulesFile
285
+ // - rulesFile undefined (e.g., Claude Code): projectPath/agentDir/AGENTS.md
286
+ const rulesPath = target.config.rulesFile
287
+ ? path.join(projectPath, target.config.rulesFile)
288
+ : path.join(projectPath, target.config.agentDir, 'AGENTS.md');
291
289
 
292
290
  if (existsSync(rulesPath)) {
293
291
  // User has AGENTS.md, append Flow rules
@@ -332,34 +330,43 @@ ${rules}
332
330
 
333
331
  /**
334
332
  * Attach MCP servers (merge strategy)
333
+ * Uses target.config.configFile and target.config.mcpConfigPath
334
+ * Note: configFile is relative to project root, not targetDir
335
335
  */
336
336
  private async attachMCPServers(
337
- targetDir: string,
338
- target: 'claude-code' | 'opencode',
339
- mcpServers: Array<{ name: string; config: any }>,
337
+ projectPath: string,
338
+ target: Target,
339
+ mcpServers: Array<{ name: string; config: Record<string, unknown> }>,
340
340
  result: AttachResult,
341
341
  manifest: BackupManifest
342
342
  ): Promise<void> {
343
- // Claude Code: .claude/settings.json (mcp.servers)
344
- // OpenCode: .opencode/.mcp.json
345
- const configPath =
346
- target === 'claude-code'
347
- ? path.join(targetDir, 'settings.json')
348
- : path.join(targetDir, '.mcp.json');
343
+ // Use target config for file path and MCP config structure
344
+ // Claude Code: .mcp.json at project root with mcpServers key
345
+ // OpenCode: opencode.jsonc at project root with mcp key
346
+ const configPath = path.join(projectPath, target.config.configFile);
347
+ const mcpPath = target.config.mcpConfigPath;
349
348
 
350
- let config: any = {};
349
+ let config: Record<string, unknown> = {};
351
350
 
352
351
  if (existsSync(configPath)) {
353
352
  config = JSON.parse(await fs.readFile(configPath, 'utf-8'));
354
353
  }
355
354
 
356
- // Ensure mcp.servers exists
357
- if (!config.mcp) config.mcp = {};
358
- if (!config.mcp.servers) config.mcp.servers = {};
355
+ // Get or create the MCP servers object at the correct path
356
+ // Claude Code: config.mcpServers = {}
357
+ // OpenCode: config.mcp = {}
358
+ let mcpContainer = config[mcpPath] as Record<string, unknown> | undefined;
359
+ if (!mcpContainer) {
360
+ mcpContainer = {};
361
+ config[mcpPath] = mcpContainer;
362
+ }
359
363
 
360
364
  // Add Flow MCP servers
361
365
  for (const server of mcpServers) {
362
- if (config.mcp.servers[server.name]) {
366
+ // Transform the server config for this target
367
+ const transformedConfig = target.transformMCPConfig(server.config as any, server.name);
368
+
369
+ if (mcpContainer[server.name]) {
363
370
  // Conflict: user has same MCP server
364
371
  result.mcpServersOverridden.push(server.name);
365
372
  result.conflicts.push({
@@ -372,8 +379,8 @@ ${rules}
372
379
  result.mcpServersAdded.push(server.name);
373
380
  }
374
381
 
375
- // Override with Flow config
376
- config.mcp.servers[server.name] = server.config;
382
+ // Override with Flow config (transformed for target)
383
+ mcpContainer[server.name] = transformedConfig;
377
384
  }
378
385
 
379
386
  // Write updated config
@@ -383,7 +390,7 @@ ${rules}
383
390
  manifest.backup.config = {
384
391
  path: configPath,
385
392
  hash: await this.calculateFileHash(configPath),
386
- mcpServersCount: Object.keys(config.mcp.servers).length,
393
+ mcpServersCount: Object.keys(mcpContainer).length,
387
394
  };
388
395
  }
389
396
 
@@ -391,12 +398,14 @@ ${rules}
391
398
  * Attach hooks (override strategy)
392
399
  */
393
400
  private async attachHooks(
394
- targetDir: string,
401
+ projectPath: string,
402
+ target: Target,
395
403
  hooks: Array<{ name: string; content: string }>,
396
404
  result: AttachResult,
397
- manifest: BackupManifest
405
+ _manifest: BackupManifest
398
406
  ): Promise<void> {
399
- const hooksDir = path.join(targetDir, 'hooks');
407
+ // Hooks are in configDir/hooks
408
+ const hooksDir = path.join(projectPath, target.config.configDir, 'hooks');
400
409
  await fs.mkdir(hooksDir, { recursive: true });
401
410
 
402
411
  for (const hook of hooks) {
@@ -430,13 +439,16 @@ ${rules}
430
439
  result: AttachResult,
431
440
  manifest: BackupManifest
432
441
  ): Promise<void> {
433
- // Get target from manifest to determine correct directory
434
- const target = manifest.target;
435
- const targetDir = this.projectManager.getTargetConfigDir(projectPath, target);
442
+ // Get target from manifest to determine config directory
443
+ const targetOption = targetManager.getTarget(manifest.target);
444
+ if (targetOption._tag === 'None') {
445
+ return; // Unknown target, skip
446
+ }
447
+ const target = targetOption.value;
436
448
 
437
449
  for (const file of singleFiles) {
438
- // Write to target config directory, not project root
439
- const filePath = path.join(targetDir, file.path);
450
+ // Write to target config directory (e.g., .claude/ or .opencode/)
451
+ const filePath = path.join(projectPath, target.config.configDir, file.path);
440
452
  const existed = existsSync(filePath);
441
453
 
442
454
  if (existed) {
@@ -474,13 +486,9 @@ ${rules}
474
486
  console.log(chalk.yellow('\n⚠️ Conflicts detected:\n'));
475
487
 
476
488
  for (const conflict of result.conflicts) {
477
- console.log(
478
- chalk.yellow(` • ${conflict.type}: ${conflict.name} - ${conflict.action}`)
479
- );
489
+ console.log(chalk.yellow(` • ${conflict.type}: ${conflict.name} - ${conflict.action}`));
480
490
  }
481
491
 
482
- console.log(
483
- chalk.dim('\n Don\'t worry! All overridden content will be restored on exit.\n')
484
- );
492
+ console.log(chalk.dim("\n Don't worry! All overridden content will be restored on exit.\n"));
485
493
  }
486
494
  }