@sylphx/flow 2.1.4 ā 2.1.6
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 +31 -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 +76 -156
- package/src/config/servers.ts +67 -0
- package/src/core/attach/file-attacher.ts +170 -0
- package/src/core/attach/index.ts +5 -0
- package/src/core/attach-manager.ts +42 -101
- package/src/services/global-config.ts +24 -6
- package/src/services/target-installer.ts +2 -19
- package/src/targets/claude-code.ts +12 -70
- package/src/targets/opencode.ts +12 -63
- package/src/targets/shared/index.ts +7 -0
- package/src/targets/shared/mcp-transforms.ts +146 -0
- package/src/targets/shared/target-operations.ts +128 -0
- package/src/utils/target-selection.ts +1 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# @sylphx/flow
|
|
2
2
|
|
|
3
|
+
## 2.1.6 (2025-11-28)
|
|
4
|
+
|
|
5
|
+
### š Bug Fixes
|
|
6
|
+
|
|
7
|
+
- **settings:** respect saved MCP server enabled state ([8447cea](https://github.com/SylphxAI/flow/commit/8447cea1b2f46e49cfc1bd7e57e557307d072163))
|
|
8
|
+
- **mcp:** return default servers when no config exists ([bd6c588](https://github.com/SylphxAI/flow/commit/bd6c58819cdde8e31bd18cdc2f05c2c45e4f3d39))
|
|
9
|
+
|
|
10
|
+
### ā»ļø Refactoring
|
|
11
|
+
|
|
12
|
+
- **mcp:** implement SSOT for server configuration ([e0b5ee0](https://github.com/SylphxAI/flow/commit/e0b5ee01d4952e825d81005465147ce39963bbd0))
|
|
13
|
+
|
|
14
|
+
### š§ Chores
|
|
15
|
+
|
|
16
|
+
- format package.json (tabs to spaces) ([305096a](https://github.com/SylphxAI/flow/commit/305096a9e276a3626415d76b8f313e95dc6daeff))
|
|
17
|
+
|
|
18
|
+
## 2.1.5 (2025-11-28)
|
|
19
|
+
|
|
20
|
+
### š Bug Fixes
|
|
21
|
+
|
|
22
|
+
- **settings:** use MCP_SERVER_REGISTRY instead of hardcoded list ([79fb625](https://github.com/SylphxAI/flow/commit/79fb625c27f58f7f62902314d92c205fdc84a06e))
|
|
23
|
+
|
|
24
|
+
### ā»ļø Refactoring
|
|
25
|
+
|
|
26
|
+
- **settings:** extract checkbox configuration handler ([66303bb](https://github.com/SylphxAI/flow/commit/66303bb21a5281e5f358c69b8a6c143f3866fa76))
|
|
27
|
+
- **attach:** extract file attachment pure functions ([5723be3](https://github.com/SylphxAI/flow/commit/5723be3817804228014ceec8de27f267c990fbe8))
|
|
28
|
+
- **targets:** extract shared pure functions for MCP transforms ([0bba2cb](https://github.com/SylphxAI/flow/commit/0bba2cbc4a4233e0d63a78875346a2e9c341d803))
|
|
29
|
+
|
|
30
|
+
### š§ Chores
|
|
31
|
+
|
|
32
|
+
- remove dead cursor target references ([bf16f75](https://github.com/SylphxAI/flow/commit/bf16f759ec4705ddf0a763ea0ef6c778c91ccbbe))
|
|
33
|
+
|
|
3
34
|
## 2.1.4 (2025-11-28)
|
|
4
35
|
|
|
5
36
|
### ā»ļø Refactoring
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sylphx/flow",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.6",
|
|
4
4
|
"description": "One CLI to rule them all. Unified orchestration layer for Claude Code, OpenCode, Cursor and all AI development tools. Auto-detection, auto-installation, auto-upgrade.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic checkbox configuration handler
|
|
3
|
+
* Pure functions for settings UI patterns
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import inquirer from 'inquirer';
|
|
8
|
+
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Types
|
|
11
|
+
// ============================================================================
|
|
12
|
+
|
|
13
|
+
export interface ConfigItem {
|
|
14
|
+
enabled: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type ConfigMap = Record<string, ConfigItem>;
|
|
18
|
+
|
|
19
|
+
export interface CheckboxConfigOptions<T extends string> {
|
|
20
|
+
/** Section title (e.g., "Agents Configuration") */
|
|
21
|
+
title: string;
|
|
22
|
+
/** Icon for the section (e.g., "š¤") */
|
|
23
|
+
icon: string;
|
|
24
|
+
/** Prompt message (e.g., "Select agents to enable:") */
|
|
25
|
+
message: string;
|
|
26
|
+
/** Available items with display names */
|
|
27
|
+
available: Record<T, string>;
|
|
28
|
+
/** Current config state */
|
|
29
|
+
current: ConfigMap;
|
|
30
|
+
/** Item type name for confirmation (e.g., "agents", "rules") */
|
|
31
|
+
itemType: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface CheckboxConfigResult<T extends string> {
|
|
35
|
+
selected: T[];
|
|
36
|
+
updated: ConfigMap;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ============================================================================
|
|
40
|
+
// Pure Functions
|
|
41
|
+
// ============================================================================
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get currently enabled keys from config
|
|
45
|
+
*/
|
|
46
|
+
export const getEnabledKeys = (config: ConfigMap): string[] =>
|
|
47
|
+
Object.keys(config).filter((key) => config[key]?.enabled);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Build checkbox choices from available items
|
|
51
|
+
*/
|
|
52
|
+
export const buildChoices = <T extends string>(
|
|
53
|
+
available: Record<T, string>,
|
|
54
|
+
enabledKeys: string[]
|
|
55
|
+
): Array<{ name: string; value: T; checked: boolean }> =>
|
|
56
|
+
Object.entries(available).map(([key, name]) => ({
|
|
57
|
+
name: name as string,
|
|
58
|
+
value: key as T,
|
|
59
|
+
checked: enabledKeys.includes(key),
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Update config based on selection
|
|
64
|
+
* Returns new config object (immutable)
|
|
65
|
+
*/
|
|
66
|
+
export const updateConfig = <T extends string>(
|
|
67
|
+
available: Record<T, string>,
|
|
68
|
+
selected: T[]
|
|
69
|
+
): ConfigMap => {
|
|
70
|
+
const updated: ConfigMap = {};
|
|
71
|
+
for (const key of Object.keys(available)) {
|
|
72
|
+
updated[key] = { enabled: selected.includes(key as T) };
|
|
73
|
+
}
|
|
74
|
+
return updated;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Print section header
|
|
79
|
+
*/
|
|
80
|
+
export const printHeader = (icon: string, title: string): void => {
|
|
81
|
+
console.log(chalk.cyan.bold(`\nāāā ${icon} ${title}\n`));
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Print confirmation message
|
|
86
|
+
*/
|
|
87
|
+
export const printConfirmation = (itemType: string, count: number): void => {
|
|
88
|
+
console.log(chalk.green(`\nā ${itemType} configuration saved`));
|
|
89
|
+
console.log(chalk.dim(` Enabled ${itemType.toLowerCase()}: ${count}`));
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// ============================================================================
|
|
93
|
+
// Main Handler
|
|
94
|
+
// ============================================================================
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Generic checkbox configuration handler
|
|
98
|
+
* Handles the common pattern of select ā update ā save
|
|
99
|
+
*/
|
|
100
|
+
export const handleCheckboxConfig = async <T extends string>(
|
|
101
|
+
options: CheckboxConfigOptions<T>
|
|
102
|
+
): Promise<CheckboxConfigResult<T>> => {
|
|
103
|
+
const { title, icon, message, available, current, itemType } = options;
|
|
104
|
+
|
|
105
|
+
// Print header
|
|
106
|
+
printHeader(icon, title);
|
|
107
|
+
|
|
108
|
+
// Get current enabled items
|
|
109
|
+
const enabledKeys = getEnabledKeys(current);
|
|
110
|
+
|
|
111
|
+
// Show checkbox prompt
|
|
112
|
+
const { selected } = await inquirer.prompt([
|
|
113
|
+
{
|
|
114
|
+
type: 'checkbox',
|
|
115
|
+
name: 'selected',
|
|
116
|
+
message,
|
|
117
|
+
choices: buildChoices(available, enabledKeys),
|
|
118
|
+
},
|
|
119
|
+
]);
|
|
120
|
+
|
|
121
|
+
// Update config
|
|
122
|
+
const updated = updateConfig(available, selected);
|
|
123
|
+
|
|
124
|
+
// Print confirmation
|
|
125
|
+
printConfirmation(itemType, selected.length);
|
|
126
|
+
|
|
127
|
+
return { selected, updated };
|
|
128
|
+
};
|
|
@@ -6,10 +6,17 @@
|
|
|
6
6
|
import chalk from 'chalk';
|
|
7
7
|
import { Command } from 'commander';
|
|
8
8
|
import inquirer from 'inquirer';
|
|
9
|
+
import {
|
|
10
|
+
computeEffectiveServers,
|
|
11
|
+
getRequiredEnvVars,
|
|
12
|
+
MCP_SERVER_REGISTRY,
|
|
13
|
+
type MCPServerID,
|
|
14
|
+
} from '../config/servers.js';
|
|
9
15
|
import { GlobalConfigService } from '../services/global-config.js';
|
|
10
16
|
import { TargetInstaller } from '../services/target-installer.js';
|
|
11
17
|
import { UserCancelledError } from '../utils/errors.js';
|
|
12
18
|
import { buildAvailableTargets, promptForDefaultTarget } from '../utils/target-selection.js';
|
|
19
|
+
import { handleCheckboxConfig } from './settings/index.js';
|
|
13
20
|
|
|
14
21
|
export const settingsCommand = new Command('settings')
|
|
15
22
|
.description('Configure Sylphx Flow settings')
|
|
@@ -106,16 +113,12 @@ async function openSection(section: string, configService: GlobalConfigService):
|
|
|
106
113
|
}
|
|
107
114
|
|
|
108
115
|
/**
|
|
109
|
-
* Configure Agents
|
|
116
|
+
* Configure Agents - uses shared checkbox handler + default selection
|
|
110
117
|
*/
|
|
111
118
|
async function configureAgents(configService: GlobalConfigService): Promise<void> {
|
|
112
|
-
console.log(chalk.cyan.bold('\nāāā š¤ Agent Configuration\n'));
|
|
113
|
-
|
|
114
119
|
const flowConfig = await configService.loadFlowConfig();
|
|
115
120
|
const settings = await configService.loadSettings();
|
|
116
|
-
const currentAgents = flowConfig.agents || {};
|
|
117
121
|
|
|
118
|
-
// Available agents
|
|
119
122
|
const availableAgents = {
|
|
120
123
|
coder: 'Coder - Write and modify code',
|
|
121
124
|
writer: 'Writer - Documentation and explanation',
|
|
@@ -123,38 +126,22 @@ async function configureAgents(configService: GlobalConfigService): Promise<void
|
|
|
123
126
|
orchestrator: 'Orchestrator - Task coordination',
|
|
124
127
|
};
|
|
125
128
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
choices: Object.entries(availableAgents).map(([key, name]) => ({
|
|
135
|
-
name,
|
|
136
|
-
value: key,
|
|
137
|
-
checked: currentEnabled.includes(key),
|
|
138
|
-
})),
|
|
139
|
-
},
|
|
140
|
-
]);
|
|
141
|
-
|
|
142
|
-
// Update agents
|
|
143
|
-
for (const key of Object.keys(availableAgents)) {
|
|
144
|
-
if (selectedAgents.includes(key)) {
|
|
145
|
-
currentAgents[key] = { enabled: true };
|
|
146
|
-
} else {
|
|
147
|
-
currentAgents[key] = { enabled: false };
|
|
148
|
-
}
|
|
149
|
-
}
|
|
129
|
+
const { selected, updated } = await handleCheckboxConfig({
|
|
130
|
+
title: 'Agent Configuration',
|
|
131
|
+
icon: 'š¤',
|
|
132
|
+
message: 'Select agents to enable:',
|
|
133
|
+
available: availableAgents,
|
|
134
|
+
current: flowConfig.agents || {},
|
|
135
|
+
itemType: 'Agents',
|
|
136
|
+
});
|
|
150
137
|
|
|
151
|
-
//
|
|
138
|
+
// Additional step: select default agent from enabled ones
|
|
152
139
|
const { defaultAgent } = await inquirer.prompt([
|
|
153
140
|
{
|
|
154
141
|
type: 'list',
|
|
155
142
|
name: 'defaultAgent',
|
|
156
143
|
message: 'Select default agent:',
|
|
157
|
-
choices:
|
|
144
|
+
choices: selected.map((key) => ({
|
|
158
145
|
name: availableAgents[key as keyof typeof availableAgents],
|
|
159
146
|
value: key,
|
|
160
147
|
})),
|
|
@@ -162,109 +149,57 @@ async function configureAgents(configService: GlobalConfigService): Promise<void
|
|
|
162
149
|
},
|
|
163
150
|
]);
|
|
164
151
|
|
|
165
|
-
flowConfig.agents =
|
|
152
|
+
flowConfig.agents = updated;
|
|
166
153
|
await configService.saveFlowConfig(flowConfig);
|
|
167
154
|
|
|
168
155
|
settings.defaultAgent = defaultAgent;
|
|
169
156
|
await configService.saveSettings(settings);
|
|
170
157
|
|
|
171
|
-
console.log(chalk.green(`\nā Agent configuration saved`));
|
|
172
|
-
console.log(chalk.dim(` Enabled agents: ${selectedAgents.length}`));
|
|
173
158
|
console.log(chalk.dim(` Default agent: ${defaultAgent}`));
|
|
174
159
|
}
|
|
175
160
|
|
|
176
161
|
/**
|
|
177
|
-
* Configure Rules
|
|
162
|
+
* Configure Rules - uses shared checkbox handler
|
|
178
163
|
*/
|
|
179
164
|
async function configureRules(configService: GlobalConfigService): Promise<void> {
|
|
180
|
-
console.log(chalk.cyan.bold('\nāāā š Rules Configuration\n'));
|
|
181
|
-
|
|
182
165
|
const flowConfig = await configService.loadFlowConfig();
|
|
183
|
-
const currentRules = flowConfig.rules || {};
|
|
184
|
-
|
|
185
|
-
// Available rules
|
|
186
|
-
const availableRules = {
|
|
187
|
-
core: 'Core - Identity, personality, execution',
|
|
188
|
-
'code-standards': 'Code Standards - Quality, patterns, anti-patterns',
|
|
189
|
-
workspace: 'Workspace - Documentation management',
|
|
190
|
-
};
|
|
191
166
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
{
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
choices: Object.entries(availableRules).map(([key, name]) => ({
|
|
201
|
-
name,
|
|
202
|
-
value: key,
|
|
203
|
-
checked: currentEnabled.includes(key),
|
|
204
|
-
})),
|
|
167
|
+
const { updated } = await handleCheckboxConfig({
|
|
168
|
+
title: 'Rules Configuration',
|
|
169
|
+
icon: 'š',
|
|
170
|
+
message: 'Select rules to enable:',
|
|
171
|
+
available: {
|
|
172
|
+
core: 'Core - Identity, personality, execution',
|
|
173
|
+
'code-standards': 'Code Standards - Quality, patterns, anti-patterns',
|
|
174
|
+
workspace: 'Workspace - Documentation management',
|
|
205
175
|
},
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
for (const key of Object.keys(availableRules)) {
|
|
210
|
-
if (selectedRules.includes(key)) {
|
|
211
|
-
currentRules[key] = { enabled: true };
|
|
212
|
-
} else {
|
|
213
|
-
currentRules[key] = { enabled: false };
|
|
214
|
-
}
|
|
215
|
-
}
|
|
176
|
+
current: flowConfig.rules || {},
|
|
177
|
+
itemType: 'Rules',
|
|
178
|
+
});
|
|
216
179
|
|
|
217
|
-
flowConfig.rules =
|
|
180
|
+
flowConfig.rules = updated;
|
|
218
181
|
await configService.saveFlowConfig(flowConfig);
|
|
219
|
-
|
|
220
|
-
console.log(chalk.green(`\nā Rules configuration saved`));
|
|
221
|
-
console.log(chalk.dim(` Enabled rules: ${selectedRules.length}`));
|
|
222
182
|
}
|
|
223
183
|
|
|
224
184
|
/**
|
|
225
|
-
* Configure Output Styles
|
|
185
|
+
* Configure Output Styles - uses shared checkbox handler
|
|
226
186
|
*/
|
|
227
187
|
async function configureOutputStyles(configService: GlobalConfigService): Promise<void> {
|
|
228
|
-
console.log(chalk.cyan.bold('\nāāā šØ Output Styles Configuration\n'));
|
|
229
|
-
|
|
230
188
|
const flowConfig = await configService.loadFlowConfig();
|
|
231
|
-
const currentStyles = flowConfig.outputStyles || {};
|
|
232
|
-
|
|
233
|
-
// Available output styles
|
|
234
|
-
const availableStyles = {
|
|
235
|
-
silent: 'Silent - Execution without narration',
|
|
236
|
-
};
|
|
237
189
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
{
|
|
243
|
-
|
|
244
|
-
name: 'selectedStyles',
|
|
245
|
-
message: 'Select output styles to enable:',
|
|
246
|
-
choices: Object.entries(availableStyles).map(([key, name]) => ({
|
|
247
|
-
name,
|
|
248
|
-
value: key,
|
|
249
|
-
checked: currentEnabled.includes(key),
|
|
250
|
-
})),
|
|
190
|
+
const { updated } = await handleCheckboxConfig({
|
|
191
|
+
title: 'Output Styles Configuration',
|
|
192
|
+
icon: 'šØ',
|
|
193
|
+
message: 'Select output styles to enable:',
|
|
194
|
+
available: {
|
|
195
|
+
silent: 'Silent - Execution without narration',
|
|
251
196
|
},
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
for (const key of Object.keys(availableStyles)) {
|
|
256
|
-
if (selectedStyles.includes(key)) {
|
|
257
|
-
currentStyles[key] = { enabled: true };
|
|
258
|
-
} else {
|
|
259
|
-
currentStyles[key] = { enabled: false };
|
|
260
|
-
}
|
|
261
|
-
}
|
|
197
|
+
current: flowConfig.outputStyles || {},
|
|
198
|
+
itemType: 'Output styles',
|
|
199
|
+
});
|
|
262
200
|
|
|
263
|
-
flowConfig.outputStyles =
|
|
201
|
+
flowConfig.outputStyles = updated;
|
|
264
202
|
await configService.saveFlowConfig(flowConfig);
|
|
265
|
-
|
|
266
|
-
console.log(chalk.green(`\nā Output styles configuration saved`));
|
|
267
|
-
console.log(chalk.dim(` Enabled styles: ${selectedStyles.length}`));
|
|
268
203
|
}
|
|
269
204
|
|
|
270
205
|
/**
|
|
@@ -274,68 +209,60 @@ async function configureMCP(configService: GlobalConfigService): Promise<void> {
|
|
|
274
209
|
console.log(chalk.cyan.bold('\nāāā š” MCP Server Configuration\n'));
|
|
275
210
|
|
|
276
211
|
const mcpConfig = await configService.loadMCPConfig();
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
// Available MCP servers (from MCP_SERVER_REGISTRY)
|
|
280
|
-
const availableServers = {
|
|
281
|
-
grep: { name: 'GitHub Code Search (grep.app)', requiresEnv: [] },
|
|
282
|
-
context7: { name: 'Context7 Docs', requiresEnv: [] },
|
|
283
|
-
playwright: { name: 'Playwright Browser Control', requiresEnv: [] },
|
|
284
|
-
github: { name: 'GitHub', requiresEnv: ['GITHUB_TOKEN'] },
|
|
285
|
-
notion: { name: 'Notion', requiresEnv: ['NOTION_API_KEY'] },
|
|
286
|
-
};
|
|
212
|
+
const savedServers = mcpConfig.servers || {};
|
|
287
213
|
|
|
288
|
-
//
|
|
289
|
-
const
|
|
214
|
+
// SSOT: compute effective state from saved config + defaults
|
|
215
|
+
const effectiveServers = computeEffectiveServers(savedServers);
|
|
216
|
+
const allServerIds = Object.keys(MCP_SERVER_REGISTRY) as MCPServerID[];
|
|
290
217
|
|
|
291
218
|
const { selectedServers } = await inquirer.prompt([
|
|
292
219
|
{
|
|
293
220
|
type: 'checkbox',
|
|
294
221
|
name: 'selectedServers',
|
|
295
222
|
message: 'Select MCP servers to enable:',
|
|
296
|
-
choices:
|
|
223
|
+
choices: allServerIds.map((id) => {
|
|
224
|
+
const server = MCP_SERVER_REGISTRY[id];
|
|
225
|
+
const effective = effectiveServers[id];
|
|
226
|
+
const requiredEnvVars = getRequiredEnvVars(id);
|
|
297
227
|
const requiresText =
|
|
298
|
-
|
|
299
|
-
? chalk.dim(` (requires ${info.requiresEnv.join(', ')})`)
|
|
300
|
-
: '';
|
|
228
|
+
requiredEnvVars.length > 0 ? chalk.dim(` (requires ${requiredEnvVars.join(', ')})`) : '';
|
|
301
229
|
return {
|
|
302
|
-
name: `${
|
|
303
|
-
value:
|
|
304
|
-
checked:
|
|
230
|
+
name: `${server.name} - ${server.description}${requiresText}`,
|
|
231
|
+
value: id,
|
|
232
|
+
checked: effective.enabled, // Use SSOT effective state
|
|
305
233
|
};
|
|
306
234
|
}),
|
|
307
235
|
},
|
|
308
236
|
]);
|
|
309
237
|
|
|
310
|
-
// Update servers
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
} else if (currentServers[key]) {
|
|
319
|
-
currentServers[key].enabled = false;
|
|
320
|
-
}
|
|
238
|
+
// Update servers - save ALL servers with explicit enabled state
|
|
239
|
+
const updatedServers: Record<string, { enabled: boolean; env: Record<string, string> }> = {};
|
|
240
|
+
for (const id of allServerIds) {
|
|
241
|
+
const effective = effectiveServers[id];
|
|
242
|
+
updatedServers[id] = {
|
|
243
|
+
enabled: selectedServers.includes(id),
|
|
244
|
+
env: effective.env, // Preserve existing env vars
|
|
245
|
+
};
|
|
321
246
|
}
|
|
322
247
|
|
|
323
248
|
// Ask for API keys for newly enabled servers
|
|
324
|
-
for (const
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
249
|
+
for (const serverId of selectedServers as MCPServerID[]) {
|
|
250
|
+
const serverDef = MCP_SERVER_REGISTRY[serverId];
|
|
251
|
+
const requiredEnvVars = getRequiredEnvVars(serverId);
|
|
252
|
+
|
|
253
|
+
if (requiredEnvVars.length > 0) {
|
|
254
|
+
const serverState = updatedServers[serverId];
|
|
328
255
|
|
|
329
|
-
for (const envKey of
|
|
330
|
-
const hasKey =
|
|
256
|
+
for (const envKey of requiredEnvVars) {
|
|
257
|
+
const hasKey = serverState.env?.[envKey];
|
|
331
258
|
|
|
332
259
|
const { shouldConfigure } = await inquirer.prompt([
|
|
333
260
|
{
|
|
334
261
|
type: 'confirm',
|
|
335
262
|
name: 'shouldConfigure',
|
|
336
263
|
message: hasKey
|
|
337
|
-
? `Update ${envKey} for ${
|
|
338
|
-
: `Configure ${envKey} for ${
|
|
264
|
+
? `Update ${envKey} for ${serverDef.name}?`
|
|
265
|
+
: `Configure ${envKey} for ${serverDef.name}?`,
|
|
339
266
|
default: !hasKey,
|
|
340
267
|
},
|
|
341
268
|
]);
|
|
@@ -350,16 +277,13 @@ async function configureMCP(configService: GlobalConfigService): Promise<void> {
|
|
|
350
277
|
},
|
|
351
278
|
]);
|
|
352
279
|
|
|
353
|
-
|
|
354
|
-
server.env = {};
|
|
355
|
-
}
|
|
356
|
-
server.env[envKey] = apiKey;
|
|
280
|
+
serverState.env[envKey] = apiKey;
|
|
357
281
|
}
|
|
358
282
|
}
|
|
359
283
|
}
|
|
360
284
|
}
|
|
361
285
|
|
|
362
|
-
mcpConfig.servers =
|
|
286
|
+
mcpConfig.servers = updatedServers;
|
|
363
287
|
await configService.saveMCPConfig(mcpConfig);
|
|
364
288
|
|
|
365
289
|
console.log(chalk.green(`\nā MCP configuration saved`));
|
|
@@ -449,11 +373,7 @@ async function configureTarget(configService: GlobalConfigService): Promise<void
|
|
|
449
373
|
|
|
450
374
|
const defaultTarget = await promptForDefaultTarget(installedTargets, settings.defaultTarget);
|
|
451
375
|
|
|
452
|
-
settings.defaultTarget = defaultTarget as
|
|
453
|
-
| 'claude-code'
|
|
454
|
-
| 'opencode'
|
|
455
|
-
| 'cursor'
|
|
456
|
-
| 'ask-every-time';
|
|
376
|
+
settings.defaultTarget = defaultTarget as 'claude-code' | 'opencode' | 'ask-every-time';
|
|
457
377
|
await configService.saveSettings(settings);
|
|
458
378
|
|
|
459
379
|
if (defaultTarget === 'ask-every-time') {
|
package/src/config/servers.ts
CHANGED
|
@@ -339,3 +339,70 @@ export function getServerDefinition(id: MCPServerID): MCPServerDefinition {
|
|
|
339
339
|
}
|
|
340
340
|
return server;
|
|
341
341
|
}
|
|
342
|
+
|
|
343
|
+
// ============================================================================
|
|
344
|
+
// SSOT: Effective Server Config
|
|
345
|
+
// ============================================================================
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Saved server state (from user config file)
|
|
349
|
+
*/
|
|
350
|
+
export interface SavedServerState {
|
|
351
|
+
enabled: boolean;
|
|
352
|
+
env?: Record<string, string>;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Effective server state (computed from saved + defaults)
|
|
357
|
+
*/
|
|
358
|
+
export interface EffectiveServerState {
|
|
359
|
+
id: MCPServerID;
|
|
360
|
+
enabled: boolean;
|
|
361
|
+
env: Record<string, string>;
|
|
362
|
+
/** Whether this server was in saved config or using default */
|
|
363
|
+
isDefault: boolean;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Compute effective server states from saved config
|
|
368
|
+
* This is the SSOT for determining which servers are enabled
|
|
369
|
+
*
|
|
370
|
+
* Logic:
|
|
371
|
+
* - If server is in savedConfig, use its enabled state
|
|
372
|
+
* - If server is NOT in savedConfig, use defaultInInit from registry
|
|
373
|
+
*
|
|
374
|
+
* @param savedServers - Servers from user's config file (may be empty/undefined)
|
|
375
|
+
* @returns Map of server ID to effective state
|
|
376
|
+
*/
|
|
377
|
+
export function computeEffectiveServers(
|
|
378
|
+
savedServers: Record<string, SavedServerState> | undefined
|
|
379
|
+
): Record<MCPServerID, EffectiveServerState> {
|
|
380
|
+
const result: Record<string, EffectiveServerState> = {};
|
|
381
|
+
const saved = savedServers || {};
|
|
382
|
+
|
|
383
|
+
for (const [id, def] of Object.entries(MCP_SERVER_REGISTRY)) {
|
|
384
|
+
const serverId = id as MCPServerID;
|
|
385
|
+
const savedState = saved[id];
|
|
386
|
+
const isInSaved = id in saved;
|
|
387
|
+
|
|
388
|
+
result[serverId] = {
|
|
389
|
+
id: serverId,
|
|
390
|
+
enabled: isInSaved ? (savedState?.enabled ?? false) : (def.defaultInInit ?? false),
|
|
391
|
+
env: savedState?.env || {},
|
|
392
|
+
isDefault: !isInSaved,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return result as Record<MCPServerID, EffectiveServerState>;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Get only enabled servers from effective config
|
|
401
|
+
*/
|
|
402
|
+
export function getEnabledServersFromEffective(
|
|
403
|
+
effective: Record<MCPServerID, EffectiveServerState>
|
|
404
|
+
): MCPServerID[] {
|
|
405
|
+
return Object.entries(effective)
|
|
406
|
+
.filter(([, state]) => state.enabled)
|
|
407
|
+
.map(([id]) => id as MCPServerID);
|
|
408
|
+
}
|