@sylphx/flow 2.16.2 → 2.17.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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @sylphx/flow
2
2
 
3
+ ## 2.17.0 (2025-12-18)
4
+
5
+ ### ✨ Features
6
+
7
+ - **skills:** implement skills loading and attach pipeline ([8d720b8](https://github.com/SylphxAI/flow/commit/8d720b8118ffe19e14d50aaacd9282bd5d42e702))
8
+
3
9
  ## 2.16.2 (2025-12-17)
4
10
 
5
11
  ### 🐛 Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sylphx/flow",
3
- "version": "2.16.2",
3
+ "version": "2.17.0",
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": {
@@ -21,6 +21,8 @@ export interface AttachResult {
21
21
  agentsOverridden: string[];
22
22
  commandsAdded: string[];
23
23
  commandsOverridden: string[];
24
+ skillsAdded: string[];
25
+ skillsOverridden: string[];
24
26
  rulesAppended: boolean;
25
27
  mcpServersAdded: string[];
26
28
  mcpServersOverridden: string[];
@@ -31,7 +33,7 @@ export interface AttachResult {
31
33
  }
32
34
 
33
35
  export interface ConflictInfo {
34
- type: 'agent' | 'command' | 'mcp' | 'hook';
36
+ type: 'agent' | 'command' | 'skill' | 'mcp' | 'hook';
35
37
  name: string;
36
38
  action: 'overridden' | 'merged';
37
39
  message: string;
@@ -40,6 +42,7 @@ export interface ConflictInfo {
40
42
  export interface FlowTemplates {
41
43
  agents: Array<{ name: string; content: string }>;
42
44
  commands: Array<{ name: string; content: string }>;
45
+ skills: Array<{ name: string; content: string }>;
43
46
  rules?: string;
44
47
  mcpServers: Array<{ name: string; config: Record<string, unknown> }>;
45
48
  hooks: Array<{ name: string; content: string }>;
@@ -143,6 +146,8 @@ export class AttachManager {
143
146
  agentsOverridden: [],
144
147
  commandsAdded: [],
145
148
  commandsOverridden: [],
149
+ skillsAdded: [],
150
+ skillsOverridden: [],
146
151
  rulesAppended: false,
147
152
  mcpServersAdded: [],
148
153
  mcpServersOverridden: [],
@@ -160,12 +165,17 @@ export class AttachManager {
160
165
  // 2. Attach commands
161
166
  await this.attachCommands(projectPath, target, templates.commands, result, manifest);
162
167
 
163
- // 3. Attach rules (if applicable)
168
+ // 3. Attach skills (if target supports them)
169
+ if (target.config.skillsDir && templates.skills.length > 0) {
170
+ await this.attachSkills(projectPath, target, templates.skills, result, manifest);
171
+ }
172
+
173
+ // 4. Attach rules (if applicable)
164
174
  if (templates.rules) {
165
175
  await this.attachRules(projectPath, target, templates.rules, result, manifest);
166
176
  }
167
177
 
168
- // 4. Attach MCP servers (merge global + template servers)
178
+ // 5. Attach MCP servers (merge global + template servers)
169
179
  const globalMCPServers = await this.loadGlobalMCPServers(target);
170
180
  const allMCPServers = [...globalMCPServers, ...templates.mcpServers];
171
181
 
@@ -173,12 +183,12 @@ export class AttachManager {
173
183
  await this.attachMCPServers(projectPath, target, allMCPServers, result, manifest);
174
184
  }
175
185
 
176
- // 5. Attach hooks
186
+ // 6. Attach hooks
177
187
  if (templates.hooks.length > 0) {
178
188
  await this.attachHooks(projectPath, target, templates.hooks, result, manifest);
179
189
  }
180
190
 
181
- // 6. Attach single files
191
+ // 7. Attach single files
182
192
  if (templates.singleFiles.length > 0) {
183
193
  await this.attachSingleFiles(projectPath, templates.singleFiles, result, manifest);
184
194
  }
@@ -238,6 +248,45 @@ export class AttachManager {
238
248
  manifest.backup.commands.flow.push(...itemManifest.flow);
239
249
  }
240
250
 
251
+ /**
252
+ * Attach skills (override strategy)
253
+ * Skills are stored as <domain>/SKILL.md subdirectories
254
+ */
255
+ private async attachSkills(
256
+ projectPath: string,
257
+ target: Target,
258
+ skills: Array<{ name: string; content: string }>,
259
+ result: AttachResult,
260
+ manifest: BackupManifest
261
+ ): Promise<void> {
262
+ const skillsDir = path.join(projectPath, target.config.skillsDir!);
263
+ await fs.mkdir(skillsDir, { recursive: true });
264
+
265
+ for (const skill of skills) {
266
+ // skill.name is like "auth/SKILL.md" - create subdirectory
267
+ const skillPath = path.join(skillsDir, skill.name);
268
+ const skillSubDir = path.dirname(skillPath);
269
+ await fs.mkdir(skillSubDir, { recursive: true });
270
+
271
+ const existed = existsSync(skillPath);
272
+ if (existed) {
273
+ result.skillsOverridden.push(skill.name);
274
+ result.conflicts.push({
275
+ type: 'skill',
276
+ name: skill.name,
277
+ action: 'overridden',
278
+ message: `Skill '${skill.name}' overridden (will be restored on exit)`,
279
+ });
280
+ manifest.backup.skills.user.push(skillPath);
281
+ } else {
282
+ result.skillsAdded.push(skill.name);
283
+ }
284
+
285
+ await fs.writeFile(skillPath, skill.content);
286
+ manifest.backup.skills.flow.push(skillPath);
287
+ }
288
+ }
289
+
241
290
  /**
242
291
  * Attach rules (append strategy for AGENTS.md)
243
292
  * Uses shared attachRulesFile function
@@ -38,6 +38,10 @@ export interface BackupManifest {
38
38
  user: string[];
39
39
  flow: string[];
40
40
  };
41
+ skills: {
42
+ user: string[];
43
+ flow: string[];
44
+ };
41
45
  rules?: {
42
46
  path: string;
43
47
  originalSize: number;
@@ -114,6 +118,7 @@ export class BackupManager {
114
118
  backup: {
115
119
  agents: { user: [], flow: [] },
116
120
  commands: { user: [], flow: [] },
121
+ skills: { user: [], flow: [] },
117
122
  singleFiles: {},
118
123
  },
119
124
  secrets: {
@@ -137,6 +137,7 @@ export class FlowExecutor {
137
137
  joined: false,
138
138
  agents: attachResult.agentsAdded.length,
139
139
  commands: attachResult.commandsAdded.length,
140
+ skills: attachResult.skillsAdded.length,
140
141
  mcp: attachResult.mcpServersAdded.length,
141
142
  };
142
143
  }
@@ -184,7 +185,15 @@ export class FlowExecutor {
184
185
  }
185
186
  }
186
187
 
187
- // 3. Clear hooks directory (in configDir)
188
+ // 3. Clear skills directory (if target supports skills)
189
+ if (target.config.skillsDir) {
190
+ const skillsDir = path.join(projectPath, target.config.skillsDir);
191
+ if (existsSync(skillsDir)) {
192
+ await fs.rm(skillsDir, { recursive: true, force: true });
193
+ }
194
+ }
195
+
196
+ // 4. Clear hooks directory (in configDir)
188
197
  const hooksDir = path.join(projectPath, target.config.configDir, 'hooks');
189
198
  if (existsSync(hooksDir)) {
190
199
  const files = await fs.readdir(hooksDir);
@@ -193,7 +202,7 @@ export class FlowExecutor {
193
202
  }
194
203
  }
195
204
 
196
- // 4. Clear MCP configuration using target config
205
+ // 5. Clear MCP configuration using target config
197
206
  const configPath = path.join(projectPath, target.config.configFile);
198
207
  const mcpPath = target.config.mcpConfigPath;
199
208
 
@@ -206,7 +215,7 @@ export class FlowExecutor {
206
215
  }
207
216
  }
208
217
 
209
- // 5. Clear rules file if target has one defined (for targets like OpenCode)
218
+ // 6. Clear rules file if target has one defined (for targets like OpenCode)
210
219
  // Claude Code puts AGENTS.md in agents directory, handled above
211
220
  if (target.config.rulesFile) {
212
221
  const rulesPath = path.join(projectPath, target.config.rulesFile);
@@ -215,7 +224,7 @@ export class FlowExecutor {
215
224
  }
216
225
  }
217
226
 
218
- // 6. Clear single files (output styles) - currently none
227
+ // 7. Clear single files (output styles) - currently none
219
228
  // These would be in the configDir if we had any
220
229
  const singleFiles: string[] = [];
221
230
  for (const fileName of singleFiles) {
@@ -225,7 +234,7 @@ export class FlowExecutor {
225
234
  }
226
235
  }
227
236
 
228
- // 7. Clean up any Flow-created files in project root (legacy bug cleanup)
237
+ // 8. Clean up any Flow-created files in project root (legacy bug cleanup)
229
238
  // This handles files that were incorrectly created in project root
230
239
  const legacySingleFiles = ['silent.md']; // Keep for cleanup of legacy installations
231
240
  for (const fileName of legacySingleFiles) {
@@ -29,6 +29,7 @@ export class TemplateLoader {
29
29
  const templates: FlowTemplates = {
30
30
  agents: [],
31
31
  commands: [],
32
+ skills: [],
32
33
  rules: undefined,
33
34
  mcpServers: [],
34
35
  hooks: [],
@@ -47,6 +48,12 @@ export class TemplateLoader {
47
48
  templates.commands = await this.loadCommands(commandsDir);
48
49
  }
49
50
 
51
+ // Load skills (skills/<domain>/SKILL.md structure)
52
+ const skillsDir = path.join(this.assetsDir, 'skills');
53
+ if (existsSync(skillsDir)) {
54
+ templates.skills = await this.loadSkills(skillsDir);
55
+ }
56
+
50
57
  // Load rules (check multiple possible locations)
51
58
  const rulesLocations = [
52
59
  path.join(this.assetsDir, 'rules', 'AGENTS.md'),
@@ -115,6 +122,34 @@ export class TemplateLoader {
115
122
  return commands;
116
123
  }
117
124
 
125
+ /**
126
+ * Load skills from directory
127
+ * Skills are stored as <domain>/SKILL.md subdirectories
128
+ */
129
+ private async loadSkills(skillsDir: string): Promise<Array<{ name: string; content: string }>> {
130
+ const skills = [];
131
+ const domains = await fs.readdir(skillsDir);
132
+
133
+ for (const domain of domains) {
134
+ const domainPath = path.join(skillsDir, domain);
135
+ const stat = await fs.stat(domainPath);
136
+
137
+ if (!stat.isDirectory()) {
138
+ continue;
139
+ }
140
+
141
+ // Look for SKILL.md in each domain directory
142
+ const skillFile = path.join(domainPath, 'SKILL.md');
143
+ if (existsSync(skillFile)) {
144
+ const content = await fs.readFile(skillFile, 'utf-8');
145
+ // Name includes subdirectory: "auth/SKILL.md"
146
+ skills.push({ name: `${domain}/SKILL.md`, content });
147
+ }
148
+ }
149
+
150
+ return skills;
151
+ }
152
+
118
153
  /**
119
154
  * Load MCP servers configuration
120
155
  */
@@ -41,6 +41,7 @@ export const claudeCodeTarget: Target = {
41
41
  rulesFile: undefined, // Rules are included in agent files
42
42
  outputStylesDir: undefined, // Output styles are included in agent files
43
43
  slashCommandsDir: '.claude/commands',
44
+ skillsDir: '.claude/skills',
44
45
  installation: {
45
46
  createAgentDir: true,
46
47
  createConfigFile: true,
@@ -35,6 +35,8 @@ export interface TargetConfig {
35
35
  outputStylesDir?: string;
36
36
  /** Slash commands directory (optional, relative to project root) */
37
37
  slashCommandsDir?: string;
38
+ /** Skills directory (optional, relative to project root) */
39
+ skillsDir?: string;
38
40
  /** Installation-specific configuration */
39
41
  installation: {
40
42
  /** Whether to create the agent directory */
@@ -137,4 +139,7 @@ export interface Target {
137
139
 
138
140
  /** Setup slash commands for this target (optional - implement if target supports slash commands) */
139
141
  setupSlashCommands?(cwd: string, options: CommonOptions): Promise<SetupResult>;
142
+
143
+ /** Setup skills for this target (optional - implement if target supports skills) */
144
+ setupSkills?(cwd: string, options: CommonOptions): Promise<SetupResult>;
140
145
  }