@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 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.4",
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
+ };
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Settings utilities
3
+ * Shared handlers for settings UI patterns
4
+ */
5
+
6
+ export * from './checkbox-config.js';
@@ -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
- // Get current enabled agents
127
- const currentEnabled = Object.keys(currentAgents).filter((key) => currentAgents[key].enabled);
128
-
129
- const { selectedAgents } = await inquirer.prompt([
130
- {
131
- type: 'checkbox',
132
- name: 'selectedAgents',
133
- message: 'Select agents to enable:',
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
- // Select default agent
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: selectedAgents.map((key: string) => ({
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 = currentAgents;
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
- // Get current enabled rules
193
- const currentEnabled = Object.keys(currentRules).filter((key) => currentRules[key].enabled);
194
-
195
- const { selectedRules } = await inquirer.prompt([
196
- {
197
- type: 'checkbox',
198
- name: 'selectedRules',
199
- message: 'Select rules to enable:',
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
- // Update rules
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 = currentRules;
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
- // Get current enabled styles
239
- const currentEnabled = Object.keys(currentStyles).filter((key) => currentStyles[key].enabled);
240
-
241
- const { selectedStyles } = await inquirer.prompt([
242
- {
243
- type: 'checkbox',
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
- // Update styles
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 = currentStyles;
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 currentServers = mcpConfig.servers || {};
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
- // Get current enabled servers
289
- const currentEnabled = Object.keys(currentServers).filter((key) => currentServers[key].enabled);
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: Object.entries(availableServers).map(([key, info]) => {
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
- info.requiresEnv.length > 0
299
- ? chalk.dim(` (requires ${info.requiresEnv.join(', ')})`)
300
- : '';
228
+ requiredEnvVars.length > 0 ? chalk.dim(` (requires ${requiredEnvVars.join(', ')})`) : '';
301
229
  return {
302
- name: `${info.name}${requiresText}`,
303
- value: key,
304
- checked: currentEnabled.includes(key),
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
- for (const key of Object.keys(availableServers)) {
312
- if (selectedServers.includes(key)) {
313
- if (currentServers[key]) {
314
- currentServers[key].enabled = true;
315
- } else {
316
- currentServers[key] = { enabled: true, env: {} };
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 serverKey of selectedServers) {
325
- const serverInfo = availableServers[serverKey as keyof typeof availableServers];
326
- if (serverInfo.requiresEnv.length > 0) {
327
- const server = currentServers[serverKey];
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 serverInfo.requiresEnv) {
330
- const hasKey = server.env?.[envKey];
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 ${serverInfo.name}?`
338
- : `Configure ${envKey} for ${serverInfo.name}?`,
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
- if (!server.env) {
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 = currentServers;
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') {
@@ -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
+ }