@sylphx/flow 2.16.2 → 2.18.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,17 @@
1
1
  # @sylphx/flow
2
2
 
3
+ ## 2.18.0 (2025-12-18)
4
+
5
+ ### ✨ Features
6
+
7
+ - **cli:** show skills count in attach summary ([a02c422](https://github.com/SylphxAI/flow/commit/a02c42239e79af1cfc891bb4554e9fac7c2a7f9b))
8
+
9
+ ## 2.17.0 (2025-12-18)
10
+
11
+ ### ✨ Features
12
+
13
+ - **skills:** implement skills loading and attach pipeline ([8d720b8](https://github.com/SylphxAI/flow/commit/8d720b8118ffe19e14d50aaacd9282bd5d42e702))
14
+
3
15
  ## 2.16.2 (2025-12-17)
4
16
 
5
17
  ### 🐛 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.18.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": {
@@ -15,7 +15,7 @@ import { GlobalConfigService } from '../../services/global-config.js';
15
15
  import { TargetInstaller } from '../../services/target-installer.js';
16
16
  import type { RunCommandOptions } from '../../types.js';
17
17
  import { extractAgentInstructions, loadAgentContent } from '../../utils/agent-enhancer.js';
18
- import { showHeader } from '../../utils/display/banner.js';
18
+ import { showAttachSummary, showHeader } from '../../utils/display/banner.js';
19
19
  import { CLIError } from '../../utils/error-handler.js';
20
20
  import { UserCancelledError } from '../../utils/errors.js';
21
21
  import { ensureTargetInstalled, promptForTargetSelection } from '../../utils/target-selection.js';
@@ -263,6 +263,9 @@ export async function executeFlowV2(
263
263
  merge: options.merge || false,
264
264
  });
265
265
 
266
+ // Show attach summary
267
+ showAttachSummary(attachResult);
268
+
266
269
  const targetId = selectedTargetId;
267
270
 
268
271
  // Provider selection (Claude Code only, silent unless prompting)
@@ -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: {
@@ -55,7 +55,13 @@ export class FlowExecutor {
55
55
  async execute(
56
56
  projectPath: string,
57
57
  options: FlowExecutorOptions = {}
58
- ): Promise<{ joined: boolean; agents?: number; commands?: number; mcp?: number }> {
58
+ ): Promise<{
59
+ joined: boolean;
60
+ agents?: number;
61
+ commands?: number;
62
+ skills?: number;
63
+ mcp?: number;
64
+ }> {
59
65
  // Initialize Flow directories
60
66
  await this.projectManager.initialize();
61
67
 
@@ -137,6 +143,7 @@ export class FlowExecutor {
137
143
  joined: false,
138
144
  agents: attachResult.agentsAdded.length,
139
145
  commands: attachResult.commandsAdded.length,
146
+ skills: attachResult.skillsAdded.length,
140
147
  mcp: attachResult.mcpServersAdded.length,
141
148
  };
142
149
  }
@@ -184,7 +191,15 @@ export class FlowExecutor {
184
191
  }
185
192
  }
186
193
 
187
- // 3. Clear hooks directory (in configDir)
194
+ // 3. Clear skills directory (if target supports skills)
195
+ if (target.config.skillsDir) {
196
+ const skillsDir = path.join(projectPath, target.config.skillsDir);
197
+ if (existsSync(skillsDir)) {
198
+ await fs.rm(skillsDir, { recursive: true, force: true });
199
+ }
200
+ }
201
+
202
+ // 4. Clear hooks directory (in configDir)
188
203
  const hooksDir = path.join(projectPath, target.config.configDir, 'hooks');
189
204
  if (existsSync(hooksDir)) {
190
205
  const files = await fs.readdir(hooksDir);
@@ -193,7 +208,7 @@ export class FlowExecutor {
193
208
  }
194
209
  }
195
210
 
196
- // 4. Clear MCP configuration using target config
211
+ // 5. Clear MCP configuration using target config
197
212
  const configPath = path.join(projectPath, target.config.configFile);
198
213
  const mcpPath = target.config.mcpConfigPath;
199
214
 
@@ -206,7 +221,7 @@ export class FlowExecutor {
206
221
  }
207
222
  }
208
223
 
209
- // 5. Clear rules file if target has one defined (for targets like OpenCode)
224
+ // 6. Clear rules file if target has one defined (for targets like OpenCode)
210
225
  // Claude Code puts AGENTS.md in agents directory, handled above
211
226
  if (target.config.rulesFile) {
212
227
  const rulesPath = path.join(projectPath, target.config.rulesFile);
@@ -215,7 +230,7 @@ export class FlowExecutor {
215
230
  }
216
231
  }
217
232
 
218
- // 6. Clear single files (output styles) - currently none
233
+ // 7. Clear single files (output styles) - currently none
219
234
  // These would be in the configDir if we had any
220
235
  const singleFiles: string[] = [];
221
236
  for (const fileName of singleFiles) {
@@ -225,7 +240,7 @@ export class FlowExecutor {
225
240
  }
226
241
  }
227
242
 
228
- // 7. Clean up any Flow-created files in project root (legacy bug cleanup)
243
+ // 8. Clean up any Flow-created files in project root (legacy bug cleanup)
229
244
  // This handles files that were incorrectly created in project root
230
245
  const legacySingleFiles = ['silent.md']; // Keep for cleanup of legacy installations
231
246
  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
  }
@@ -12,6 +12,40 @@ export function showHeader(version: string, target: string): void {
12
12
  console.log(`\n${chalk.cyan('flow')} ${chalk.dim(version)} ${chalk.dim('→')} ${target}\n`);
13
13
  }
14
14
 
15
+ /**
16
+ * Show attach summary: ✓ Attached {n} agents, {n} commands, {n} skills, {n} MCP
17
+ */
18
+ export function showAttachSummary(result: {
19
+ joined: boolean;
20
+ agents?: number;
21
+ commands?: number;
22
+ skills?: number;
23
+ mcp?: number;
24
+ }): void {
25
+ if (result.joined) {
26
+ // Joining existing session - no summary needed
27
+ return;
28
+ }
29
+
30
+ const parts: string[] = [];
31
+ if (result.agents && result.agents > 0) {
32
+ parts.push(`${result.agents} agents`);
33
+ }
34
+ if (result.commands && result.commands > 0) {
35
+ parts.push(`${result.commands} commands`);
36
+ }
37
+ if (result.skills && result.skills > 0) {
38
+ parts.push(`${result.skills} skills`);
39
+ }
40
+ if (result.mcp && result.mcp > 0) {
41
+ parts.push(`${result.mcp} MCP`);
42
+ }
43
+
44
+ if (parts.length > 0) {
45
+ console.log(`${chalk.green('✓')} Attached ${parts.join(', ')}`);
46
+ }
47
+ }
48
+
15
49
  /**
16
50
  * @deprecated Use showHeader instead
17
51
  */