@sylphx/flow 2.1.3 → 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 +28 -0
- package/README.md +44 -0
- package/package.json +79 -73
- package/src/commands/flow/execute-v2.ts +37 -29
- package/src/commands/flow/prompt.ts +5 -3
- package/src/commands/flow/types.ts +0 -2
- package/src/commands/flow-command.ts +20 -13
- package/src/commands/hook-command.ts +1 -3
- package/src/commands/settings/checkbox-config.ts +128 -0
- package/src/commands/settings/index.ts +6 -0
- package/src/commands/settings-command.ts +84 -156
- package/src/config/ai-config.ts +60 -41
- package/src/core/agent-loader.ts +11 -6
- package/src/core/attach/file-attacher.ts +172 -0
- package/src/core/attach/index.ts +5 -0
- package/src/core/attach-manager.ts +117 -171
- package/src/core/backup-manager.ts +35 -29
- package/src/core/cleanup-handler.ts +11 -8
- package/src/core/error-handling.ts +23 -30
- package/src/core/flow-executor.ts +58 -76
- package/src/core/formatting/bytes.ts +2 -4
- package/src/core/functional/async.ts +5 -4
- package/src/core/functional/error-handler.ts +2 -2
- package/src/core/git-stash-manager.ts +21 -10
- package/src/core/installers/file-installer.ts +0 -1
- package/src/core/installers/mcp-installer.ts +0 -1
- package/src/core/project-manager.ts +24 -18
- package/src/core/secrets-manager.ts +54 -73
- package/src/core/session-manager.ts +20 -22
- package/src/core/state-detector.ts +139 -80
- package/src/core/template-loader.ts +13 -31
- package/src/core/upgrade-manager.ts +122 -69
- package/src/index.ts +8 -5
- package/src/services/auto-upgrade.ts +1 -1
- package/src/services/config-service.ts +41 -29
- package/src/services/global-config.ts +3 -3
- package/src/services/target-installer.ts +11 -26
- package/src/targets/claude-code.ts +35 -81
- package/src/targets/opencode.ts +28 -68
- 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/types/cli.types.ts +2 -2
- package/src/types/provider.types.ts +1 -7
- package/src/types/session.types.ts +11 -11
- package/src/types/target.types.ts +3 -1
- package/src/types/todo.types.ts +1 -1
- package/src/types.ts +1 -1
- package/src/utils/__tests__/package-manager-detector.test.ts +6 -6
- package/src/utils/agent-enhancer.ts +4 -4
- package/src/utils/config/paths.ts +3 -1
- package/src/utils/config/target-utils.ts +2 -2
- package/src/utils/display/banner.ts +2 -2
- package/src/utils/display/notifications.ts +58 -45
- package/src/utils/display/status.ts +29 -12
- package/src/utils/files/file-operations.ts +1 -1
- package/src/utils/files/sync-utils.ts +38 -41
- package/src/utils/index.ts +19 -27
- package/src/utils/package-manager-detector.ts +15 -5
- package/src/utils/security/security.ts +8 -4
- package/src/utils/target-selection.ts +6 -8
- package/src/utils/version.ts +4 -2
- package/src/commands/flow-orchestrator.ts +0 -328
- package/src/commands/init-command.ts +0 -92
- package/src/commands/init-core.ts +0 -331
- package/src/core/agent-manager.ts +0 -174
- package/src/core/loop-controller.ts +0 -200
- package/src/core/rule-loader.ts +0 -147
- package/src/core/rule-manager.ts +0 -240
- package/src/services/claude-config-service.ts +0 -252
- package/src/services/first-run-setup.ts +0 -220
- package/src/services/smart-config-service.ts +0 -269
- package/src/types/api.types.ts +0 -9
|
@@ -3,13 +3,15 @@
|
|
|
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
|
+
import { getRequiredEnvVars, MCP_SERVER_REGISTRY, type MCPServerID } from '../config/servers.js';
|
|
9
10
|
import { GlobalConfigService } from '../services/global-config.js';
|
|
10
|
-
import { UserCancelledError } from '../utils/errors.js';
|
|
11
11
|
import { TargetInstaller } from '../services/target-installer.js';
|
|
12
|
-
import {
|
|
12
|
+
import { UserCancelledError } from '../utils/errors.js';
|
|
13
|
+
import { buildAvailableTargets, promptForDefaultTarget } from '../utils/target-selection.js';
|
|
14
|
+
import { handleCheckboxConfig, printHeader, printConfirmation } from './settings/index.js';
|
|
13
15
|
|
|
14
16
|
export const settingsCommand = new Command('settings')
|
|
15
17
|
.description('Configure Sylphx Flow settings')
|
|
@@ -30,9 +32,10 @@ export const settingsCommand = new Command('settings')
|
|
|
30
32
|
} else {
|
|
31
33
|
await showMainMenu(configService);
|
|
32
34
|
}
|
|
33
|
-
} catch (error:
|
|
35
|
+
} catch (error: unknown) {
|
|
34
36
|
// Handle user cancellation (Ctrl+C)
|
|
35
|
-
|
|
37
|
+
const err = error as Error & { name?: string };
|
|
38
|
+
if (err.name === 'ExitPromptError' || err.message?.includes('force closed')) {
|
|
36
39
|
throw new UserCancelledError('Settings cancelled by user');
|
|
37
40
|
}
|
|
38
41
|
throw error;
|
|
@@ -105,16 +108,12 @@ async function openSection(section: string, configService: GlobalConfigService):
|
|
|
105
108
|
}
|
|
106
109
|
|
|
107
110
|
/**
|
|
108
|
-
* Configure Agents
|
|
111
|
+
* Configure Agents - uses shared checkbox handler + default selection
|
|
109
112
|
*/
|
|
110
113
|
async function configureAgents(configService: GlobalConfigService): Promise<void> {
|
|
111
|
-
console.log(chalk.cyan.bold('\n━━━ 🤖 Agent Configuration\n'));
|
|
112
|
-
|
|
113
114
|
const flowConfig = await configService.loadFlowConfig();
|
|
114
115
|
const settings = await configService.loadSettings();
|
|
115
|
-
const currentAgents = flowConfig.agents || {};
|
|
116
116
|
|
|
117
|
-
// Available agents
|
|
118
117
|
const availableAgents = {
|
|
119
118
|
coder: 'Coder - Write and modify code',
|
|
120
119
|
writer: 'Writer - Documentation and explanation',
|
|
@@ -122,40 +121,22 @@ async function configureAgents(configService: GlobalConfigService): Promise<void
|
|
|
122
121
|
orchestrator: 'Orchestrator - Task coordination',
|
|
123
122
|
};
|
|
124
123
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
name: 'selectedAgents',
|
|
134
|
-
message: 'Select agents to enable:',
|
|
135
|
-
choices: Object.entries(availableAgents).map(([key, name]) => ({
|
|
136
|
-
name,
|
|
137
|
-
value: key,
|
|
138
|
-
checked: currentEnabled.includes(key),
|
|
139
|
-
})),
|
|
140
|
-
},
|
|
141
|
-
]);
|
|
142
|
-
|
|
143
|
-
// Update agents
|
|
144
|
-
for (const key of Object.keys(availableAgents)) {
|
|
145
|
-
if (selectedAgents.includes(key)) {
|
|
146
|
-
currentAgents[key] = { enabled: true };
|
|
147
|
-
} else {
|
|
148
|
-
currentAgents[key] = { enabled: false };
|
|
149
|
-
}
|
|
150
|
-
}
|
|
124
|
+
const { selected, updated } = await handleCheckboxConfig({
|
|
125
|
+
title: 'Agent Configuration',
|
|
126
|
+
icon: '🤖',
|
|
127
|
+
message: 'Select agents to enable:',
|
|
128
|
+
available: availableAgents,
|
|
129
|
+
current: flowConfig.agents || {},
|
|
130
|
+
itemType: 'Agents',
|
|
131
|
+
});
|
|
151
132
|
|
|
152
|
-
//
|
|
133
|
+
// Additional step: select default agent from enabled ones
|
|
153
134
|
const { defaultAgent } = await inquirer.prompt([
|
|
154
135
|
{
|
|
155
136
|
type: 'list',
|
|
156
137
|
name: 'defaultAgent',
|
|
157
138
|
message: 'Select default agent:',
|
|
158
|
-
choices:
|
|
139
|
+
choices: selected.map((key) => ({
|
|
159
140
|
name: availableAgents[key as keyof typeof availableAgents],
|
|
160
141
|
value: key,
|
|
161
142
|
})),
|
|
@@ -163,113 +144,57 @@ async function configureAgents(configService: GlobalConfigService): Promise<void
|
|
|
163
144
|
},
|
|
164
145
|
]);
|
|
165
146
|
|
|
166
|
-
flowConfig.agents =
|
|
147
|
+
flowConfig.agents = updated;
|
|
167
148
|
await configService.saveFlowConfig(flowConfig);
|
|
168
149
|
|
|
169
150
|
settings.defaultAgent = defaultAgent;
|
|
170
151
|
await configService.saveSettings(settings);
|
|
171
152
|
|
|
172
|
-
console.log(chalk.green(`\n✓ Agent configuration saved`));
|
|
173
|
-
console.log(chalk.dim(` Enabled agents: ${selectedAgents.length}`));
|
|
174
153
|
console.log(chalk.dim(` Default agent: ${defaultAgent}`));
|
|
175
154
|
}
|
|
176
155
|
|
|
177
156
|
/**
|
|
178
|
-
* Configure Rules
|
|
157
|
+
* Configure Rules - uses shared checkbox handler
|
|
179
158
|
*/
|
|
180
159
|
async function configureRules(configService: GlobalConfigService): Promise<void> {
|
|
181
|
-
console.log(chalk.cyan.bold('\n━━━ 📋 Rules Configuration\n'));
|
|
182
|
-
|
|
183
160
|
const flowConfig = await configService.loadFlowConfig();
|
|
184
|
-
const currentRules = flowConfig.rules || {};
|
|
185
|
-
|
|
186
|
-
// Available rules
|
|
187
|
-
const availableRules = {
|
|
188
|
-
core: 'Core - Identity, personality, execution',
|
|
189
|
-
'code-standards': 'Code Standards - Quality, patterns, anti-patterns',
|
|
190
|
-
workspace: 'Workspace - Documentation management',
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
// Get current enabled rules
|
|
194
|
-
const currentEnabled = Object.keys(currentRules).filter(
|
|
195
|
-
(key) => currentRules[key].enabled
|
|
196
|
-
);
|
|
197
161
|
|
|
198
|
-
const {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
checked: currentEnabled.includes(key),
|
|
207
|
-
})),
|
|
162
|
+
const { updated } = await handleCheckboxConfig({
|
|
163
|
+
title: 'Rules Configuration',
|
|
164
|
+
icon: '📋',
|
|
165
|
+
message: 'Select rules to enable:',
|
|
166
|
+
available: {
|
|
167
|
+
core: 'Core - Identity, personality, execution',
|
|
168
|
+
'code-standards': 'Code Standards - Quality, patterns, anti-patterns',
|
|
169
|
+
workspace: 'Workspace - Documentation management',
|
|
208
170
|
},
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
for (const key of Object.keys(availableRules)) {
|
|
213
|
-
if (selectedRules.includes(key)) {
|
|
214
|
-
currentRules[key] = { enabled: true };
|
|
215
|
-
} else {
|
|
216
|
-
currentRules[key] = { enabled: false };
|
|
217
|
-
}
|
|
218
|
-
}
|
|
171
|
+
current: flowConfig.rules || {},
|
|
172
|
+
itemType: 'Rules',
|
|
173
|
+
});
|
|
219
174
|
|
|
220
|
-
flowConfig.rules =
|
|
175
|
+
flowConfig.rules = updated;
|
|
221
176
|
await configService.saveFlowConfig(flowConfig);
|
|
222
|
-
|
|
223
|
-
console.log(chalk.green(`\n✓ Rules configuration saved`));
|
|
224
|
-
console.log(chalk.dim(` Enabled rules: ${selectedRules.length}`));
|
|
225
177
|
}
|
|
226
178
|
|
|
227
179
|
/**
|
|
228
|
-
* Configure Output Styles
|
|
180
|
+
* Configure Output Styles - uses shared checkbox handler
|
|
229
181
|
*/
|
|
230
182
|
async function configureOutputStyles(configService: GlobalConfigService): Promise<void> {
|
|
231
|
-
console.log(chalk.cyan.bold('\n━━━ 🎨 Output Styles Configuration\n'));
|
|
232
|
-
|
|
233
183
|
const flowConfig = await configService.loadFlowConfig();
|
|
234
|
-
const currentStyles = flowConfig.outputStyles || {};
|
|
235
|
-
|
|
236
|
-
// Available output styles
|
|
237
|
-
const availableStyles = {
|
|
238
|
-
silent: 'Silent - Execution without narration',
|
|
239
|
-
};
|
|
240
184
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
{
|
|
248
|
-
type: 'checkbox',
|
|
249
|
-
name: 'selectedStyles',
|
|
250
|
-
message: 'Select output styles to enable:',
|
|
251
|
-
choices: Object.entries(availableStyles).map(([key, name]) => ({
|
|
252
|
-
name,
|
|
253
|
-
value: key,
|
|
254
|
-
checked: currentEnabled.includes(key),
|
|
255
|
-
})),
|
|
185
|
+
const { updated } = await handleCheckboxConfig({
|
|
186
|
+
title: 'Output Styles Configuration',
|
|
187
|
+
icon: '🎨',
|
|
188
|
+
message: 'Select output styles to enable:',
|
|
189
|
+
available: {
|
|
190
|
+
silent: 'Silent - Execution without narration',
|
|
256
191
|
},
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
for (const key of Object.keys(availableStyles)) {
|
|
261
|
-
if (selectedStyles.includes(key)) {
|
|
262
|
-
currentStyles[key] = { enabled: true };
|
|
263
|
-
} else {
|
|
264
|
-
currentStyles[key] = { enabled: false };
|
|
265
|
-
}
|
|
266
|
-
}
|
|
192
|
+
current: flowConfig.outputStyles || {},
|
|
193
|
+
itemType: 'Output styles',
|
|
194
|
+
});
|
|
267
195
|
|
|
268
|
-
flowConfig.outputStyles =
|
|
196
|
+
flowConfig.outputStyles = updated;
|
|
269
197
|
await configService.saveFlowConfig(flowConfig);
|
|
270
|
-
|
|
271
|
-
console.log(chalk.green(`\n✓ Output styles configuration saved`));
|
|
272
|
-
console.log(chalk.dim(` Enabled styles: ${selectedStyles.length}`));
|
|
273
198
|
}
|
|
274
199
|
|
|
275
200
|
/**
|
|
@@ -281,67 +206,62 @@ async function configureMCP(configService: GlobalConfigService): Promise<void> {
|
|
|
281
206
|
const mcpConfig = await configService.loadMCPConfig();
|
|
282
207
|
const currentServers = mcpConfig.servers || {};
|
|
283
208
|
|
|
284
|
-
//
|
|
285
|
-
const
|
|
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'] },
|
|
291
|
-
};
|
|
209
|
+
// Get all servers from registry
|
|
210
|
+
const allServerIds = Object.keys(MCP_SERVER_REGISTRY) as MCPServerID[];
|
|
292
211
|
|
|
293
212
|
// Get current enabled servers
|
|
294
|
-
const currentEnabled = Object.keys(currentServers).filter(
|
|
295
|
-
(key) => currentServers[key].enabled
|
|
296
|
-
);
|
|
213
|
+
const currentEnabled = Object.keys(currentServers).filter((key) => currentServers[key].enabled);
|
|
297
214
|
|
|
298
215
|
const { selectedServers } = await inquirer.prompt([
|
|
299
216
|
{
|
|
300
217
|
type: 'checkbox',
|
|
301
218
|
name: 'selectedServers',
|
|
302
219
|
message: 'Select MCP servers to enable:',
|
|
303
|
-
choices:
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
220
|
+
choices: allServerIds.map((id) => {
|
|
221
|
+
const server = MCP_SERVER_REGISTRY[id];
|
|
222
|
+
const requiredEnvVars = getRequiredEnvVars(id);
|
|
223
|
+
const requiresText =
|
|
224
|
+
requiredEnvVars.length > 0 ? chalk.dim(` (requires ${requiredEnvVars.join(', ')})`) : '';
|
|
307
225
|
return {
|
|
308
|
-
name: `${
|
|
309
|
-
value:
|
|
310
|
-
checked: currentEnabled.includes(
|
|
226
|
+
name: `${server.name} - ${server.description}${requiresText}`,
|
|
227
|
+
value: id,
|
|
228
|
+
checked: currentEnabled.includes(id) || server.defaultInInit,
|
|
311
229
|
};
|
|
312
230
|
}),
|
|
313
231
|
},
|
|
314
232
|
]);
|
|
315
233
|
|
|
316
234
|
// Update servers
|
|
317
|
-
for (const
|
|
318
|
-
if (selectedServers.includes(
|
|
319
|
-
if (
|
|
320
|
-
currentServers[
|
|
235
|
+
for (const id of allServerIds) {
|
|
236
|
+
if (selectedServers.includes(id)) {
|
|
237
|
+
if (currentServers[id]) {
|
|
238
|
+
currentServers[id].enabled = true;
|
|
321
239
|
} else {
|
|
322
|
-
currentServers[
|
|
240
|
+
currentServers[id] = { enabled: true, env: {} };
|
|
323
241
|
}
|
|
324
|
-
} else if (currentServers[
|
|
325
|
-
currentServers[
|
|
242
|
+
} else if (currentServers[id]) {
|
|
243
|
+
currentServers[id].enabled = false;
|
|
326
244
|
}
|
|
327
245
|
}
|
|
328
246
|
|
|
329
247
|
// Ask for API keys for newly enabled servers
|
|
330
|
-
for (const
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
const server = currentServers[serverKey];
|
|
248
|
+
for (const serverId of selectedServers as MCPServerID[]) {
|
|
249
|
+
const serverDef = MCP_SERVER_REGISTRY[serverId];
|
|
250
|
+
const requiredEnvVars = getRequiredEnvVars(serverId);
|
|
334
251
|
|
|
335
|
-
|
|
336
|
-
|
|
252
|
+
if (requiredEnvVars.length > 0) {
|
|
253
|
+
const server = currentServers[serverId];
|
|
254
|
+
|
|
255
|
+
for (const envKey of requiredEnvVars) {
|
|
256
|
+
const hasKey = server.env?.[envKey];
|
|
337
257
|
|
|
338
258
|
const { shouldConfigure } = await inquirer.prompt([
|
|
339
259
|
{
|
|
340
260
|
type: 'confirm',
|
|
341
261
|
name: 'shouldConfigure',
|
|
342
262
|
message: hasKey
|
|
343
|
-
? `Update ${envKey} for ${
|
|
344
|
-
: `Configure ${envKey} for ${
|
|
263
|
+
? `Update ${envKey} for ${serverDef.name}?`
|
|
264
|
+
: `Configure ${envKey} for ${serverDef.name}?`,
|
|
345
265
|
default: !hasKey,
|
|
346
266
|
},
|
|
347
267
|
]);
|
|
@@ -406,7 +326,9 @@ async function configureProvider(configService: GlobalConfigService): Promise<vo
|
|
|
406
326
|
{
|
|
407
327
|
type: 'confirm',
|
|
408
328
|
name: 'shouldConfigure',
|
|
409
|
-
message: currentKey
|
|
329
|
+
message: currentKey
|
|
330
|
+
? `Update ${defaultProvider} API key?`
|
|
331
|
+
: `Configure ${defaultProvider} API key?`,
|
|
410
332
|
default: !currentKey,
|
|
411
333
|
},
|
|
412
334
|
]);
|
|
@@ -424,8 +346,11 @@ async function configureProvider(configService: GlobalConfigService): Promise<vo
|
|
|
424
346
|
if (!providerConfig.claudeCode.providers[defaultProvider]) {
|
|
425
347
|
providerConfig.claudeCode.providers[defaultProvider] = { enabled: true };
|
|
426
348
|
}
|
|
427
|
-
providerConfig.claudeCode.providers[defaultProvider]
|
|
428
|
-
|
|
349
|
+
const provider = providerConfig.claudeCode.providers[defaultProvider];
|
|
350
|
+
if (provider) {
|
|
351
|
+
provider.apiKey = apiKey;
|
|
352
|
+
provider.enabled = true;
|
|
353
|
+
}
|
|
429
354
|
}
|
|
430
355
|
}
|
|
431
356
|
|
|
@@ -450,7 +375,10 @@ async function configureTarget(configService: GlobalConfigService): Promise<void
|
|
|
450
375
|
|
|
451
376
|
const defaultTarget = await promptForDefaultTarget(installedTargets, settings.defaultTarget);
|
|
452
377
|
|
|
453
|
-
settings.defaultTarget = defaultTarget as
|
|
378
|
+
settings.defaultTarget = defaultTarget as
|
|
379
|
+
| 'claude-code'
|
|
380
|
+
| 'opencode'
|
|
381
|
+
| 'ask-every-time';
|
|
454
382
|
await configService.saveSettings(settings);
|
|
455
383
|
|
|
456
384
|
if (defaultTarget === 'ask-every-time') {
|
package/src/config/ai-config.ts
CHANGED
|
@@ -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,
|
|
16
|
+
import { type Result, tryCatchAsync } from '../core/functional/result.js';
|
|
17
17
|
import { getAllProviders } from '../providers/index.js';
|
|
18
|
-
import type {
|
|
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
|
|
45
|
+
defaultProvider: z
|
|
46
|
+
.enum(['anthropic', 'openai', 'google', 'openrouter', 'claude-code', 'zai'])
|
|
47
|
+
.optional(),
|
|
43
48
|
defaultModel: z.string().optional(),
|
|
44
|
-
providers: z
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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 = (
|
|
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:
|
|
83
|
-
|
|
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,
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
|
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 =
|
|
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)
|
|
160
|
-
|
|
161
|
-
|
|
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:
|
|
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 =
|
|
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)
|
|
233
|
+
await fs.writeFile(configPath, `${JSON.stringify(validated, null, 2)}\n`, 'utf8');
|
|
215
234
|
},
|
|
216
|
-
(error:
|
|
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 =
|
|
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)
|
|
259
|
+
await fs.writeFile(configPath, `${JSON.stringify(validated, null, 2)}\n`, 'utf8');
|
|
241
260
|
},
|
|
242
|
-
(error:
|
|
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
|
-
|
package/src/core/agent-loader.ts
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* Loads agent definitions from markdown files with front matter
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
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(
|
|
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 (
|
|
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
|
-
|
|
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 =>
|
|
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
|