@sylphx/flow 2.1.2 → 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 (70) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +44 -0
  3. package/package.json +79 -73
  4. package/src/commands/flow/execute-v2.ts +39 -30
  5. package/src/commands/flow/index.ts +2 -4
  6. package/src/commands/flow/prompt.ts +5 -3
  7. package/src/commands/flow/types.ts +0 -9
  8. package/src/commands/flow-command.ts +20 -13
  9. package/src/commands/hook-command.ts +1 -3
  10. package/src/commands/settings-command.ts +36 -33
  11. package/src/config/ai-config.ts +60 -41
  12. package/src/core/agent-loader.ts +11 -6
  13. package/src/core/attach-manager.ts +92 -84
  14. package/src/core/backup-manager.ts +35 -29
  15. package/src/core/cleanup-handler.ts +11 -8
  16. package/src/core/error-handling.ts +23 -30
  17. package/src/core/flow-executor.ts +58 -76
  18. package/src/core/formatting/bytes.ts +2 -4
  19. package/src/core/functional/async.ts +5 -4
  20. package/src/core/functional/error-handler.ts +2 -2
  21. package/src/core/git-stash-manager.ts +21 -10
  22. package/src/core/installers/file-installer.ts +0 -1
  23. package/src/core/installers/mcp-installer.ts +0 -1
  24. package/src/core/project-manager.ts +24 -18
  25. package/src/core/secrets-manager.ts +54 -73
  26. package/src/core/session-manager.ts +20 -22
  27. package/src/core/state-detector.ts +139 -80
  28. package/src/core/template-loader.ts +13 -31
  29. package/src/core/upgrade-manager.ts +122 -69
  30. package/src/index.ts +8 -5
  31. package/src/services/auto-upgrade.ts +1 -1
  32. package/src/services/config-service.ts +41 -29
  33. package/src/services/global-config.ts +2 -2
  34. package/src/services/target-installer.ts +9 -7
  35. package/src/targets/claude-code.ts +28 -15
  36. package/src/targets/opencode.ts +17 -6
  37. package/src/types/cli.types.ts +2 -2
  38. package/src/types/provider.types.ts +1 -7
  39. package/src/types/session.types.ts +11 -11
  40. package/src/types/target.types.ts +3 -1
  41. package/src/types/todo.types.ts +1 -1
  42. package/src/types.ts +1 -1
  43. package/src/utils/__tests__/package-manager-detector.test.ts +6 -6
  44. package/src/utils/agent-enhancer.ts +111 -3
  45. package/src/utils/config/paths.ts +3 -1
  46. package/src/utils/config/target-utils.ts +2 -2
  47. package/src/utils/display/banner.ts +2 -2
  48. package/src/utils/display/notifications.ts +58 -45
  49. package/src/utils/display/status.ts +29 -12
  50. package/src/utils/files/file-operations.ts +1 -1
  51. package/src/utils/files/sync-utils.ts +38 -41
  52. package/src/utils/index.ts +19 -27
  53. package/src/utils/package-manager-detector.ts +15 -5
  54. package/src/utils/security/security.ts +8 -4
  55. package/src/utils/target-selection.ts +5 -2
  56. package/src/utils/version.ts +4 -2
  57. package/src/commands/flow/execute.ts +0 -453
  58. package/src/commands/flow/setup.ts +0 -312
  59. package/src/commands/flow-orchestrator.ts +0 -328
  60. package/src/commands/init-command.ts +0 -92
  61. package/src/commands/init-core.ts +0 -331
  62. package/src/commands/run-command.ts +0 -126
  63. package/src/core/agent-manager.ts +0 -174
  64. package/src/core/loop-controller.ts +0 -200
  65. package/src/core/rule-loader.ts +0 -147
  66. package/src/core/rule-manager.ts +0 -240
  67. package/src/services/claude-config-service.ts +0 -252
  68. package/src/services/first-run-setup.ts +0 -220
  69. package/src/services/smart-config-service.ts +0 -269
  70. package/src/types/api.types.ts +0 -9
