skiller 0.7.0 → 0.7.1

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.
@@ -139,10 +139,6 @@ function extractFrontmatter(parsed, body) {
139
139
  if (typeof parsed.alwaysApply === 'boolean') {
140
140
  frontmatter.alwaysApply = parsed.alwaysApply;
141
141
  }
142
- // Extract synced (for skill sync detection)
143
- if (typeof parsed.synced === 'boolean') {
144
- frontmatter.synced = parsed.synced;
145
- }
146
142
  // Extract name (for SKILL.md)
147
143
  if (typeof parsed.name === 'string') {
148
144
  frontmatter.name = parsed.name;
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.isReferenceBody = isReferenceBody;
36
37
  exports.syncMdcToSkillMd = syncMdcToSkillMd;
37
38
  exports.discoverSkills = discoverSkills;
38
39
  exports.getSkillsGitignorePaths = getSkillsGitignorePaths;
@@ -44,16 +45,34 @@ const yaml = __importStar(require("js-yaml"));
44
45
  const constants_1 = require("../constants");
45
46
  const SkillsUtils_1 = require("./SkillsUtils");
46
47
  const FrontmatterParser_1 = require("./FrontmatterParser");
48
+ /**
49
+ * Check if SKILL.md body is just a reference (single non-empty line starting with @).
50
+ * This replaces the previous synced: true frontmatter detection.
51
+ */
52
+ function isReferenceBody(body) {
53
+ const lines = body.split('\n').filter((line) => line.trim().length > 0);
54
+ if (lines.length === 1 && lines[0].trim().startsWith('@')) {
55
+ return {
56
+ isReference: true,
57
+ referencePath: lines[0].trim().slice(1), // Remove @ prefix
58
+ };
59
+ }
60
+ return { isReference: false };
61
+ }
47
62
  /**
48
63
  * Bidirectional sync between .mdc files and SKILL.md in the skills directory.
49
64
  *
50
- * Sync logic:
51
- * 1. If .mdc exists but no SKILL.md → generate SKILL.md with synced: true
52
- * 2. If SKILL.md has synced: true + .mdc exists regenerate SKILL.md from .mdc
53
- * 3. If SKILL.md exists WITHOUT synced: true → generate .mdc from SKILL.md, add synced: true
65
+ * Sync logic (using @reference pattern instead of synced: true):
66
+ * 1. If sibling .mdc exists (name/name.mdc) but no SKILL.md → generate SKILL.md with @./name.mdc
67
+ * 2. If SKILL.md body is @reference referenced file is source of truth
68
+ * 3. If SKILL.md has full content → generate sibling .mdc, update SKILL.md to @./name.mdc
54
69
  *
55
70
  * File structure:
56
- * - .claude/skills/foo.mdc → .claude/skills/foo/SKILL.md
71
+ * - .claude/skills/foo/foo.mdc → .claude/skills/foo/SKILL.md (with @./foo.mdc body)
72
+ *
73
+ * Backward compatibility:
74
+ * - .claude/skills/foo.mdc at root → migrates to sibling pattern
75
+ * - @.claude/rules/name.mdc → recognized as reference (pre-0.7 pattern)
57
76
  */
58
77
  async function syncMdcToSkillMd(skillsDir, verbose, dryRun) {
59
78
  const synced = [];
@@ -66,15 +85,51 @@ async function syncMdcToSkillMd(skillsDir, verbose, dryRun) {
66
85
  return { synced, warnings };
67
86
  }
68
87
  const entries = await fs.readdir(skillsDir, { withFileTypes: true });
69
- // Find .mdc files at the skills root level
70
- const mdcFiles = entries.filter((e) => e.isFile() && e.name.endsWith('.mdc'));
71
- const skillFolders = entries.filter((e) => e.isDirectory());
72
- // Case 1 & 2: Process .mdc files
73
- for (const mdcFile of mdcFiles) {
88
+ // Find .mdc files at skills root (for backward compatibility/migration)
89
+ const rootMdcFiles = entries.filter((e) => e.isFile() && e.name.endsWith('.mdc'));
90
+ // First, migrate any root .mdc files to sibling pattern
91
+ for (const mdcFile of rootMdcFiles) {
74
92
  const skillName = path.basename(mdcFile.name, '.mdc');
75
- const mdcPath = path.join(skillsDir, mdcFile.name);
93
+ const rootMdcPath = path.join(skillsDir, mdcFile.name);
94
+ const skillFolderPath = path.join(skillsDir, skillName);
95
+ const siblingMdcPath = path.join(skillFolderPath, mdcFile.name);
96
+ try {
97
+ // Create skill folder if needed
98
+ if (!dryRun) {
99
+ await fs.mkdir(skillFolderPath, { recursive: true });
100
+ }
101
+ // Move .mdc to sibling location
102
+ if (dryRun) {
103
+ (0, constants_1.logVerboseInfo)(`DRY RUN: Would migrate ${mdcFile.name} to ${skillName}/${mdcFile.name}`, verbose, dryRun);
104
+ }
105
+ else {
106
+ const mdcContent = await fs.readFile(rootMdcPath, 'utf8');
107
+ await fs.writeFile(siblingMdcPath, mdcContent, 'utf8');
108
+ await fs.unlink(rootMdcPath);
109
+ (0, constants_1.logVerboseInfo)(`Migrated ${mdcFile.name} to ${skillName}/${mdcFile.name}`, verbose, dryRun);
110
+ }
111
+ }
112
+ catch (err) {
113
+ warnings.push(`Failed to migrate ${skillName}.mdc: ${err.message}`);
114
+ }
115
+ }
116
+ // Re-read entries after migration
117
+ const updatedEntries = await fs.readdir(skillsDir, { withFileTypes: true });
118
+ const updatedSkillFolders = updatedEntries.filter((e) => e.isDirectory());
119
+ // Process skill folders
120
+ for (const folder of updatedSkillFolders) {
121
+ const skillName = folder.name;
76
122
  const skillFolderPath = path.join(skillsDir, skillName);
77
123
  const skillMdPath = path.join(skillFolderPath, constants_1.SKILL_MD_FILENAME);
124
+ const siblingMdcPath = path.join(skillFolderPath, `${skillName}.mdc`);
125
+ // Check if sibling .mdc exists
126
+ let siblingMdcContent = null;
127
+ try {
128
+ siblingMdcContent = await fs.readFile(siblingMdcPath, 'utf8');
129
+ }
130
+ catch {
131
+ // No sibling .mdc
132
+ }
78
133
  // Check if SKILL.md exists
79
134
  let skillMdContent = null;
80
135
  try {
@@ -84,175 +139,180 @@ async function syncMdcToSkillMd(skillsDir, verbose, dryRun) {
84
139
  // No SKILL.md
85
140
  }
86
141
  try {
87
- const mdcContent = await fs.readFile(mdcPath, 'utf8');
88
- const { frontmatter: mdcFrontmatter, body: mdcBody } = (0, FrontmatterParser_1.parseFrontmatter)(mdcContent);
89
- if (skillMdContent === null) {
90
- // Case 1: No SKILL.md exists → generate from .mdc with synced: true
142
+ if (siblingMdcContent !== null && skillMdContent === null) {
143
+ // Case 1: Sibling .mdc exists but no SKILL.md → generate SKILL.md with @reference
144
+ const { frontmatter: mdcFrontmatter } = (0, FrontmatterParser_1.parseFrontmatter)(siblingMdcContent);
91
145
  const skillFrontmatter = {
92
146
  name: skillName,
93
147
  description: mdcFrontmatter?.description || `Skill: ${skillName}`,
94
- synced: true,
95
148
  };
96
149
  const newSkillMd = `---
97
150
  ${yaml.dump(skillFrontmatter, { lineWidth: -1, noRefs: true }).trim()}
98
151
  ---
99
152
 
100
- ${mdcBody}
153
+ @./${skillName}.mdc
101
154
  `;
102
155
  if (dryRun) {
103
- (0, constants_1.logVerboseInfo)(`DRY RUN: Would generate ${skillName}/SKILL.md from ${mdcFile.name}`, verbose, dryRun);
156
+ (0, constants_1.logVerboseInfo)(`DRY RUN: Would generate ${skillName}/SKILL.md with @reference`, verbose, dryRun);
104
157
  }
105
158
  else {
106
- await fs.mkdir(skillFolderPath, { recursive: true });
107
159
  await fs.writeFile(skillMdPath, newSkillMd, 'utf8');
108
- (0, constants_1.logVerboseInfo)(`Generated ${skillName}/SKILL.md from ${mdcFile.name}`, verbose, dryRun);
160
+ (0, constants_1.logVerboseInfo)(`Generated ${skillName}/SKILL.md with @./${skillName}.mdc reference`, verbose, dryRun);
109
161
  }
110
162
  synced.push(skillName);
111
163
  }
112
- else {
113
- // SKILL.md exists - check if it has synced: true
164
+ else if (skillMdContent !== null) {
165
+ // SKILL.md exists - check if it's a reference
114
166
  const { frontmatter: skillFrontmatter, body: skillBody } = (0, FrontmatterParser_1.parseFrontmatter)(skillMdContent);
115
- if (skillFrontmatter?.synced === true) {
116
- // Case 2: synced: true → regenerate SKILL.md from .mdc
117
- const newFrontmatter = {
118
- name: skillFrontmatter.name || skillName,
119
- description: mdcFrontmatter?.description ||
120
- skillFrontmatter.description ||
121
- `Skill: ${skillName}`,
122
- synced: true,
123
- };
124
- const newSkillMd = `---
167
+ const refCheck = isReferenceBody(skillBody);
168
+ if (refCheck.isReference) {
169
+ // Case 2: SKILL.md is @reference → source file is truth
170
+ if (refCheck.referencePath === `./${skillName}.mdc`) {
171
+ // Sibling reference pattern - validate/update frontmatter if .mdc changed
172
+ if (siblingMdcContent !== null) {
173
+ const { frontmatter: mdcFrontmatter } = (0, FrontmatterParser_1.parseFrontmatter)(siblingMdcContent);
174
+ // Update SKILL.md frontmatter if description changed in .mdc
175
+ if (mdcFrontmatter?.description &&
176
+ mdcFrontmatter.description !== skillFrontmatter?.description) {
177
+ const newFrontmatter = {
178
+ name: skillFrontmatter?.name || skillName,
179
+ description: mdcFrontmatter.description,
180
+ };
181
+ const newSkillMd = `---
125
182
  ${yaml.dump(newFrontmatter, { lineWidth: -1, noRefs: true }).trim()}
126
183
  ---
127
184
 
128
- ${mdcBody}
185
+ @./${skillName}.mdc
129
186
  `;
130
- if (dryRun) {
131
- (0, constants_1.logVerboseInfo)(`DRY RUN: Would update ${skillName}/SKILL.md from ${mdcFile.name}`, verbose, dryRun);
187
+ if (dryRun) {
188
+ (0, constants_1.logVerboseInfo)(`DRY RUN: Would update ${skillName}/SKILL.md frontmatter from .mdc`, verbose, dryRun);
189
+ }
190
+ else {
191
+ await fs.writeFile(skillMdPath, newSkillMd, 'utf8');
192
+ (0, constants_1.logVerboseInfo)(`Updated ${skillName}/SKILL.md frontmatter from .mdc`, verbose, dryRun);
193
+ }
194
+ synced.push(skillName);
195
+ }
196
+ }
132
197
  }
133
- else {
134
- await fs.writeFile(skillMdPath, newSkillMd, 'utf8');
135
- (0, constants_1.logVerboseInfo)(`Updated ${skillName}/SKILL.md from ${mdcFile.name}`, verbose, dryRun);
198
+ else if (refCheck.referencePath) {
199
+ // Pre-0.7 pattern or other external reference - migrate to sibling pattern
200
+ // Determine base path for resolution:
201
+ // - Paths starting with .claude/ are relative to project root
202
+ // - Other paths are relative to the skill folder
203
+ let referencedPath;
204
+ if (refCheck.referencePath.startsWith('.claude/')) {
205
+ // Project root is parent of .claude directory (skillsDir is .claude/skills)
206
+ const projectRoot = path.dirname(path.dirname(skillsDir));
207
+ referencedPath = path.join(projectRoot, refCheck.referencePath);
208
+ }
209
+ else {
210
+ referencedPath = path.resolve(skillFolderPath, refCheck.referencePath);
211
+ }
212
+ try {
213
+ const referencedContent = await fs.readFile(referencedPath, 'utf8');
214
+ // Parse the referenced file for frontmatter
215
+ const { frontmatter: refFrontmatter, body: refBody } = (0, FrontmatterParser_1.parseFrontmatter)(referencedContent);
216
+ // Create sibling .mdc with the content
217
+ let mdcContent;
218
+ if (refFrontmatter &&
219
+ Object.keys(refFrontmatter).length > 0) {
220
+ const mdcFrontmatterData = {};
221
+ if (refFrontmatter.description) {
222
+ mdcFrontmatterData.description = refFrontmatter.description;
223
+ }
224
+ if (refFrontmatter.globs) {
225
+ mdcFrontmatterData.globs = refFrontmatter.globs;
226
+ }
227
+ if (refFrontmatter.alwaysApply !== undefined) {
228
+ mdcFrontmatterData.alwaysApply = refFrontmatter.alwaysApply;
229
+ }
230
+ if (Object.keys(mdcFrontmatterData).length > 0) {
231
+ mdcContent = `---
232
+ ${yaml.dump(mdcFrontmatterData, { lineWidth: -1, noRefs: true }).trim()}
233
+ ---
234
+
235
+ ${refBody}
236
+ `;
237
+ }
238
+ else {
239
+ mdcContent = refBody;
240
+ }
241
+ }
242
+ else {
243
+ mdcContent = referencedContent;
244
+ }
245
+ // Update SKILL.md to point to sibling .mdc
246
+ const newFrontmatter = {
247
+ name: skillFrontmatter?.name || skillName,
248
+ description: refFrontmatter?.description ||
249
+ skillFrontmatter?.description ||
250
+ `Skill: ${skillName}`,
251
+ };
252
+ const newSkillMd = `---
253
+ ${yaml.dump(newFrontmatter, { lineWidth: -1, noRefs: true }).trim()}
254
+ ---
255
+
256
+ @./${skillName}.mdc
257
+ `;
258
+ if (dryRun) {
259
+ (0, constants_1.logVerboseInfo)(`DRY RUN: Would migrate ${skillName} from ${refCheck.referencePath} to sibling pattern`, verbose, dryRun);
260
+ }
261
+ else {
262
+ await fs.writeFile(siblingMdcPath, mdcContent, 'utf8');
263
+ await fs.writeFile(skillMdPath, newSkillMd, 'utf8');
264
+ (0, constants_1.logVerboseInfo)(`Migrated ${skillName} from ${refCheck.referencePath} to sibling pattern`, verbose, dryRun);
265
+ }
266
+ synced.push(skillName);
267
+ }
268
+ catch {
269
+ // Referenced file doesn't exist or can't be read
270
+ warnings.push(`Cannot migrate ${skillName}: referenced file ${refCheck.referencePath} not found or unreadable`);
271
+ }
136
272
  }
137
- synced.push(skillName);
138
273
  }
139
274
  else {
140
- // SKILL.md exists without synced: true SKILL.md is source of truth
141
- // Update .mdc from SKILL.md and add synced: true
142
- const mdcFrontmatterObj = {};
275
+ // Case 3: SKILL.md has full contentgenerate sibling .mdc, update to @reference
276
+ // Generate .mdc from SKILL.md body
277
+ const mdcFrontmatter = {};
143
278
  if (skillFrontmatter?.description) {
144
- mdcFrontmatterObj.description = skillFrontmatter.description;
279
+ mdcFrontmatter.description = skillFrontmatter.description;
145
280
  }
146
- let newMdcContent;
147
- if (Object.keys(mdcFrontmatterObj).length > 0) {
148
- newMdcContent = `---
149
- ${yaml.dump(mdcFrontmatterObj, { lineWidth: -1, noRefs: true }).trim()}
281
+ let mdcContent;
282
+ if (Object.keys(mdcFrontmatter).length > 0) {
283
+ mdcContent = `---
284
+ ${yaml.dump(mdcFrontmatter, { lineWidth: -1, noRefs: true }).trim()}
150
285
  ---
151
286
 
152
287
  ${skillBody}
153
288
  `;
154
289
  }
155
290
  else {
156
- newMdcContent = skillBody;
291
+ mdcContent = skillBody;
157
292
  }
158
- // Update SKILL.md with synced: true
293
+ // Update SKILL.md to @reference
159
294
  const newSkillFrontmatter = {
160
295
  name: skillFrontmatter?.name || skillName,
161
296
  description: skillFrontmatter?.description || `Skill: ${skillName}`,
162
- synced: true,
163
297
  };
164
298
  const newSkillMd = `---
165
299
  ${yaml.dump(newSkillFrontmatter, { lineWidth: -1, noRefs: true }).trim()}
166
300
  ---
167
301
 
168
- ${skillBody}
302
+ @./${skillName}.mdc
169
303
  `;
170
304
  if (dryRun) {
171
- (0, constants_1.logVerboseInfo)(`DRY RUN: Would update ${skillName}.mdc from ${skillName}/SKILL.md`, verbose, dryRun);
305
+ (0, constants_1.logVerboseInfo)(`DRY RUN: Would generate ${skillName}/${skillName}.mdc and update SKILL.md`, verbose, dryRun);
172
306
  }
173
307
  else {
174
- await fs.writeFile(mdcPath, newMdcContent, 'utf8');
308
+ await fs.writeFile(siblingMdcPath, mdcContent, 'utf8');
175
309
  await fs.writeFile(skillMdPath, newSkillMd, 'utf8');
176
- (0, constants_1.logVerboseInfo)(`Updated ${skillName}.mdc from ${skillName}/SKILL.md`, verbose, dryRun);
310
+ (0, constants_1.logVerboseInfo)(`Generated ${skillName}/${skillName}.mdc and updated SKILL.md to @reference`, verbose, dryRun);
177
311
  }
178
312
  synced.push(skillName);
179
313
  }
180
314
  }
181
- }
182
- catch (err) {
183
- warnings.push(`Failed to sync ${skillName}: ${err.message}`);
184
- }
185
- }
186
- // Case 3: Process skill folders without .mdc (SKILL.md → .mdc)
187
- for (const folder of skillFolders) {
188
- const skillName = folder.name;
189
- const mdcPath = path.join(skillsDir, `${skillName}.mdc`);
190
- const skillMdPath = path.join(skillsDir, skillName, constants_1.SKILL_MD_FILENAME);
191
- // Check if .mdc already exists
192
- let hasMdc = false;
193
- try {
194
- await fs.access(mdcPath);
195
- hasMdc = true;
196
- }
197
- catch {
198
- // No .mdc
199
- }
200
- if (hasMdc) {
201
- // Already processed in Cases 1 & 2
202
- continue;
203
- }
204
- // Check if SKILL.md exists
205
- let skillMdContent = null;
206
- try {
207
- skillMdContent = await fs.readFile(skillMdPath, 'utf8');
208
- }
209
- catch {
210
- // No SKILL.md - not a valid skill
211
- continue;
212
- }
213
- try {
214
- const { frontmatter, body } = (0, FrontmatterParser_1.parseFrontmatter)(skillMdContent);
215
- if (frontmatter?.synced !== true) {
216
- // Case 3: SKILL.md without synced: true → generate .mdc, add synced: true to SKILL.md
217
- // Generate .mdc from SKILL.md body
218
- const mdcFrontmatter = {};
219
- if (frontmatter?.description) {
220
- mdcFrontmatter.description = frontmatter.description;
221
- }
222
- let mdcContent;
223
- if (Object.keys(mdcFrontmatter).length > 0) {
224
- mdcContent = `---
225
- ${yaml.dump(mdcFrontmatter, { lineWidth: -1, noRefs: true }).trim()}
226
- ---
227
-
228
- ${body}
229
- `;
230
- }
231
- else {
232
- mdcContent = body;
233
- }
234
- // Update SKILL.md with synced: true
235
- const newSkillFrontmatter = {
236
- name: frontmatter?.name || skillName,
237
- description: frontmatter?.description || `Skill: ${skillName}`,
238
- synced: true,
239
- };
240
- const newSkillMd = `---
241
- ${yaml.dump(newSkillFrontmatter, { lineWidth: -1, noRefs: true }).trim()}
242
- ---
243
-
244
- ${body}
245
- `;
246
- if (dryRun) {
247
- (0, constants_1.logVerboseInfo)(`DRY RUN: Would generate ${skillName}.mdc from ${skillName}/SKILL.md`, verbose, dryRun);
248
- }
249
- else {
250
- await fs.writeFile(mdcPath, mdcContent, 'utf8');
251
- await fs.writeFile(skillMdPath, newSkillMd, 'utf8');
252
- (0, constants_1.logVerboseInfo)(`Generated ${skillName}.mdc from ${skillName}/SKILL.md`, verbose, dryRun);
253
- }
254
- synced.push(skillName);
255
- }
315
+ // If neither exists, skip - not a valid skill folder
256
316
  }
257
317
  catch (err) {
258
318
  warnings.push(`Failed to sync ${skillName}: ${err.message}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skiller",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "Skiller — apply the same rules to all coding agents",
5
5
  "main": "dist/lib.js",
6
6
  "publishConfig": {