skiller 0.7.1 → 0.7.3

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.
@@ -38,7 +38,10 @@ exports.syncMdcToSkillMd = syncMdcToSkillMd;
38
38
  exports.discoverSkills = discoverSkills;
39
39
  exports.getSkillsGitignorePaths = getSkillsGitignorePaths;
40
40
  exports.propagateSkills = propagateSkills;
41
+ exports.migrateRulesToSkills = migrateRulesToSkills;
41
42
  exports.copySkillFoldersFromRules = copySkillFoldersFromRules;
43
+ exports.copyMdcFilesFromRules = copyMdcFilesFromRules;
44
+ exports.deleteRulesDir = deleteRulesDir;
42
45
  const path = __importStar(require("path"));
43
46
  const fs = __importStar(require("fs/promises"));
44
47
  const yaml = __importStar(require("js-yaml"));
@@ -140,8 +143,15 @@ async function syncMdcToSkillMd(skillsDir, verbose, dryRun) {
140
143
  }
141
144
  try {
142
145
  if (siblingMdcContent !== null && skillMdContent === null) {
143
- // Case 1: Sibling .mdc exists but no SKILL.md → generate SKILL.md with @reference
146
+ // Case 1: Sibling .mdc exists but no SKILL.md
144
147
  const { frontmatter: mdcFrontmatter } = (0, FrontmatterParser_1.parseFrontmatter)(siblingMdcContent);
148
+ // Skip SKILL.md generation for .mdc files with alwaysApply: true
149
+ // These are Cursor-style rules, not Claude Code skills
150
+ if (mdcFrontmatter?.alwaysApply === true) {
151
+ (0, constants_1.logVerboseInfo)(`Skipping SKILL.md generation for ${skillName} (alwaysApply rule)`, verbose, dryRun);
152
+ continue;
153
+ }
154
+ // Generate SKILL.md with @reference (absolute path)
145
155
  const skillFrontmatter = {
146
156
  name: skillName,
147
157
  description: mdcFrontmatter?.description || `Skill: ${skillName}`,
@@ -150,14 +160,14 @@ async function syncMdcToSkillMd(skillsDir, verbose, dryRun) {
150
160
  ${yaml.dump(skillFrontmatter, { lineWidth: -1, noRefs: true }).trim()}
151
161
  ---
152
162
 
153
- @./${skillName}.mdc
163
+ @.claude/skills/${skillName}/${skillName}.mdc
154
164
  `;
155
165
  if (dryRun) {
156
166
  (0, constants_1.logVerboseInfo)(`DRY RUN: Would generate ${skillName}/SKILL.md with @reference`, verbose, dryRun);
157
167
  }
158
168
  else {
159
169
  await fs.writeFile(skillMdPath, newSkillMd, 'utf8');
160
- (0, constants_1.logVerboseInfo)(`Generated ${skillName}/SKILL.md with @./${skillName}.mdc reference`, verbose, dryRun);
170
+ (0, constants_1.logVerboseInfo)(`Generated ${skillName}/SKILL.md with @.claude/skills/${skillName}/${skillName}.mdc reference`, verbose, dryRun);
161
171
  }
162
172
  synced.push(skillName);
163
173
  }
@@ -167,22 +177,32 @@ ${yaml.dump(skillFrontmatter, { lineWidth: -1, noRefs: true }).trim()}
167
177
  const refCheck = isReferenceBody(skillBody);
168
178
  if (refCheck.isReference) {
169
179
  // Case 2: SKILL.md is @reference → source file is truth
170
- if (refCheck.referencePath === `./${skillName}.mdc`) {
180
+ // Check for both relative and absolute sibling reference patterns
181
+ const isRelativeSiblingRef = refCheck.referencePath === `./${skillName}.mdc`;
182
+ const isAbsoluteSiblingRef = refCheck.referencePath ===
183
+ `.claude/skills/${skillName}/${skillName}.mdc`;
184
+ if (isRelativeSiblingRef || isAbsoluteSiblingRef) {
171
185
  // Sibling reference pattern - validate/update frontmatter if .mdc changed
172
186
  if (siblingMdcContent !== null) {
173
187
  const { frontmatter: mdcFrontmatter } = (0, FrontmatterParser_1.parseFrontmatter)(siblingMdcContent);
174
188
  // Update SKILL.md frontmatter if description changed in .mdc
175
- if (mdcFrontmatter?.description &&
176
- mdcFrontmatter.description !== skillFrontmatter?.description) {
189
+ // Also migrate from relative to absolute path if needed
190
+ const needsUpdate = (mdcFrontmatter?.description &&
191
+ mdcFrontmatter.description !==
192
+ skillFrontmatter?.description) ||
193
+ isRelativeSiblingRef; // Migrate old relative refs to absolute
194
+ if (needsUpdate) {
177
195
  const newFrontmatter = {
178
196
  name: skillFrontmatter?.name || skillName,
179
- description: mdcFrontmatter.description,
197
+ description: mdcFrontmatter?.description ||
198
+ skillFrontmatter?.description ||
199
+ `Skill: ${skillName}`,
180
200
  };
181
201
  const newSkillMd = `---
182
202
  ${yaml.dump(newFrontmatter, { lineWidth: -1, noRefs: true }).trim()}
183
203
  ---
184
204
 
185
- @./${skillName}.mdc
205
+ @.claude/skills/${skillName}/${skillName}.mdc
186
206
  `;
187
207
  if (dryRun) {
188
208
  (0, constants_1.logVerboseInfo)(`DRY RUN: Would update ${skillName}/SKILL.md frontmatter from .mdc`, verbose, dryRun);
@@ -215,8 +235,7 @@ ${yaml.dump(newFrontmatter, { lineWidth: -1, noRefs: true }).trim()}
215
235
  const { frontmatter: refFrontmatter, body: refBody } = (0, FrontmatterParser_1.parseFrontmatter)(referencedContent);
216
236
  // Create sibling .mdc with the content
217
237
  let mdcContent;
218
- if (refFrontmatter &&
219
- Object.keys(refFrontmatter).length > 0) {
238
+ if (refFrontmatter && Object.keys(refFrontmatter).length > 0) {
220
239
  const mdcFrontmatterData = {};
221
240
  if (refFrontmatter.description) {
222
241
  mdcFrontmatterData.description = refFrontmatter.description;
@@ -242,7 +261,7 @@ ${refBody}
242
261
  else {
243
262
  mdcContent = referencedContent;
244
263
  }
245
- // Update SKILL.md to point to sibling .mdc
264
+ // Update SKILL.md to point to sibling .mdc (absolute path)
246
265
  const newFrontmatter = {
247
266
  name: skillFrontmatter?.name || skillName,
248
267
  description: refFrontmatter?.description ||
@@ -253,7 +272,7 @@ ${refBody}
253
272
  ${yaml.dump(newFrontmatter, { lineWidth: -1, noRefs: true }).trim()}
254
273
  ---
255
274
 
256
- @./${skillName}.mdc
275
+ @.claude/skills/${skillName}/${skillName}.mdc
257
276
  `;
258
277
  if (dryRun) {
259
278
  (0, constants_1.logVerboseInfo)(`DRY RUN: Would migrate ${skillName} from ${refCheck.referencePath} to sibling pattern`, verbose, dryRun);
@@ -290,7 +309,7 @@ ${skillBody}
290
309
  else {
291
310
  mdcContent = skillBody;
292
311
  }
293
- // Update SKILL.md to @reference
312
+ // Update SKILL.md to @reference (absolute path)
294
313
  const newSkillFrontmatter = {
295
314
  name: skillFrontmatter?.name || skillName,
296
315
  description: skillFrontmatter?.description || `Skill: ${skillName}`,
@@ -299,7 +318,7 @@ ${skillBody}
299
318
  ${yaml.dump(newSkillFrontmatter, { lineWidth: -1, noRefs: true }).trim()}
300
319
  ---
301
320
 
302
- @./${skillName}.mdc
321
+ @.claude/skills/${skillName}/${skillName}.mdc
303
322
  `;
304
323
  if (dryRun) {
305
324
  (0, constants_1.logVerboseInfo)(`DRY RUN: Would generate ${skillName}/${skillName}.mdc and update SKILL.md`, verbose, dryRun);
@@ -428,6 +447,27 @@ async function findSkillFoldersInRules(dir, depth = 0) {
428
447
  }
429
448
  return skillFolders;
430
449
  }
450
+ /**
451
+ * Migrates all content from .claude/rules to .claude/skills and deletes the rules directory.
452
+ * This is the main entry point for rules migration - it only processes if rules directory exists.
453
+ */
454
+ async function migrateRulesToSkills(skillerDir, verbose, dryRun) {
455
+ const rulesDir = path.join(skillerDir, 'rules');
456
+ // Check if rules directory exists - early exit if not
457
+ try {
458
+ await fs.access(rulesDir);
459
+ }
460
+ catch {
461
+ // No rules directory - nothing to migrate
462
+ return;
463
+ }
464
+ // Copy skill folders (folders with SKILL.md)
465
+ await copySkillFoldersFromRules(skillerDir, verbose, dryRun);
466
+ // Copy standalone .mdc files
467
+ await copyMdcFilesFromRules(skillerDir, verbose, dryRun);
468
+ // Delete the rules directory after migration
469
+ await deleteRulesDir(skillerDir, verbose, dryRun);
470
+ }
431
471
  /**
432
472
  * Copies skill folders (folders containing SKILL.md) from .claude/rules to .claude/skills.
433
473
  * This allows users to organize skills in the rules directory and have them automatically
@@ -465,3 +505,103 @@ async function copySkillFoldersFromRules(skillerDir, verbose, dryRun) {
465
505
  }
466
506
  (0, constants_1.logVerboseInfo)(`Copied ${skillFolders.length} skill folder(s) from rules to skills`, verbose, dryRun);
467
507
  }
508
+ /**
509
+ * Copies standalone .mdc files from .claude/rules to .claude/skills/name/name.mdc.
510
+ * These are rule files (not skill folders) that should be available in the skills directory.
511
+ * No SKILL.md is generated - these remain as .mdc files only.
512
+ */
513
+ async function copyMdcFilesFromRules(skillerDir, verbose, dryRun) {
514
+ const rulesDir = path.join(skillerDir, 'rules');
515
+ const skillsDir = path.join(skillerDir, 'skills');
516
+ const copiedNames = [];
517
+ // Check if rules directory exists
518
+ try {
519
+ await fs.access(rulesDir);
520
+ }
521
+ catch {
522
+ return copiedNames;
523
+ }
524
+ const entries = await fs.readdir(rulesDir, { withFileTypes: true });
525
+ // Find .mdc files at rules root (not in subdirectories)
526
+ const mdcFiles = entries.filter((e) => e.isFile() && e.name.endsWith('.mdc'));
527
+ for (const mdcFile of mdcFiles) {
528
+ const skillName = path.basename(mdcFile.name, '.mdc');
529
+ const sourcePath = path.join(rulesDir, mdcFile.name);
530
+ const targetDir = path.join(skillsDir, skillName);
531
+ const targetPath = path.join(targetDir, mdcFile.name);
532
+ try {
533
+ const content = await fs.readFile(sourcePath, 'utf8');
534
+ // Parse and clean frontmatter - remove globs and alwaysApply: false
535
+ const { frontmatter, body } = (0, FrontmatterParser_1.parseFrontmatter)(content);
536
+ let cleanedContent;
537
+ if (frontmatter && Object.keys(frontmatter).length > 0) {
538
+ const cleanedFrontmatter = {};
539
+ // Only keep description and alwaysApply: true
540
+ if (frontmatter.description) {
541
+ cleanedFrontmatter.description = frontmatter.description;
542
+ }
543
+ if (frontmatter.alwaysApply === true) {
544
+ cleanedFrontmatter.alwaysApply = true;
545
+ }
546
+ // Note: globs and alwaysApply: false are intentionally omitted
547
+ if (Object.keys(cleanedFrontmatter).length > 0) {
548
+ cleanedContent = `---
549
+ ${yaml.dump(cleanedFrontmatter, { lineWidth: -1, noRefs: true }).trim()}
550
+ ---
551
+
552
+ ${body}
553
+ `;
554
+ }
555
+ else {
556
+ cleanedContent = body;
557
+ }
558
+ }
559
+ else {
560
+ cleanedContent = content;
561
+ }
562
+ if (dryRun) {
563
+ (0, constants_1.logVerboseInfo)(`DRY RUN: Would copy ${mdcFile.name} from rules to skills/${skillName}/${mdcFile.name}`, verbose, dryRun);
564
+ }
565
+ else {
566
+ await fs.mkdir(targetDir, { recursive: true });
567
+ await fs.writeFile(targetPath, cleanedContent, 'utf8');
568
+ (0, constants_1.logVerboseInfo)(`Copied ${mdcFile.name} from rules to skills/${skillName}/${mdcFile.name}`, verbose, dryRun);
569
+ }
570
+ copiedNames.push(skillName);
571
+ }
572
+ catch (err) {
573
+ (0, constants_1.logWarn)(`Failed to copy ${mdcFile.name}: ${err.message}`, dryRun);
574
+ }
575
+ }
576
+ if (copiedNames.length > 0) {
577
+ (0, constants_1.logVerboseInfo)(`Copied ${copiedNames.length} .mdc file(s) from rules to skills`, verbose, dryRun);
578
+ }
579
+ return copiedNames;
580
+ }
581
+ /**
582
+ * Deletes the .claude/rules directory after content has been migrated to .claude/skills.
583
+ * This completes the migration from the old rules-based structure to the new skills-based structure.
584
+ */
585
+ async function deleteRulesDir(skillerDir, verbose, dryRun) {
586
+ const rulesDir = path.join(skillerDir, 'rules');
587
+ // Check if rules directory exists
588
+ try {
589
+ await fs.access(rulesDir);
590
+ }
591
+ catch {
592
+ return false; // No rules directory to delete
593
+ }
594
+ if (dryRun) {
595
+ (0, constants_1.logVerboseInfo)(`DRY RUN: Would delete .claude/rules directory after migration`, verbose, dryRun);
596
+ return true;
597
+ }
598
+ try {
599
+ await fs.rm(rulesDir, { recursive: true, force: true });
600
+ (0, constants_1.logVerboseInfo)(`Deleted .claude/rules directory after migration to .claude/skills`, verbose, dryRun);
601
+ return true;
602
+ }
603
+ catch (err) {
604
+ (0, constants_1.logWarn)(`Failed to delete .claude/rules: ${err.message}`, dryRun);
605
+ return false;
606
+ }
607
+ }
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.hasSkillMd = hasSkillMd;
37
+ exports.hasAlwaysApplyMdc = hasAlwaysApplyMdc;
37
38
  exports.isGroupingDir = isGroupingDir;
38
39
  exports.walkSkillsTree = walkSkillsTree;
39
40
  exports.formatValidationWarnings = formatValidationWarnings;
@@ -42,6 +43,7 @@ exports.copySkillsDirectoryWithTransform = copySkillsDirectoryWithTransform;
42
43
  const path = __importStar(require("path"));
43
44
  const fs = __importStar(require("fs/promises"));
44
45
  const constants_1 = require("../constants");
46
+ const FrontmatterParser_1 = require("./FrontmatterParser");
45
47
  /**
46
48
  * Checks if a directory contains a SKILL.md file.
47
49
  */
@@ -55,6 +57,30 @@ async function hasSkillMd(dirPath) {
55
57
  return false;
56
58
  }
57
59
  }
60
+ /**
61
+ * Checks if a directory contains an .mdc file with alwaysApply: true.
62
+ * These directories are valid without SKILL.md since alwaysApply rules
63
+ * are Cursor-style rules, not Claude Code skills.
64
+ */
65
+ async function hasAlwaysApplyMdc(dirPath) {
66
+ try {
67
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
68
+ for (const entry of entries) {
69
+ if (entry.isFile() && entry.name.endsWith('.mdc')) {
70
+ const mdcPath = path.join(dirPath, entry.name);
71
+ const content = await fs.readFile(mdcPath, 'utf8');
72
+ const { frontmatter } = (0, FrontmatterParser_1.parseFrontmatter)(content);
73
+ if (frontmatter?.alwaysApply === true) {
74
+ return true;
75
+ }
76
+ }
77
+ }
78
+ return false;
79
+ }
80
+ catch {
81
+ return false;
82
+ }
83
+ }
58
84
  /**
59
85
  * Checks if a directory is a grouping directory (contains subdirectories with SKILL.md).
60
86
  */
@@ -121,8 +147,12 @@ async function walkSkillsTree(root) {
121
147
  await walk(entryPath, entryRelativePath, depth + 1);
122
148
  }
123
149
  else {
124
- // This is neither a skill nor a grouping directory - warn about it
125
- warnings.push(`Directory '${entryRelativePath}' in .claude/skills has no SKILL.md and contains no sub-skills. It may be malformed or stray.`);
150
+ // Check if this is a valid alwaysApply directory (no SKILL.md expected)
151
+ const hasAlwaysApply = await hasAlwaysApplyMdc(entryPath);
152
+ if (!hasAlwaysApply) {
153
+ // This is neither a skill nor a grouping directory - warn about it
154
+ warnings.push(`Directory '${entryRelativePath}' in .claude/skills has no SKILL.md and contains no sub-skills. It may be malformed or stray.`);
155
+ }
126
156
  }
127
157
  }
128
158
  }
package/dist/lib.js CHANGED
@@ -102,11 +102,11 @@ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cli
102
102
  (0, constants_1.logVerbose)(`Selected ${selectedAgents.length} agents: ${selectedAgents.map((a) => a.getName()).join(', ')}`, verbose);
103
103
  // Propagate skills (or cleanup if disabled) - do this for each nested directory
104
104
  const skillsEnabledResolved = resolveSkillsEnabled(skillsEnabled, rootConfig.skills?.enabled);
105
- const { propagateSkills, copySkillFoldersFromRules } = await Promise.resolve().then(() => __importStar(require('./core/SkillsProcessor')));
106
- // Copy skill folders from rules to skills
105
+ const { propagateSkills, migrateRulesToSkills } = await Promise.resolve().then(() => __importStar(require('./core/SkillsProcessor')));
106
+ // Migrate content from .claude/rules to .claude/skills (only if rules exists)
107
107
  if (skillsEnabledResolved) {
108
108
  for (const configEntry of hierarchicalConfigs) {
109
- await copySkillFoldersFromRules(configEntry.skillerDir, verbose, dryRun);
109
+ await migrateRulesToSkills(configEntry.skillerDir, verbose, dryRun);
110
110
  }
111
111
  }
112
112
  // Propagate skills for each nested .claude directory (or cleanup if disabled)
@@ -130,10 +130,10 @@ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cli
130
130
  (0, constants_1.logVerbose)(`Selected ${selectedAgents.length} agents: ${selectedAgents.map((a) => a.getName()).join(', ')}`, verbose);
131
131
  // Propagate skills (or cleanup if disabled)
132
132
  const skillsEnabledResolved = resolveSkillsEnabled(skillsEnabled, singleConfig.config.skills?.enabled);
133
- const { propagateSkills, copySkillFoldersFromRules } = await Promise.resolve().then(() => __importStar(require('./core/SkillsProcessor')));
134
- // Copy skill folders from rules to skills
133
+ const { propagateSkills, migrateRulesToSkills } = await Promise.resolve().then(() => __importStar(require('./core/SkillsProcessor')));
134
+ // Migrate content from .claude/rules to .claude/skills (only if rules exists)
135
135
  if (skillsEnabledResolved) {
136
- await copySkillFoldersFromRules(singleConfig.skillerDir, verbose, dryRun);
136
+ await migrateRulesToSkills(singleConfig.skillerDir, verbose, dryRun);
137
137
  }
138
138
  // Always call propagateSkills - it handles cleanup when disabled
139
139
  await propagateSkills(projectRoot, selectedAgents, skillsEnabledResolved, verbose, dryRun, singleConfig.skillerDir);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skiller",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
4
4
  "description": "Skiller — apply the same rules to all coding agents",
5
5
  "main": "dist/lib.js",
6
6
  "publishConfig": {