@sylphx/flow 2.1.4 → 2.1.5
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.
- package/CHANGELOG.md +16 -0
- package/package.json +1 -1
- package/src/commands/settings/checkbox-config.ts +128 -0
- package/src/commands/settings/index.ts +6 -0
- package/src/commands/settings-command.ts +63 -138
- package/src/core/attach/file-attacher.ts +172 -0
- package/src/core/attach/index.ts +5 -0
- package/src/core/attach-manager.ts +32 -94
- package/src/services/global-config.ts +1 -1
- package/src/services/target-installer.ts +2 -19
- package/src/targets/claude-code.ts +12 -70
- package/src/targets/opencode.ts +11 -62
- package/src/targets/shared/index.ts +7 -0
- package/src/targets/shared/mcp-transforms.ts +132 -0
- package/src/targets/shared/target-operations.ts +135 -0
- package/src/utils/target-selection.ts +1 -6
|
@@ -12,6 +12,7 @@ import chalk from 'chalk';
|
|
|
12
12
|
import { MCP_SERVER_REGISTRY } from '../config/servers.js';
|
|
13
13
|
import { GlobalConfigService } from '../services/global-config.js';
|
|
14
14
|
import type { Target } from '../types/target.types.js';
|
|
15
|
+
import { attachItemsToDir, attachRulesFile } from './attach/index.js';
|
|
15
16
|
import type { BackupManifest } from './backup-manager.js';
|
|
16
17
|
import type { ProjectManager } from './project-manager.js';
|
|
17
18
|
import { targetManager } from './target-manager.js';
|
|
@@ -188,6 +189,7 @@ export class AttachManager {
|
|
|
188
189
|
|
|
189
190
|
/**
|
|
190
191
|
* Attach agents (override strategy)
|
|
192
|
+
* Uses shared attachItemsToDir function
|
|
191
193
|
*/
|
|
192
194
|
private async attachAgents(
|
|
193
195
|
projectPath: string,
|
|
@@ -196,40 +198,24 @@ export class AttachManager {
|
|
|
196
198
|
result: AttachResult,
|
|
197
199
|
manifest: BackupManifest
|
|
198
200
|
): Promise<void> {
|
|
199
|
-
// Use full path from target config
|
|
200
201
|
const agentsDir = path.join(projectPath, target.config.agentDir);
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
action: 'overridden',
|
|
214
|
-
message: `Agent '${agent.name}' overridden (will be restored on exit)`,
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
// Track in manifest
|
|
218
|
-
manifest.backup.agents.user.push(agent.name);
|
|
219
|
-
} else {
|
|
220
|
-
result.agentsAdded.push(agent.name);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// Write Flow agent (override)
|
|
224
|
-
await fs.writeFile(agentPath, agent.content);
|
|
225
|
-
|
|
226
|
-
// Track Flow agent
|
|
227
|
-
manifest.backup.agents.flow.push(agent.name);
|
|
228
|
-
}
|
|
202
|
+
const { stats, manifest: itemManifest } = await attachItemsToDir(agents, agentsDir, 'agent');
|
|
203
|
+
|
|
204
|
+
// Update result
|
|
205
|
+
result.agentsAdded.push(...stats.added);
|
|
206
|
+
result.agentsOverridden.push(...stats.overridden);
|
|
207
|
+
result.conflicts.push(
|
|
208
|
+
...stats.conflicts.map((c) => ({ ...c, type: 'agent' as const }))
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
// Update manifest
|
|
212
|
+
manifest.backup.agents.user.push(...itemManifest.user);
|
|
213
|
+
manifest.backup.agents.flow.push(...itemManifest.flow);
|
|
229
214
|
}
|
|
230
215
|
|
|
231
216
|
/**
|
|
232
217
|
* Attach commands (override strategy)
|
|
218
|
+
* Uses shared attachItemsToDir function
|
|
233
219
|
*/
|
|
234
220
|
private async attachCommands(
|
|
235
221
|
projectPath: string,
|
|
@@ -238,40 +224,24 @@ export class AttachManager {
|
|
|
238
224
|
result: AttachResult,
|
|
239
225
|
manifest: BackupManifest
|
|
240
226
|
): Promise<void> {
|
|
241
|
-
// Use full path from target config
|
|
242
227
|
const commandsDir = path.join(projectPath, target.config.slashCommandsDir);
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
action: 'overridden',
|
|
256
|
-
message: `Command '${command.name}' overridden (will be restored on exit)`,
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
// Track in manifest
|
|
260
|
-
manifest.backup.commands.user.push(command.name);
|
|
261
|
-
} else {
|
|
262
|
-
result.commandsAdded.push(command.name);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// Write Flow command (override)
|
|
266
|
-
await fs.writeFile(commandPath, command.content);
|
|
267
|
-
|
|
268
|
-
// Track Flow command
|
|
269
|
-
manifest.backup.commands.flow.push(command.name);
|
|
270
|
-
}
|
|
228
|
+
const { stats, manifest: itemManifest } = await attachItemsToDir(commands, commandsDir, 'command');
|
|
229
|
+
|
|
230
|
+
// Update result
|
|
231
|
+
result.commandsAdded.push(...stats.added);
|
|
232
|
+
result.commandsOverridden.push(...stats.overridden);
|
|
233
|
+
result.conflicts.push(
|
|
234
|
+
...stats.conflicts.map((c) => ({ ...c, type: 'command' as const }))
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
// Update manifest
|
|
238
|
+
manifest.backup.commands.user.push(...itemManifest.user);
|
|
239
|
+
manifest.backup.commands.flow.push(...itemManifest.flow);
|
|
271
240
|
}
|
|
272
241
|
|
|
273
242
|
/**
|
|
274
243
|
* Attach rules (append strategy for AGENTS.md)
|
|
244
|
+
* Uses shared attachRulesFile function
|
|
275
245
|
*/
|
|
276
246
|
private async attachRules(
|
|
277
247
|
projectPath: string,
|
|
@@ -280,52 +250,20 @@ export class AttachManager {
|
|
|
280
250
|
result: AttachResult,
|
|
281
251
|
manifest: BackupManifest
|
|
282
252
|
): Promise<void> {
|
|
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
253
|
const rulesPath = target.config.rulesFile
|
|
287
254
|
? path.join(projectPath, target.config.rulesFile)
|
|
288
255
|
: path.join(projectPath, target.config.agentDir, 'AGENTS.md');
|
|
289
256
|
|
|
290
|
-
|
|
291
|
-
// User has AGENTS.md, append Flow rules
|
|
292
|
-
const userRules = await fs.readFile(rulesPath, 'utf-8');
|
|
293
|
-
|
|
294
|
-
// Check if already appended (avoid duplicates)
|
|
295
|
-
if (userRules.includes('<!-- Sylphx Flow Rules -->')) {
|
|
296
|
-
// Already appended, skip
|
|
297
|
-
return;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
const merged = `${userRules}
|
|
301
|
-
|
|
302
|
-
<!-- ========== Sylphx Flow Rules (Auto-injected) ========== -->
|
|
303
|
-
|
|
304
|
-
${rules}
|
|
305
|
-
|
|
306
|
-
<!-- ========== End of Sylphx Flow Rules ========== -->
|
|
307
|
-
`;
|
|
308
|
-
|
|
309
|
-
await fs.writeFile(rulesPath, merged);
|
|
257
|
+
const { originalSize, flowContentAdded } = await attachRulesFile(rulesPath, rules);
|
|
310
258
|
|
|
259
|
+
if (flowContentAdded) {
|
|
311
260
|
manifest.backup.rules = {
|
|
312
261
|
path: rulesPath,
|
|
313
|
-
originalSize
|
|
314
|
-
flowContentAdded: true,
|
|
315
|
-
};
|
|
316
|
-
} else {
|
|
317
|
-
// User doesn't have AGENTS.md, create new
|
|
318
|
-
await fs.mkdir(path.dirname(rulesPath), { recursive: true });
|
|
319
|
-
await fs.writeFile(rulesPath, rules);
|
|
320
|
-
|
|
321
|
-
manifest.backup.rules = {
|
|
322
|
-
path: rulesPath,
|
|
323
|
-
originalSize: 0,
|
|
262
|
+
originalSize,
|
|
324
263
|
flowContentAdded: true,
|
|
325
264
|
};
|
|
265
|
+
result.rulesAppended = true;
|
|
326
266
|
}
|
|
327
|
-
|
|
328
|
-
result.rulesAppended = true;
|
|
329
267
|
}
|
|
330
268
|
|
|
331
269
|
/**
|
|
@@ -10,7 +10,7 @@ import path from 'node:path';
|
|
|
10
10
|
|
|
11
11
|
export interface GlobalSettings {
|
|
12
12
|
version: string;
|
|
13
|
-
defaultTarget?: 'claude-code' | 'opencode' | '
|
|
13
|
+
defaultTarget?: 'claude-code' | 'opencode' | 'ask-every-time';
|
|
14
14
|
defaultAgent?: string; // Default agent to use (e.g., 'coder', 'writer', 'reviewer', 'orchestrator')
|
|
15
15
|
firstRun: boolean;
|
|
16
16
|
lastUpdated: string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Target Installation Service
|
|
3
|
-
* Auto-detects and installs AI CLI tools (Claude Code, OpenCode
|
|
3
|
+
* Auto-detects and installs AI CLI tools (Claude Code, OpenCode)
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { exec } from 'node:child_process';
|
|
@@ -14,7 +14,7 @@ import { detectPackageManager, type PackageManager } from '../utils/package-mana
|
|
|
14
14
|
const execAsync = promisify(exec);
|
|
15
15
|
|
|
16
16
|
export interface TargetInstallation {
|
|
17
|
-
id: 'claude-code' | 'opencode'
|
|
17
|
+
id: 'claude-code' | 'opencode';
|
|
18
18
|
name: string;
|
|
19
19
|
package: string;
|
|
20
20
|
checkCommand: string;
|
|
@@ -61,16 +61,6 @@ const TARGET_INSTALLATIONS: TargetInstallation[] = [
|
|
|
61
61
|
}
|
|
62
62
|
},
|
|
63
63
|
},
|
|
64
|
-
{
|
|
65
|
-
id: 'cursor',
|
|
66
|
-
name: 'Cursor',
|
|
67
|
-
package: 'cursor',
|
|
68
|
-
checkCommand: 'cursor --version',
|
|
69
|
-
installCommand: () => {
|
|
70
|
-
// Cursor is typically installed via installer, not npm
|
|
71
|
-
return 'Visit https://cursor.sh to download and install';
|
|
72
|
-
},
|
|
73
|
-
},
|
|
74
64
|
];
|
|
75
65
|
|
|
76
66
|
export class TargetInstaller {
|
|
@@ -160,13 +150,6 @@ export class TargetInstaller {
|
|
|
160
150
|
return false;
|
|
161
151
|
}
|
|
162
152
|
|
|
163
|
-
// Special handling for Cursor (not npm-installable)
|
|
164
|
-
if (targetId === 'cursor') {
|
|
165
|
-
console.log(chalk.yellow('\n⚠️ Cursor requires manual installation'));
|
|
166
|
-
console.log(chalk.cyan(' Visit https://cursor.sh to download and install\n'));
|
|
167
|
-
return false;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
153
|
// Confirm installation unless auto-confirm is enabled
|
|
171
154
|
if (!autoConfirm) {
|
|
172
155
|
try {
|
|
@@ -3,14 +3,19 @@ import fs from 'node:fs';
|
|
|
3
3
|
import fsPromises from 'node:fs/promises';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import chalk from 'chalk';
|
|
6
|
-
import { installToDirectory } from '../core/installers/file-installer.js';
|
|
7
6
|
import { createMCPInstaller } from '../core/installers/mcp-installer.js';
|
|
8
7
|
import type { AgentMetadata } from '../types/target-config.types.js';
|
|
9
8
|
import type { CommonOptions, MCPServerConfigUnion, SetupResult, Target } from '../types.js';
|
|
10
|
-
import { getAgentsDir
|
|
9
|
+
import { getAgentsDir } from '../utils/config/paths.js';
|
|
11
10
|
import { fileUtils, generateHelpText, pathUtils, yamlUtils } from '../utils/config/target-utils.js';
|
|
12
11
|
import { CLIError } from '../utils/error-handler.js';
|
|
13
12
|
import { sanitize } from '../utils/security/security.js';
|
|
13
|
+
import {
|
|
14
|
+
transformMCPConfig as transformMCP,
|
|
15
|
+
detectTargetConfig,
|
|
16
|
+
stripFrontMatter,
|
|
17
|
+
setupSlashCommandsTo,
|
|
18
|
+
} from './shared/index.js';
|
|
14
19
|
|
|
15
20
|
/**
|
|
16
21
|
* Claude Code target - composition approach with all original functionality
|
|
@@ -68,50 +73,10 @@ export const claudeCodeTarget: Target = {
|
|
|
68
73
|
|
|
69
74
|
/**
|
|
70
75
|
* Transform MCP server configuration for Claude Code
|
|
71
|
-
*
|
|
76
|
+
* Uses shared pure function for bidirectional conversion
|
|
72
77
|
*/
|
|
73
78
|
transformMCPConfig(config: MCPServerConfigUnion, _serverId?: string): Record<string, unknown> {
|
|
74
|
-
|
|
75
|
-
if (config.type === 'local') {
|
|
76
|
-
// Convert OpenCode 'local' array command to Claude Code format
|
|
77
|
-
const [command, ...args] = config.command;
|
|
78
|
-
return {
|
|
79
|
-
type: 'stdio',
|
|
80
|
-
command,
|
|
81
|
-
...(args && args.length > 0 && { args }),
|
|
82
|
-
...(config.environment && { env: config.environment }),
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Handle new stdio format (already optimized for Claude Code)
|
|
87
|
-
if (config.type === 'stdio') {
|
|
88
|
-
return {
|
|
89
|
-
type: 'stdio',
|
|
90
|
-
command: config.command,
|
|
91
|
-
...(config.args && config.args.length > 0 && { args: config.args }),
|
|
92
|
-
...(config.env && { env: config.env }),
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Handle legacy OpenCode 'remote' type
|
|
97
|
-
if (config.type === 'remote') {
|
|
98
|
-
return {
|
|
99
|
-
type: 'http',
|
|
100
|
-
url: config.url,
|
|
101
|
-
...(config.headers && { headers: config.headers }),
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Handle new http format (already optimized for Claude Code)
|
|
106
|
-
if (config.type === 'http') {
|
|
107
|
-
return {
|
|
108
|
-
type: 'http',
|
|
109
|
-
url: config.url,
|
|
110
|
-
...(config.headers && { headers: config.headers }),
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return config;
|
|
79
|
+
return transformMCP(config, 'claude-code');
|
|
115
80
|
},
|
|
116
81
|
|
|
117
82
|
getConfigPath: (cwd: string) =>
|
|
@@ -321,12 +286,7 @@ Please begin your response with a comprehensive summary of all the instructions
|
|
|
321
286
|
* Detect if this target is being used in the current environment
|
|
322
287
|
*/
|
|
323
288
|
detectFromEnvironment(): boolean {
|
|
324
|
-
|
|
325
|
-
const cwd = process.cwd();
|
|
326
|
-
return fs.existsSync(path.join(cwd, '.mcp.json'));
|
|
327
|
-
} catch {
|
|
328
|
-
return false;
|
|
329
|
-
}
|
|
289
|
+
return detectTargetConfig(process.cwd(), '.mcp.json');
|
|
330
290
|
},
|
|
331
291
|
|
|
332
292
|
/**
|
|
@@ -377,9 +337,7 @@ Please begin your response with a comprehensive summary of all the instructions
|
|
|
377
337
|
* Transform rules content for Claude Code
|
|
378
338
|
* Claude Code doesn't need front matter in rules files (CLAUDE.md)
|
|
379
339
|
*/
|
|
380
|
-
|
|
381
|
-
return yamlUtils.stripFrontMatter(content);
|
|
382
|
-
},
|
|
340
|
+
transformRulesContent: stripFrontMatter,
|
|
383
341
|
|
|
384
342
|
/**
|
|
385
343
|
* Setup hooks for Claude Code
|
|
@@ -524,23 +482,7 @@ Please begin your response with a comprehensive summary of all the instructions
|
|
|
524
482
|
if (!this.config.slashCommandsDir) {
|
|
525
483
|
return { count: 0 };
|
|
526
484
|
}
|
|
527
|
-
|
|
528
|
-
const slashCommandsDir = path.join(cwd, this.config.slashCommandsDir);
|
|
529
|
-
|
|
530
|
-
const results = await installToDirectory(
|
|
531
|
-
getSlashCommandsDir(),
|
|
532
|
-
slashCommandsDir,
|
|
533
|
-
async (content) => {
|
|
534
|
-
// Slash commands are plain markdown with front matter - no transformation needed
|
|
535
|
-
return content;
|
|
536
|
-
},
|
|
537
|
-
{
|
|
538
|
-
...options,
|
|
539
|
-
showProgress: false, // UI handled by init-command
|
|
540
|
-
}
|
|
541
|
-
);
|
|
542
|
-
|
|
543
|
-
return { count: results.length };
|
|
485
|
+
return setupSlashCommandsTo(path.join(cwd, this.config.slashCommandsDir), undefined, options);
|
|
544
486
|
},
|
|
545
487
|
};
|
|
546
488
|
|
package/src/targets/opencode.ts
CHANGED
|
@@ -11,6 +11,12 @@ import { getAgentsDir, getOutputStylesDir, getSlashCommandsDir } from '../utils/
|
|
|
11
11
|
import { fileUtils, generateHelpText, yamlUtils } from '../utils/config/target-utils.js';
|
|
12
12
|
import { CLIError } from '../utils/error-handler.js';
|
|
13
13
|
import { secretUtils } from '../utils/security/secret-utils.js';
|
|
14
|
+
import {
|
|
15
|
+
transformMCPConfig as transformMCP,
|
|
16
|
+
detectTargetConfig,
|
|
17
|
+
stripFrontMatter,
|
|
18
|
+
setupSlashCommandsTo,
|
|
19
|
+
} from './shared/index.js';
|
|
14
20
|
|
|
15
21
|
/**
|
|
16
22
|
* OpenCode target - composition approach with all original functionality
|
|
@@ -80,44 +86,10 @@ export const opencodeTarget: Target = {
|
|
|
80
86
|
|
|
81
87
|
/**
|
|
82
88
|
* Transform MCP server configuration for OpenCode
|
|
83
|
-
*
|
|
89
|
+
* Uses shared pure function for bidirectional conversion
|
|
84
90
|
*/
|
|
85
91
|
transformMCPConfig(config: MCPServerConfigUnion, _serverId?: string): Record<string, unknown> {
|
|
86
|
-
|
|
87
|
-
if (config.type === 'stdio') {
|
|
88
|
-
// Convert Claude Code format to OpenCode format
|
|
89
|
-
const openCodeConfig: Record<string, unknown> = {
|
|
90
|
-
type: 'local',
|
|
91
|
-
command: [config.command],
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
if (config.args && config.args.length > 0) {
|
|
95
|
-
openCodeConfig.command.push(...config.args);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (config.env) {
|
|
99
|
-
openCodeConfig.environment = config.env;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return openCodeConfig;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Handle new Claude Code http format
|
|
106
|
-
if (config.type === 'http') {
|
|
107
|
-
// Claude Code http format is compatible with OpenCode remote format
|
|
108
|
-
return {
|
|
109
|
-
type: 'remote',
|
|
110
|
-
url: config.url,
|
|
111
|
-
...(config.headers && { headers: config.headers }),
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Handle legacy OpenCode formats (pass through)
|
|
116
|
-
if (config.type === 'local' || config.type === 'remote') {
|
|
117
|
-
return config;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
return config;
|
|
92
|
+
return transformMCP(config, 'opencode');
|
|
121
93
|
},
|
|
122
94
|
|
|
123
95
|
getConfigPath: (cwd: string) =>
|
|
@@ -217,21 +189,14 @@ export const opencodeTarget: Target = {
|
|
|
217
189
|
* Detect if this target is being used in the current environment
|
|
218
190
|
*/
|
|
219
191
|
detectFromEnvironment(): boolean {
|
|
220
|
-
|
|
221
|
-
const cwd = process.cwd();
|
|
222
|
-
return fs.existsSync(path.join(cwd, 'opencode.jsonc'));
|
|
223
|
-
} catch {
|
|
224
|
-
return false;
|
|
225
|
-
}
|
|
192
|
+
return detectTargetConfig(process.cwd(), 'opencode.jsonc');
|
|
226
193
|
},
|
|
227
194
|
|
|
228
195
|
/**
|
|
229
196
|
* Transform rules content for OpenCode
|
|
230
197
|
* OpenCode doesn't need front matter in rules files (AGENTS.md)
|
|
231
198
|
*/
|
|
232
|
-
|
|
233
|
-
return yamlUtils.stripFrontMatter(content);
|
|
234
|
-
},
|
|
199
|
+
transformRulesContent: stripFrontMatter,
|
|
235
200
|
|
|
236
201
|
/**
|
|
237
202
|
* Setup agents for OpenCode
|
|
@@ -380,23 +345,7 @@ export const opencodeTarget: Target = {
|
|
|
380
345
|
if (!this.config.slashCommandsDir) {
|
|
381
346
|
return { count: 0 };
|
|
382
347
|
}
|
|
383
|
-
|
|
384
|
-
const slashCommandsDir = path.join(cwd, this.config.slashCommandsDir);
|
|
385
|
-
|
|
386
|
-
const results = await installToDirectory(
|
|
387
|
-
getSlashCommandsDir(),
|
|
388
|
-
slashCommandsDir,
|
|
389
|
-
async (content) => {
|
|
390
|
-
// Slash commands are plain markdown with front matter - no transformation needed
|
|
391
|
-
return content;
|
|
392
|
-
},
|
|
393
|
-
{
|
|
394
|
-
...options,
|
|
395
|
-
showProgress: false, // UI handled by init-command
|
|
396
|
-
}
|
|
397
|
-
);
|
|
398
|
-
|
|
399
|
-
return { count: results.length };
|
|
348
|
+
return setupSlashCommandsTo(path.join(cwd, this.config.slashCommandsDir), undefined, options);
|
|
400
349
|
},
|
|
401
350
|
|
|
402
351
|
/**
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure functions for MCP configuration transformations
|
|
3
|
+
* Bidirectional conversion between Claude Code and OpenCode formats
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { MCPServerConfigUnion } from '../../types.js';
|
|
7
|
+
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// Types
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
export type MCPFormat = 'claude-code' | 'opencode';
|
|
13
|
+
|
|
14
|
+
export interface StdioConfig {
|
|
15
|
+
type: 'stdio';
|
|
16
|
+
command: string;
|
|
17
|
+
args?: string[];
|
|
18
|
+
env?: Record<string, string>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface HttpConfig {
|
|
22
|
+
type: 'http';
|
|
23
|
+
url: string;
|
|
24
|
+
headers?: Record<string, string>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface LocalConfig {
|
|
28
|
+
type: 'local';
|
|
29
|
+
command: string[];
|
|
30
|
+
environment?: Record<string, string>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface RemoteConfig {
|
|
34
|
+
type: 'remote';
|
|
35
|
+
url: string;
|
|
36
|
+
headers?: Record<string, string>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ============================================================================
|
|
40
|
+
// Pure Transform Functions
|
|
41
|
+
// ============================================================================
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Convert stdio format to local format (Claude Code → OpenCode)
|
|
45
|
+
*/
|
|
46
|
+
export const stdioToLocal = (config: StdioConfig): LocalConfig => ({
|
|
47
|
+
type: 'local',
|
|
48
|
+
command: config.args ? [config.command, ...config.args] : [config.command],
|
|
49
|
+
...(config.env && { environment: config.env }),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Convert local format to stdio format (OpenCode → Claude Code)
|
|
54
|
+
*/
|
|
55
|
+
export const localToStdio = (config: LocalConfig): StdioConfig => {
|
|
56
|
+
const [command, ...args] = config.command;
|
|
57
|
+
return {
|
|
58
|
+
type: 'stdio',
|
|
59
|
+
command,
|
|
60
|
+
...(args.length > 0 && { args }),
|
|
61
|
+
...(config.environment && { env: config.environment }),
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Convert http format to remote format (Claude Code → OpenCode)
|
|
67
|
+
*/
|
|
68
|
+
export const httpToRemote = (config: HttpConfig): RemoteConfig => ({
|
|
69
|
+
type: 'remote',
|
|
70
|
+
url: config.url,
|
|
71
|
+
...(config.headers && { headers: config.headers }),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Convert remote format to http format (OpenCode → Claude Code)
|
|
76
|
+
*/
|
|
77
|
+
export const remoteToHttp = (config: RemoteConfig): HttpConfig => ({
|
|
78
|
+
type: 'http',
|
|
79
|
+
url: config.url,
|
|
80
|
+
...(config.headers && { headers: config.headers }),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Normalize stdio config (ensure consistent structure)
|
|
85
|
+
*/
|
|
86
|
+
export const normalizeStdio = (config: StdioConfig): StdioConfig => ({
|
|
87
|
+
type: 'stdio',
|
|
88
|
+
command: config.command,
|
|
89
|
+
...(config.args && config.args.length > 0 && { args: config.args }),
|
|
90
|
+
...(config.env && { env: config.env }),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Normalize http config (ensure consistent structure)
|
|
95
|
+
*/
|
|
96
|
+
export const normalizeHttp = (config: HttpConfig): HttpConfig => ({
|
|
97
|
+
type: 'http',
|
|
98
|
+
url: config.url,
|
|
99
|
+
...(config.headers && { headers: config.headers }),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// ============================================================================
|
|
103
|
+
// Main Transform Function
|
|
104
|
+
// ============================================================================
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Transform MCP config to target format
|
|
108
|
+
* Pure function - no side effects
|
|
109
|
+
*/
|
|
110
|
+
export const transformMCPConfig = (
|
|
111
|
+
config: MCPServerConfigUnion,
|
|
112
|
+
targetFormat: MCPFormat
|
|
113
|
+
): Record<string, unknown> => {
|
|
114
|
+
// Claude Code format (stdio/http)
|
|
115
|
+
if (targetFormat === 'claude-code') {
|
|
116
|
+
if (config.type === 'local') return localToStdio(config as LocalConfig);
|
|
117
|
+
if (config.type === 'remote') return remoteToHttp(config as RemoteConfig);
|
|
118
|
+
if (config.type === 'stdio') return normalizeStdio(config as StdioConfig);
|
|
119
|
+
if (config.type === 'http') return normalizeHttp(config as HttpConfig);
|
|
120
|
+
return config;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// OpenCode format (local/remote)
|
|
124
|
+
if (targetFormat === 'opencode') {
|
|
125
|
+
if (config.type === 'stdio') return stdioToLocal(config as StdioConfig);
|
|
126
|
+
if (config.type === 'http') return httpToRemote(config as HttpConfig);
|
|
127
|
+
if (config.type === 'local' || config.type === 'remote') return config;
|
|
128
|
+
return config;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return config;
|
|
132
|
+
};
|