@sylphx/flow 1.8.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts CHANGED
@@ -16,7 +16,9 @@ import {
16
16
  doctorCommand,
17
17
  upgradeCommand,
18
18
  } from './commands/flow-command.js';
19
- import { executeFlow } from './commands/flow/execute.js';
19
+ import { settingsCommand } from './commands/settings-command.js';
20
+ import { executeFlow } from './commands/flow/execute-v2.js';
21
+ import { UserCancelledError } from './utils/errors.js';
20
22
 
21
23
  // Read version from package.json
22
24
  const __filename = fileURLToPath(import.meta.url);
@@ -52,36 +54,13 @@ export function createCLI(): Command {
52
54
  // This allows `sylphx-flow "prompt"` instead of requiring `sylphx-flow flow "prompt"`
53
55
  program
54
56
  .argument('[prompt]', 'Prompt to execute with agent (optional, supports @file.txt for file input)')
55
- .option('--init-only', 'Only initialize, do not run')
56
- .option('--run-only', 'Only run, skip initialization')
57
- .option('--sync', 'Synchronize with Flow templates (delete and re-install template files)')
58
- .option('--upgrade', 'Upgrade Sylphx Flow to latest version')
59
- .option('--upgrade-target', 'Upgrade target platform (Claude Code/OpenCode)')
60
- .option('--quick', 'Quick mode: use saved defaults and skip all prompts')
61
- .option('--select-provider', 'Prompt to select provider each run')
62
- .option('--select-agent', 'Prompt to select agent each run')
63
- .option('--use-defaults', 'Skip prompts, use saved defaults')
64
- .option('--provider <provider>', 'Override provider for this run (anthropic|z.ai|kimi)')
65
- .option('--target <type>', 'Target platform (opencode, claude-code, auto-detect)')
66
- .option('--verbose', 'Show detailed output')
67
- .option('--dry-run', 'Show what would be done without making changes')
68
- .option('--no-mcp', 'Skip MCP installation')
69
- .option('--no-agents', 'Skip agents installation')
70
- .option('--no-rules', 'Skip rules installation')
71
- .option('--no-output-styles', 'Skip output styles installation')
72
- .option('--no-slash-commands', 'Skip slash commands installation')
73
- .option('--no-hooks', 'Skip hooks setup')
74
57
  .option('--agent <name>', 'Agent to use (default: coder)', 'coder')
75
58
  .option('--agent-file <path>', 'Load agent from specific file')
59
+ .option('--verbose', 'Show detailed output')
60
+ .option('--dry-run', 'Show what would be done without making changes')
76
61
  .option('-p, --print', 'Headless print mode (output only, no interactive)')
77
62
  .option('-c, --continue', 'Continue previous conversation (requires print mode)')
78
-
79
- // Loop mode options
80
- .option('--loop [seconds]', 'Loop mode: wait N seconds between runs (default: 0 = immediate)', (value) => {
81
- // If no value provided, default to 0 (no wait time)
82
- return value ? parseInt(value) : 0;
83
- })
84
- .option('--max-runs <count>', 'Maximum iterations before stopping (default: infinite)', parseInt)
63
+ .option('--merge', 'Merge Flow settings with existing settings (default: replace all)')
85
64
 
86
65
  .action(async (prompt, options) => {
87
66
  await executeFlow(prompt, options);
@@ -94,6 +73,7 @@ export function createCLI(): Command {
94
73
  program.addCommand(doctorCommand);
95
74
  program.addCommand(upgradeCommand);
96
75
  program.addCommand(hookCommand);
76
+ program.addCommand(settingsCommand);
97
77
 
98
78
  return program;
99
79
  }
@@ -168,6 +148,12 @@ function setupGlobalErrorHandling(): void {
168
148
  */
169
149
  function handleCommandError(error: unknown): void {
170
150
  if (error instanceof Error) {
151
+ // Handle user cancellation gracefully
152
+ if (error instanceof UserCancelledError) {
153
+ console.log('\n⚠️ Operation cancelled by user\n');
154
+ process.exit(0);
155
+ }
156
+
171
157
  // Handle Commander.js specific errors
172
158
  if (error.name === 'CommanderError') {
173
159
  const commanderError = error as any;
@@ -0,0 +1,220 @@
1
+ /**
2
+ * First Run Setup
3
+ * Quick configuration wizard for new users
4
+ */
5
+
6
+ import chalk from 'chalk';
7
+ import inquirer from 'inquirer';
8
+ import { GlobalConfigService } from './global-config.js';
9
+ import { UserCancelledError } from '../utils/errors.js';
10
+
11
+ export interface QuickSetupResult {
12
+ target: 'claude-code' | 'opencode';
13
+ provider?: 'default' | 'kimi' | 'zai' | 'ask-every-time';
14
+ mcpServers: string[];
15
+ apiKeys: Record<string, Record<string, string>>;
16
+ }
17
+
18
+ export class FirstRunSetup {
19
+ private configService: GlobalConfigService;
20
+
21
+ constructor() {
22
+ this.configService = new GlobalConfigService();
23
+ }
24
+
25
+ /**
26
+ * Run setup wizard
27
+ */
28
+ async run(): Promise<QuickSetupResult> {
29
+ try {
30
+ console.log(chalk.cyan.bold('\n╭─────────────────────────────────────────────────╮'));
31
+ console.log(chalk.cyan.bold('│ │'));
32
+ console.log(chalk.cyan.bold('│ Welcome to Sylphx Flow! │'));
33
+ console.log(chalk.cyan.bold('│ Let\'s configure your environment │'));
34
+ console.log(chalk.cyan.bold('│ │'));
35
+ console.log(chalk.cyan.bold('╰─────────────────────────────────────────────────╯\n'));
36
+
37
+ // Step 1: Select target platform
38
+ console.log(chalk.cyan('🔧 Setup (1/3) - Target Platform\n'));
39
+ const { target } = await inquirer.prompt([
40
+ {
41
+ type: 'list',
42
+ name: 'target',
43
+ message: 'Select your preferred platform:',
44
+ choices: [
45
+ { name: 'Claude Code', value: 'claude-code' },
46
+ { name: 'OpenCode', value: 'opencode' },
47
+ ],
48
+ default: 'claude-code',
49
+ },
50
+ ]);
51
+
52
+ let provider: string | undefined = 'ask-every-time';
53
+ const apiKeys: Record<string, Record<string, string>> = {};
54
+
55
+ // Step 2: Provider setup (Claude Code only)
56
+ if (target === 'claude-code') {
57
+ console.log(chalk.cyan('\n🔧 Setup (2/3) - Provider\n'));
58
+ const { selectedProvider } = await inquirer.prompt([
59
+ {
60
+ type: 'list',
61
+ name: 'selectedProvider',
62
+ message: 'Select your preferred provider:',
63
+ choices: [
64
+ { name: 'Ask me every time', value: 'ask-every-time' },
65
+ { name: 'Default (Claude Code built-in)', value: 'default' },
66
+ { name: 'Kimi (requires API key)', value: 'kimi' },
67
+ { name: 'Z.ai (requires API key)', value: 'zai' },
68
+ ],
69
+ default: 'ask-every-time',
70
+ },
71
+ ]);
72
+
73
+ provider = selectedProvider;
74
+
75
+ // Configure API key if needed
76
+ if (provider === 'kimi' || provider === 'zai') {
77
+ const { apiKey } = await inquirer.prompt([
78
+ {
79
+ type: 'password',
80
+ name: 'apiKey',
81
+ message: provider === 'kimi' ? 'Enter Kimi API key:' : 'Enter Z.ai API key:',
82
+ mask: '*',
83
+ },
84
+ ]);
85
+
86
+ apiKeys[provider] = { apiKey };
87
+ }
88
+ }
89
+
90
+ // Step 3: MCP Servers
91
+ const stepNumber = target === 'claude-code' ? '3/3' : '2/2';
92
+ console.log(chalk.cyan('\n🔧 Setup (' + stepNumber + ') - MCP Servers\n'));
93
+
94
+ const { mcpServers } = await inquirer.prompt([
95
+ {
96
+ type: 'checkbox',
97
+ name: 'mcpServers',
98
+ message: 'Select MCP servers to enable:',
99
+ choices: [
100
+ { name: 'GitHub Code Search (grep.app)', value: 'grep', checked: true },
101
+ { name: 'Context7 Docs', value: 'context7', checked: true },
102
+ { name: 'Playwright Browser Control', value: 'playwright', checked: true },
103
+ { name: 'GitHub (requires GITHUB_TOKEN)', value: 'github' },
104
+ { name: 'Notion (requires NOTION_API_KEY)', value: 'notion' },
105
+ ],
106
+ },
107
+ ]);
108
+
109
+ // Configure MCP API keys
110
+ const mcpServerRequirements: Record<string, string[]> = {
111
+ github: ['GITHUB_TOKEN'],
112
+ notion: ['NOTION_API_KEY'],
113
+ };
114
+
115
+ for (const serverKey of mcpServers) {
116
+ const requirements = mcpServerRequirements[serverKey];
117
+ if (requirements) {
118
+ const configMessage = 'Configure ' + requirements[0] + ' for ' + serverKey + '?';
119
+ const { shouldConfigure } = await inquirer.prompt([
120
+ {
121
+ type: 'confirm',
122
+ name: 'shouldConfigure',
123
+ message: configMessage,
124
+ default: true,
125
+ },
126
+ ]);
127
+
128
+ if (shouldConfigure) {
129
+ const questions = requirements.map((key) => {
130
+ return {
131
+ type: 'password' as const,
132
+ name: key,
133
+ message: 'Enter ' + key + ':',
134
+ mask: '*',
135
+ };
136
+ });
137
+ const answers = await inquirer.prompt(questions);
138
+
139
+ apiKeys[serverKey] = answers;
140
+ }
141
+ }
142
+ }
143
+
144
+ // Save configuration
145
+ await this.saveConfiguration({
146
+ target,
147
+ provider,
148
+ mcpServers,
149
+ apiKeys,
150
+ });
151
+
152
+ console.log(chalk.green('\n✓ Configuration saved to ~/.sylphx-flow/\n'));
153
+
154
+ return {
155
+ target,
156
+ provider: provider as any,
157
+ mcpServers,
158
+ apiKeys,
159
+ };
160
+ } catch (error: any) {
161
+ // Handle user cancellation (Ctrl+C)
162
+ if (error.name === 'ExitPromptError' || error.message?.includes('force closed')) {
163
+ throw new UserCancelledError('Setup cancelled by user');
164
+ }
165
+ throw error;
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Save configuration to global config files
171
+ */
172
+ private async saveConfiguration(result: QuickSetupResult): Promise<void> {
173
+ // Save global settings
174
+ await this.configService.saveSettings({
175
+ version: '1.0.0',
176
+ defaultTarget: result.target,
177
+ firstRun: false,
178
+ lastUpdated: new Date().toISOString(),
179
+ });
180
+
181
+ // Save provider config (Claude Code)
182
+ if (result.target === 'claude-code' && result.provider) {
183
+ const providerConfig = await this.configService.loadProviderConfig();
184
+ providerConfig.claudeCode.defaultProvider = result.provider;
185
+
186
+ // Save API keys
187
+ if (result.provider === 'kimi' && result.apiKeys.kimi) {
188
+ providerConfig.claudeCode.providers.kimi = {
189
+ apiKey: result.apiKeys.kimi.apiKey,
190
+ enabled: true,
191
+ };
192
+ }
193
+ if (result.provider === 'zai' && result.apiKeys.zai) {
194
+ providerConfig.claudeCode.providers.zai = {
195
+ apiKey: result.apiKeys.zai.apiKey,
196
+ enabled: true,
197
+ };
198
+ }
199
+
200
+ await this.configService.saveProviderConfig(providerConfig);
201
+ }
202
+
203
+ // Save MCP config
204
+ const mcpConfig = await this.configService.loadMCPConfig();
205
+ for (const serverKey of result.mcpServers) {
206
+ mcpConfig.servers[serverKey] = {
207
+ enabled: true,
208
+ env: result.apiKeys[serverKey] || {},
209
+ };
210
+ }
211
+ await this.configService.saveMCPConfig(mcpConfig);
212
+ }
213
+
214
+ /**
215
+ * Check if we should run quick setup
216
+ */
217
+ async shouldRun(): Promise<boolean> {
218
+ return await this.configService.isFirstRun();
219
+ }
220
+ }
@@ -0,0 +1,337 @@
1
+ /**
2
+ * Global Configuration Service
3
+ * Manages all Flow settings in ~/.sylphx-flow/
4
+ */
5
+
6
+ import fs from 'node:fs/promises';
7
+ import path from 'node:path';
8
+ import os from 'node:os';
9
+ import { existsSync } from 'node:fs';
10
+
11
+ export interface GlobalSettings {
12
+ version: string;
13
+ defaultTarget?: 'claude-code' | 'opencode';
14
+ defaultAgent?: string; // Default agent to use (e.g., 'coder', 'writer', 'reviewer', 'orchestrator')
15
+ firstRun: boolean;
16
+ lastUpdated: string;
17
+ }
18
+
19
+ export interface AgentConfig {
20
+ enabled: boolean;
21
+ }
22
+
23
+ export interface RuleConfig {
24
+ enabled: boolean;
25
+ }
26
+
27
+ export interface OutputStyleConfig {
28
+ enabled: boolean;
29
+ }
30
+
31
+ export interface FlowConfig {
32
+ version: string;
33
+ agents: Record<string, AgentConfig>; // e.g., { coder: { enabled: true }, writer: { enabled: false } }
34
+ rules: Record<string, RuleConfig>; // e.g., { core: { enabled: true }, workspace: { enabled: true } }
35
+ outputStyles: Record<string, OutputStyleConfig>; // e.g., { silent: { enabled: true } }
36
+ }
37
+
38
+ export interface ProviderConfig {
39
+ claudeCode: {
40
+ defaultProvider: 'default' | 'kimi' | 'zai' | 'ask-every-time';
41
+ providers: {
42
+ kimi?: {
43
+ apiKey?: string;
44
+ enabled: boolean;
45
+ };
46
+ zai?: {
47
+ apiKey?: string;
48
+ enabled: boolean;
49
+ };
50
+ };
51
+ };
52
+ }
53
+
54
+ export interface MCPServerConfig {
55
+ enabled: boolean;
56
+ command?: string;
57
+ args?: string[];
58
+ env?: Record<string, string>;
59
+ }
60
+
61
+ export interface MCPConfig {
62
+ version: string;
63
+ servers: Record<string, MCPServerConfig>;
64
+ }
65
+
66
+ export class GlobalConfigService {
67
+ private flowHomeDir: string;
68
+
69
+ constructor() {
70
+ this.flowHomeDir = path.join(os.homedir(), '.sylphx-flow');
71
+ }
72
+
73
+ /**
74
+ * Get Flow home directory
75
+ */
76
+ getFlowHomeDir(): string {
77
+ return this.flowHomeDir;
78
+ }
79
+
80
+ /**
81
+ * Initialize Flow home directory structure
82
+ */
83
+ async initialize(): Promise<void> {
84
+ const dirs = [
85
+ this.flowHomeDir,
86
+ path.join(this.flowHomeDir, 'sessions'),
87
+ path.join(this.flowHomeDir, 'backups'),
88
+ path.join(this.flowHomeDir, 'secrets'),
89
+ ];
90
+
91
+ for (const dir of dirs) {
92
+ await fs.mkdir(dir, { recursive: true });
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Get global settings file path
98
+ */
99
+ private getSettingsPath(): string {
100
+ return path.join(this.flowHomeDir, 'settings.json');
101
+ }
102
+
103
+ /**
104
+ * Get provider config file path
105
+ */
106
+ private getProviderConfigPath(): string {
107
+ return path.join(this.flowHomeDir, 'provider-config.json');
108
+ }
109
+
110
+ /**
111
+ * Get MCP config file path
112
+ */
113
+ private getMCPConfigPath(): string {
114
+ return path.join(this.flowHomeDir, 'mcp-config.json');
115
+ }
116
+
117
+ /**
118
+ * Get Flow config file path
119
+ */
120
+ private getFlowConfigPath(): string {
121
+ return path.join(this.flowHomeDir, 'flow-config.json');
122
+ }
123
+
124
+ /**
125
+ * Check if this is first run
126
+ */
127
+ async isFirstRun(): Promise<boolean> {
128
+ const settingsPath = this.getSettingsPath();
129
+ if (!existsSync(settingsPath)) {
130
+ return true;
131
+ }
132
+
133
+ try {
134
+ const settings = await this.loadSettings();
135
+ return settings.firstRun !== false;
136
+ } catch {
137
+ return true;
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Load global settings
143
+ */
144
+ async loadSettings(): Promise<GlobalSettings> {
145
+ const settingsPath = this.getSettingsPath();
146
+
147
+ if (!existsSync(settingsPath)) {
148
+ return {
149
+ version: '1.0.0',
150
+ firstRun: true,
151
+ lastUpdated: new Date().toISOString(),
152
+ };
153
+ }
154
+
155
+ const data = await fs.readFile(settingsPath, 'utf-8');
156
+ return JSON.parse(data);
157
+ }
158
+
159
+ /**
160
+ * Save global settings
161
+ */
162
+ async saveSettings(settings: GlobalSettings): Promise<void> {
163
+ await this.initialize();
164
+ const settingsPath = this.getSettingsPath();
165
+ settings.lastUpdated = new Date().toISOString();
166
+ await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2));
167
+ }
168
+
169
+ /**
170
+ * Load provider config
171
+ */
172
+ async loadProviderConfig(): Promise<ProviderConfig> {
173
+ const configPath = this.getProviderConfigPath();
174
+
175
+ if (!existsSync(configPath)) {
176
+ return {
177
+ claudeCode: {
178
+ defaultProvider: 'ask-every-time',
179
+ providers: {
180
+ kimi: { enabled: false },
181
+ zai: { enabled: false },
182
+ },
183
+ },
184
+ };
185
+ }
186
+
187
+ const data = await fs.readFile(configPath, 'utf-8');
188
+ return JSON.parse(data);
189
+ }
190
+
191
+ /**
192
+ * Save provider config
193
+ */
194
+ async saveProviderConfig(config: ProviderConfig): Promise<void> {
195
+ await this.initialize();
196
+ const configPath = this.getProviderConfigPath();
197
+ await fs.writeFile(configPath, JSON.stringify(config, null, 2));
198
+ }
199
+
200
+ /**
201
+ * Load MCP config
202
+ */
203
+ async loadMCPConfig(): Promise<MCPConfig> {
204
+ const configPath = this.getMCPConfigPath();
205
+
206
+ if (!existsSync(configPath)) {
207
+ return {
208
+ version: '1.0.0',
209
+ servers: {},
210
+ };
211
+ }
212
+
213
+ const data = await fs.readFile(configPath, 'utf-8');
214
+ return JSON.parse(data);
215
+ }
216
+
217
+ /**
218
+ * Save MCP config
219
+ */
220
+ async saveMCPConfig(config: MCPConfig): Promise<void> {
221
+ await this.initialize();
222
+ const configPath = this.getMCPConfigPath();
223
+ await fs.writeFile(configPath, JSON.stringify(config, null, 2));
224
+ }
225
+
226
+ /**
227
+ * Get enabled MCP servers
228
+ */
229
+ async getEnabledMCPServers(): Promise<Record<string, MCPServerConfig>> {
230
+ const config = await this.loadMCPConfig();
231
+ const enabled: Record<string, MCPServerConfig> = {};
232
+
233
+ for (const [name, serverConfig] of Object.entries(config.servers)) {
234
+ if (serverConfig.enabled) {
235
+ enabled[name] = serverConfig;
236
+ }
237
+ }
238
+
239
+ return enabled;
240
+ }
241
+
242
+ /**
243
+ * Load Flow config (agents, rules, output styles)
244
+ */
245
+ async loadFlowConfig(): Promise<FlowConfig> {
246
+ const configPath = this.getFlowConfigPath();
247
+
248
+ if (!existsSync(configPath)) {
249
+ // Default: all agents, all rules, all output styles enabled
250
+ return {
251
+ version: '1.0.0',
252
+ agents: {
253
+ coder: { enabled: true },
254
+ writer: { enabled: true },
255
+ reviewer: { enabled: true },
256
+ orchestrator: { enabled: true },
257
+ },
258
+ rules: {
259
+ core: { enabled: true },
260
+ 'code-standards': { enabled: true },
261
+ workspace: { enabled: true },
262
+ },
263
+ outputStyles: {
264
+ silent: { enabled: true },
265
+ },
266
+ };
267
+ }
268
+
269
+ const data = await fs.readFile(configPath, 'utf-8');
270
+ return JSON.parse(data);
271
+ }
272
+
273
+ /**
274
+ * Save Flow config
275
+ */
276
+ async saveFlowConfig(config: FlowConfig): Promise<void> {
277
+ await this.initialize();
278
+ const configPath = this.getFlowConfigPath();
279
+ await fs.writeFile(configPath, JSON.stringify(config, null, 2));
280
+ }
281
+
282
+ /**
283
+ * Get enabled agents
284
+ */
285
+ async getEnabledAgents(): Promise<string[]> {
286
+ const config = await this.loadFlowConfig();
287
+ return Object.entries(config.agents)
288
+ .filter(([_, agentConfig]) => agentConfig.enabled)
289
+ .map(([name]) => name);
290
+ }
291
+
292
+ /**
293
+ * Get enabled rules
294
+ */
295
+ async getEnabledRules(): Promise<string[]> {
296
+ const config = await this.loadFlowConfig();
297
+ return Object.entries(config.rules)
298
+ .filter(([_, ruleConfig]) => ruleConfig.enabled)
299
+ .map(([name]) => name);
300
+ }
301
+
302
+ /**
303
+ * Get enabled output styles
304
+ */
305
+ async getEnabledOutputStyles(): Promise<string[]> {
306
+ const config = await this.loadFlowConfig();
307
+ return Object.entries(config.outputStyles)
308
+ .filter(([_, styleConfig]) => styleConfig.enabled)
309
+ .map(([name]) => name);
310
+ }
311
+
312
+ /**
313
+ * Update default target
314
+ */
315
+ async setDefaultTarget(target: 'claude-code' | 'opencode'): Promise<void> {
316
+ const settings = await this.loadSettings();
317
+ settings.defaultTarget = target;
318
+ await this.saveSettings(settings);
319
+ }
320
+
321
+ /**
322
+ * Get default target
323
+ */
324
+ async getDefaultTarget(): Promise<'claude-code' | 'opencode' | undefined> {
325
+ const settings = await this.loadSettings();
326
+ return settings.defaultTarget;
327
+ }
328
+
329
+ /**
330
+ * Mark first run as complete
331
+ */
332
+ async markFirstRunComplete(): Promise<void> {
333
+ const settings = await this.loadSettings();
334
+ settings.firstRun = false;
335
+ await this.saveSettings(settings);
336
+ }
337
+ }