@@ -3,13 +3,13 @@
3
3
  * Interactive configuration for Sylphx Flow
4
4
  */
5
5
 
6
- import { Command } from 'commander';
7
6
  import chalk from 'chalk';
7
+ import { Command } from 'commander';
8
8
  import inquirer from 'inquirer';
9
9
  import { GlobalConfigService } from '../services/global-config.js';
10
- import { UserCancelledError } from '../utils/errors.js';
11
10
  import { TargetInstaller } from '../services/target-installer.js';
12
- import { promptForDefaultTarget, buildAvailableTargets } from '../utils/target-selection.js';
11
+ import { UserCancelledError } from '../utils/errors.js';
12
+ import { buildAvailableTargets, promptForDefaultTarget } from '../utils/target-selection.js';
13
13
 
14
14
  export const settingsCommand = new Command('settings')
15
15
  .description('Configure Sylphx Flow settings')
@@ -30,9 +30,10 @@ export const settingsCommand = new Command('settings')
30
30
  } else {
31
31
  await showMainMenu(configService);
32
32
  }
33
- } catch (error: any) {
33
+ } catch (error: unknown) {
34
34
  // Handle user cancellation (Ctrl+C)
35
- if (error.name === 'ExitPromptError' || error.message?.includes('force closed')) {
35
+ const err = error as Error & { name?: string };
36
+ if (err.name === 'ExitPromptError' || err.message?.includes('force closed')) {
36
37
  throw new UserCancelledError('Settings cancelled by user');
37
38
  }
38
39
  throw error;
@@ -123,9 +124,7 @@ async function configureAgents(configService: GlobalConfigService): Promise<void
123
124
  };
124
125
 
125
126
  // Get current enabled agents
126
- const currentEnabled = Object.keys(currentAgents).filter(
127
- (key) => currentAgents[key].enabled
128
- );
127
+ const currentEnabled = Object.keys(currentAgents).filter((key) => currentAgents[key].enabled);
129
128
 
130
129
  const { selectedAgents } = await inquirer.prompt([
131
130
  {
@@ -191,9 +190,7 @@ async function configureRules(configService: GlobalConfigService): Promise<void>
191
190
  };
192
191
 
193
192
  // Get current enabled rules
194
- const currentEnabled = Object.keys(currentRules).filter(
195
- (key) => currentRules[key].enabled
196
- );
193
+ const currentEnabled = Object.keys(currentRules).filter((key) => currentRules[key].enabled);
197
194
 
198
195
  const { selectedRules } = await inquirer.prompt([
199
196
  {
@@ -239,9 +236,7 @@ async function configureOutputStyles(configService: GlobalConfigService): Promis
239
236
  };
240
237
 
241
238
  // Get current enabled styles
242
- const currentEnabled = Object.keys(currentStyles).filter(
243
- (key) => currentStyles[key].enabled
244
- );
239
+ const currentEnabled = Object.keys(currentStyles).filter((key) => currentStyles[key].enabled);
245
240
 
246
241
  const { selectedStyles } = await inquirer.prompt([
247
242
  {
@@ -283,17 +278,15 @@ async function configureMCP(configService: GlobalConfigService): Promise<void> {
283
278
 
284
279
  // Available MCP servers (from MCP_SERVER_REGISTRY)
285
280
  const availableServers = {
286
- 'grep': { name: 'GitHub Code Search (grep.app)', requiresEnv: [] },
287
- 'context7': { name: 'Context7 Docs', requiresEnv: [] },
288
- 'playwright': { name: 'Playwright Browser Control', requiresEnv: [] },
289
- 'github': { name: 'GitHub', requiresEnv: ['GITHUB_TOKEN'] },
290
- 'notion': { name: 'Notion', requiresEnv: ['NOTION_API_KEY'] },
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'] },
291
286
  };
292
287
 
293
288
  // Get current enabled servers
294
- const currentEnabled = Object.keys(currentServers).filter(
295
- (key) => currentServers[key].enabled
296
- );
289
+ const currentEnabled = Object.keys(currentServers).filter((key) => currentServers[key].enabled);
297
290
 
298
291
  const { selectedServers } = await inquirer.prompt([
299
292
  {
@@ -301,9 +294,10 @@ async function configureMCP(configService: GlobalConfigService): Promise<void> {
301
294
  name: 'selectedServers',
302
295
  message: 'Select MCP servers to enable:',
303
296
  choices: Object.entries(availableServers).map(([key, info]) => {
304
- const requiresText = info.requiresEnv.length > 0
305
- ? chalk.dim(` (requires ${info.requiresEnv.join(', ')})`)
306
- : '';
297
+ const requiresText =
298
+ info.requiresEnv.length > 0
299
+ ? chalk.dim(` (requires ${info.requiresEnv.join(', ')})`)
300
+ : '';
307
301
  return {
308
302
  name: `${info.name}${requiresText}`,
309
303
  value: key,
@@ -316,10 +310,10 @@ async function configureMCP(configService: GlobalConfigService): Promise<void> {
316
310
  // Update servers
317
311
  for (const key of Object.keys(availableServers)) {
318
312
  if (selectedServers.includes(key)) {
319
- if (!currentServers[key]) {
320
- currentServers[key] = { enabled: true, env: {} };
321
- } else {
313
+ if (currentServers[key]) {
322
314
  currentServers[key].enabled = true;
315
+ } else {
316
+ currentServers[key] = { enabled: true, env: {} };
323
317
  }
324
318
  } else if (currentServers[key]) {
325
319
  currentServers[key].enabled = false;
@@ -333,7 +327,7 @@ async function configureMCP(configService: GlobalConfigService): Promise<void> {
333
327
  const server = currentServers[serverKey];
334
328
 
335
329
  for (const envKey of serverInfo.requiresEnv) {
336
- const hasKey = server.env && server.env[envKey];
330
+ const hasKey = server.env?.[envKey];
337
331
 
338
332
  const { shouldConfigure } = await inquirer.prompt([
339
333
  {
@@ -406,7 +400,9 @@ async function configureProvider(configService: GlobalConfigService): Promise<vo
406
400
  {
407
401
  type: 'confirm',
408
402
  name: 'shouldConfigure',
409
- message: currentKey ? `Update ${defaultProvider} API key?` : `Configure ${defaultProvider} API key?`,
403
+ message: currentKey
404
+ ? `Update ${defaultProvider} API key?`
405
+ : `Configure ${defaultProvider} API key?`,
410
406
  default: !currentKey,
411
407
  },
412
408
  ]);
@@ -424,8 +420,11 @@ async function configureProvider(configService: GlobalConfigService): Promise<vo
424
420
  if (!providerConfig.claudeCode.providers[defaultProvider]) {
425
421
  providerConfig.claudeCode.providers[defaultProvider] = { enabled: true };
426
422
  }
427
- providerConfig.claudeCode.providers[defaultProvider]!.apiKey = apiKey;
428
- providerConfig.claudeCode.providers[defaultProvider]!.enabled = true;
423
+ const provider = providerConfig.claudeCode.providers[defaultProvider];
424
+ if (provider) {
425
+ provider.apiKey = apiKey;
426
+ provider.enabled = true;
427
+ }
429
428
  }
430
429
  }
431
430
 
@@ -450,7 +449,11 @@ async function configureTarget(configService: GlobalConfigService): Promise<void
450
449
 
451
450
  const defaultTarget = await promptForDefaultTarget(installedTargets, settings.defaultTarget);
452
451
 
453
- settings.defaultTarget = defaultTarget as 'claude-code' | 'opencode' | 'cursor' | 'ask-every-time';
452
+ settings.defaultTarget = defaultTarget as
453
+ | 'claude-code'
454
+ | 'opencode'
455
+ | 'cursor'
456
+ | 'ask-every-time';
454
457
  await configService.saveSettings(settings);
455
458
 
456
459
  if (defaultTarget === 'ask-every-time') {
@@ -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