skiller 0.7.14 → 0.7.16

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/README.md CHANGED
@@ -59,11 +59,21 @@ A Claude-centric fork of [ruler](https://github.com/intellectronica/ruler) with
59
59
  - Reads `.claude/settings.json` `enabledPlugins`
60
60
  - Syncs enabled plugin `skills/` into agent skills directories on `skiller apply`
61
61
  - Syncs enabled plugin `commands/*.md` as skills (`SKILL.md`) into agent skills directories
62
- - Uses the skill/command name by default (matches existing Codex skill names)
63
- - If a name conflicts, local skills win and the plugin skill is namespaced as `<pluginId>__<name>`
62
+ - Syncs enabled plugin `agents/**/*.md` as skills (`SKILL.md`) into agent skills directories
63
+ - Uses the skill/command/agent name by default (matches existing Codex skill names)
64
+ - If a name conflicts, local skills win and the plugin skill is namespaced as `<pluginId>-<name>`
64
65
  - Tracks plugin-managed skills in a single `.skiller-plugins.json` file per agent skills directory
65
66
  - Removes stale plugin skills when plugins are disabled
66
67
 
68
+ ## 10. Claude Commands/Agents → Skills
69
+
70
+ - Syncs `.claude/commands/**/*.md` as skills (`SKILL.md`) into agent skills directories
71
+ - Syncs `.claude/agents/**/*.md` as skills (`SKILL.md`) into agent skills directories
72
+ - Uses the command/agent name by default
73
+ - If a name conflicts, existing local/manual skills win and the project item is namespaced as `claude-<name>`
74
+ - Project items win over plugin skills/commands/agents on name conflicts
75
+ - Tracks project-managed items in a single `.skiller-claude.json` file per agent skills directory
76
+
67
77
  ---
68
78
 
69
79
  # Skiller: Centralise Your AI Coding Assistant Instructions
@@ -596,7 +606,7 @@ If your project enables Claude Code plugins in `.claude/settings.json`, Skiller
596
606
  - Plugin `skills/` are copied as skills
597
607
  - Plugin `commands/*.md` are converted into skills (`SKILL.md`)
598
608
  - Plugin skills use their original skill/command name by default
599
- - If a name conflicts, local skills win and the plugin skill is namespaced as `<pluginId>__<name>`
609
+ - If a name conflicts, local skills win and the plugin skill is namespaced as `<pluginId>-<name>`
600
610
  - Plugin-managed skills are tracked via `.skiller-plugins.json` in each agent skills directory
601
611
 
602
612
  ### Skills Directory Structure
@@ -38,6 +38,7 @@ exports.readInstalledPluginsIndex = readInstalledPluginsIndex;
38
38
  exports.resolvePluginInstall = resolvePluginInstall;
39
39
  exports.discoverPluginSkillDirs = discoverPluginSkillDirs;
40
40
  exports.discoverPluginCommandFiles = discoverPluginCommandFiles;
41
+ exports.discoverPluginAgentFiles = discoverPluginAgentFiles;
41
42
  exports.syncClaudePluginsToSkillsDirs = syncClaudePluginsToSkillsDirs;
42
43
  const fs = __importStar(require("fs/promises"));
43
44
  const os = __importStar(require("os"));
@@ -141,8 +142,24 @@ function resolvePluginInstall(pluginId, projectRoot, index) {
141
142
  };
142
143
  }
143
144
  const userCandidates = entries.filter((e) => e && e.scope === 'user');
144
- if (userCandidates.length === 0)
145
- return null;
145
+ if (userCandidates.length === 0) {
146
+ // Fallback: if the plugin is installed for a different project but enabled
147
+ // here, still use the newest available install. This avoids noisy
148
+ // "not installed" warnings for plugins that don't ship skills/commands.
149
+ const anyCandidates = entries.filter((e) => e && typeof e.installPath === 'string' && e.installPath.length > 0);
150
+ if (anyCandidates.length === 0)
151
+ return null;
152
+ anyCandidates.sort((a, b) => {
153
+ const at = parseDate(a.lastUpdated) || parseDate(a.installedAt);
154
+ const bt = parseDate(b.lastUpdated) || parseDate(b.installedAt);
155
+ return bt - at;
156
+ });
157
+ return {
158
+ pluginId,
159
+ installPath: anyCandidates[0].installPath,
160
+ version: anyCandidates[0].version,
161
+ };
162
+ }
146
163
  userCandidates.sort((a, b) => {
147
164
  const at = parseDate(a.lastUpdated) || parseDate(a.installedAt);
148
165
  const bt = parseDate(b.lastUpdated) || parseDate(b.installedAt);
@@ -203,6 +220,49 @@ async function discoverPluginCommandFiles(installPath) {
203
220
  file: path.join(commandsRoot, e.name),
204
221
  }));
205
222
  }
223
+ async function discoverPluginAgentFiles(installPath) {
224
+ const agentsRoot = path.join(installPath, 'agents');
225
+ if (!(await fileExists(agentsRoot)))
226
+ return [];
227
+ const results = [];
228
+ async function walk(current, depth) {
229
+ if (depth >= constants_1.MAX_RECURSION_DEPTH)
230
+ return;
231
+ let entries;
232
+ try {
233
+ entries = await fs.readdir(current, { withFileTypes: true });
234
+ }
235
+ catch {
236
+ return;
237
+ }
238
+ for (const entry of entries) {
239
+ const full = path.join(current, entry.name);
240
+ if (entry.isDirectory()) {
241
+ await walk(full, depth + 1);
242
+ continue;
243
+ }
244
+ if (!entry.isFile() || !entry.name.endsWith('.md'))
245
+ continue;
246
+ let content;
247
+ try {
248
+ content = await fs.readFile(full, 'utf8');
249
+ }
250
+ catch {
251
+ continue;
252
+ }
253
+ const parsed = (0, FrontmatterParser_1.parseFrontmatter)(content);
254
+ const fmName = parsed.rawFrontmatter && typeof parsed.rawFrontmatter.name === 'string'
255
+ ? parsed.rawFrontmatter.name
256
+ : parsed.frontmatter?.name;
257
+ if (typeof fmName !== 'string' || fmName.trim() === '')
258
+ continue;
259
+ const rel = path.relative(agentsRoot, full).replace(/\\/g, '/');
260
+ results.push({ name: fmName.trim(), file: full, rel });
261
+ }
262
+ }
263
+ await walk(agentsRoot, 0);
264
+ return results;
265
+ }
206
266
  function generateBaseNameFromRelId(relId) {
207
267
  const normalized = relId.replace(/\\/g, '/');
208
268
  const segments = normalized.split('/').filter(Boolean);
@@ -212,7 +272,7 @@ function generateBaseNameFromCommand(commandName) {
212
272
  return sanitizeId(commandName);
213
273
  }
214
274
  function generateNamespacedName(pluginId, baseName) {
215
- return `${sanitizeId(pluginId)}__${baseName}`;
275
+ return `${sanitizeId(pluginId)}-${baseName}`;
216
276
  }
217
277
  async function readManifestFile(targetSkillsDir) {
218
278
  const manifestPath = path.join(targetSkillsDir, MANIFEST_FILENAME);
@@ -233,7 +293,9 @@ async function readManifestFile(targetSkillsDir) {
233
293
  if (typeof e.pluginId !== 'string')
234
294
  continue;
235
295
  const sourceKind = e.sourceKind;
236
- if (sourceKind !== 'skill' && sourceKind !== 'command')
296
+ if (sourceKind !== 'skill' &&
297
+ sourceKind !== 'command' &&
298
+ sourceKind !== 'agent')
237
299
  continue;
238
300
  if (typeof e.sourceRelPath !== 'string')
239
301
  continue;
@@ -294,7 +356,9 @@ async function readLegacyMarkerFile(dir) {
294
356
  return null;
295
357
  if (typeof obj.generatedName !== 'string')
296
358
  return null;
297
- if (obj.sourceKind !== 'skill' && obj.sourceKind !== 'command')
359
+ if (obj.sourceKind !== 'skill' &&
360
+ obj.sourceKind !== 'command' &&
361
+ obj.sourceKind !== 'agent')
298
362
  return null;
299
363
  if (typeof obj.sourceRelPath !== 'string')
300
364
  return null;
@@ -390,6 +454,77 @@ async function discoverLocalSkillNames(projectRoot) {
390
454
  await walk(localSkillsDir, 0);
391
455
  return names;
392
456
  }
457
+ async function discoverLocalCommandNames(projectRoot) {
458
+ const localCommandsDir = path.join(projectRoot, '.claude', 'commands');
459
+ if (!(await fileExists(localCommandsDir)))
460
+ return new Set();
461
+ const names = new Set();
462
+ async function walk(current, depth) {
463
+ if (depth >= constants_1.MAX_RECURSION_DEPTH)
464
+ return;
465
+ let entries;
466
+ try {
467
+ entries = await fs.readdir(current, { withFileTypes: true });
468
+ }
469
+ catch {
470
+ return;
471
+ }
472
+ for (const entry of entries) {
473
+ const full = path.join(current, entry.name);
474
+ if (entry.isDirectory()) {
475
+ await walk(full, depth + 1);
476
+ continue;
477
+ }
478
+ if (!entry.isFile() || !entry.name.endsWith('.md'))
479
+ continue;
480
+ names.add(sanitizeId(path.basename(entry.name, '.md')));
481
+ }
482
+ }
483
+ await walk(localCommandsDir, 0);
484
+ return names;
485
+ }
486
+ async function discoverLocalAgentNames(projectRoot) {
487
+ const localAgentsDir = path.join(projectRoot, '.claude', 'agents');
488
+ if (!(await fileExists(localAgentsDir)))
489
+ return new Set();
490
+ const names = new Set();
491
+ async function walk(current, depth) {
492
+ if (depth >= constants_1.MAX_RECURSION_DEPTH)
493
+ return;
494
+ let entries;
495
+ try {
496
+ entries = await fs.readdir(current, { withFileTypes: true });
497
+ }
498
+ catch {
499
+ return;
500
+ }
501
+ for (const entry of entries) {
502
+ const full = path.join(current, entry.name);
503
+ if (entry.isDirectory()) {
504
+ await walk(full, depth + 1);
505
+ continue;
506
+ }
507
+ if (!entry.isFile() || !entry.name.endsWith('.md'))
508
+ continue;
509
+ let content;
510
+ try {
511
+ content = await fs.readFile(full, 'utf8');
512
+ }
513
+ catch {
514
+ continue;
515
+ }
516
+ const parsed = (0, FrontmatterParser_1.parseFrontmatter)(content);
517
+ const fmName = parsed.rawFrontmatter && typeof parsed.rawFrontmatter.name === 'string'
518
+ ? parsed.rawFrontmatter.name
519
+ : parsed.frontmatter?.name;
520
+ if (typeof fmName !== 'string' || fmName.trim() === '')
521
+ continue;
522
+ names.add(sanitizeId(fmName.trim()));
523
+ }
524
+ }
525
+ await walk(localAgentsDir, 0);
526
+ return names;
527
+ }
393
528
  async function ensureDir(dir, dryRun) {
394
529
  if (dryRun)
395
530
  return;
@@ -423,15 +558,15 @@ async function rewriteSkillMdName(skillMdPath, name, pluginId, dryRun) {
423
558
  return;
424
559
  await fs.writeFile(skillMdPath, next, 'utf8');
425
560
  }
426
- async function writeCommandAsSkill(srcCommandPath, destDir, generatedName, pluginId, dryRun) {
427
- const content = await fs.readFile(srcCommandPath, 'utf8');
561
+ async function writeMarkdownAsSkill(srcMarkdownPath, destDir, generatedName, pluginId, kindLabel, dryRun) {
562
+ const content = await fs.readFile(srcMarkdownPath, 'utf8');
428
563
  const { rawFrontmatter, body } = (0, FrontmatterParser_1.parseFrontmatter)(content);
429
564
  const fm = rawFrontmatter
430
565
  ? { ...rawFrontmatter }
431
566
  : {};
432
567
  fm.name = generatedName;
433
568
  if (typeof fm.description !== 'string' || fm.description.trim() === '') {
434
- fm.description = `Command from ${pluginId}: ${path.basename(srcCommandPath, '.md')}`;
569
+ fm.description = `${kindLabel} from ${pluginId}: ${path.basename(srcMarkdownPath, '.md')}`;
435
570
  }
436
571
  const next = `---\n${yaml
437
572
  .dump(fm, { lineWidth: -1, noRefs: true })
@@ -450,6 +585,13 @@ async function syncClaudePluginsToSkillsDirs(args) {
450
585
  const claudeDir = path.join(getUserHomeDir(), '.claude');
451
586
  const index = await readInstalledPluginsIndex(claudeDir);
452
587
  const localSkillNames = await discoverLocalSkillNames(projectRoot);
588
+ const localCommandNames = await discoverLocalCommandNames(projectRoot);
589
+ const localAgentNames = await discoverLocalAgentNames(projectRoot);
590
+ const localReservedNames = new Set([
591
+ ...localSkillNames,
592
+ ...localCommandNames,
593
+ ...localAgentNames,
594
+ ]);
453
595
  // If we can't read the installed plugins index, we can't install/update
454
596
  // anything, but we can still clean up managed folders for plugins that
455
597
  // are no longer enabled.
@@ -460,7 +602,7 @@ async function syncClaudePluginsToSkillsDirs(args) {
460
602
  const managedEntries = await loadManagedEntries(targetSkillsDir, dryRun);
461
603
  const nextEntries = [];
462
604
  // Build reserved set (local skills always win).
463
- const reserved = new Set(localSkillNames);
605
+ const reserved = new Set(localReservedNames);
464
606
  // Also reserve any existing non-managed directories.
465
607
  const managedDest = new Set(managedEntries.map((e) => e.destRelPath));
466
608
  let dirents = [];
@@ -537,6 +679,20 @@ async function syncClaudePluginsToSkillsDirs(args) {
537
679
  baseName,
538
680
  });
539
681
  }
682
+ const agentFiles = await discoverPluginAgentFiles(plugin.installPath);
683
+ for (const a of agentFiles) {
684
+ const baseName = sanitizeId(a.name);
685
+ const sourceRelPath = `agents/${a.rel}`;
686
+ expectedItems.push({
687
+ itemKey: makeItemKey(plugin.pluginId, 'agent', sourceRelPath),
688
+ pluginId: plugin.pluginId,
689
+ pluginVersion: plugin.version,
690
+ kind: 'agent',
691
+ sourcePath: a.file,
692
+ sourceRelPath,
693
+ baseName,
694
+ });
695
+ }
540
696
  }
541
697
  const sortedItems = [...expectedItems].sort((a, b) => {
542
698
  const ak = `${a.baseName}::${a.pluginId}::${a.kind}::${a.sourceRelPath}`;
@@ -558,7 +714,7 @@ async function syncClaudePluginsToSkillsDirs(args) {
558
714
  }
559
715
  const managedDest = new Set(managedEntries.map((e) => e.destRelPath));
560
716
  // Reserve: local skills always win. Also reserve any existing non-managed folders.
561
- const reserved = new Set(localSkillNames);
717
+ const reserved = new Set(localReservedNames);
562
718
  if (targetExists) {
563
719
  let dirents = [];
564
720
  try {
@@ -583,6 +739,11 @@ async function syncClaudePluginsToSkillsDirs(args) {
583
739
  const prev = prevDestByItemKey.get(item.itemKey);
584
740
  if (!prev)
585
741
  continue;
742
+ // Migration: previous versions used `${pluginId}__${name}`.
743
+ // Don't preserve legacy namespaced destinations so we can rename to the
744
+ // new `${pluginId}-${name}` format.
745
+ if (prev.startsWith(`${sanitizeId(item.pluginId)}__`))
746
+ continue;
586
747
  if (taken.has(prev))
587
748
  continue;
588
749
  assignedDestByItemKey.set(item.itemKey, prev);
@@ -602,7 +763,7 @@ async function syncClaudePluginsToSkillsDirs(args) {
602
763
  let candidate = namespacedBase;
603
764
  let i = 2;
604
765
  while (taken.has(candidate)) {
605
- candidate = `${namespacedBase}__${i++}`;
766
+ candidate = `${namespacedBase}-${i++}`;
606
767
  }
607
768
  assignedDestByItemKey.set(item.itemKey, candidate);
608
769
  taken.add(candidate);
@@ -642,12 +803,13 @@ async function syncClaudePluginsToSkillsDirs(args) {
642
803
  await removeLegacyMarkerFile(destDir, dryRun);
643
804
  }
644
805
  else {
806
+ const kindLabel = item.kind === 'command' ? 'command' : 'agent';
645
807
  (0, constants_1.logVerboseInfo)(dryRun
646
- ? `DRY RUN: Would install plugin command '${destRelPath}' as skill to ${targetSkillsDir}`
647
- : `Installing plugin command '${destRelPath}' as skill to ${targetSkillsDir}`, verbose, dryRun);
808
+ ? `DRY RUN: Would install plugin ${kindLabel} '${destRelPath}' as skill to ${targetSkillsDir}`
809
+ : `Installing plugin ${kindLabel} '${destRelPath}' as skill to ${targetSkillsDir}`, verbose, dryRun);
648
810
  await ensureDir(destDir, dryRun);
649
811
  if (!dryRun) {
650
- await writeCommandAsSkill(item.sourcePath, destDir, destRelPath, item.pluginId, dryRun);
812
+ await writeMarkdownAsSkill(item.sourcePath, destDir, destRelPath, item.pluginId, item.kind === 'command' ? 'Command' : 'Agent', dryRun);
651
813
  }
652
814
  await removeLegacyMarkerFile(destDir, dryRun);
653
815
  }
@@ -0,0 +1,460 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.syncClaudeProjectCommandsAndAgentsToSkillsDirs = syncClaudeProjectCommandsAndAgentsToSkillsDirs;
37
+ const fs = __importStar(require("fs/promises"));
38
+ const path = __importStar(require("path"));
39
+ const yaml = __importStar(require("js-yaml"));
40
+ const constants_1 = require("../constants");
41
+ const FrontmatterParser_1 = require("./FrontmatterParser");
42
+ const MANIFEST_FILENAME = '.skiller-claude.json';
43
+ const MANIFEST_VERSION = 1;
44
+ const LEGACY_PLUGIN_MARKER_FILENAME = '.skiller-plugin.json';
45
+ function sanitizeId(value) {
46
+ return value.replace(/[^A-Za-z0-9._-]+/g, '_');
47
+ }
48
+ async function fileExists(p) {
49
+ try {
50
+ await fs.access(p);
51
+ return true;
52
+ }
53
+ catch {
54
+ return false;
55
+ }
56
+ }
57
+ function makeItemKey(sourceKind, sourceRelPath) {
58
+ return `${sourceKind}::${sourceRelPath}`;
59
+ }
60
+ function getCommandsRoot(projectRoot) {
61
+ return path.join(projectRoot, '.claude', 'commands');
62
+ }
63
+ function getAgentsRoot(projectRoot) {
64
+ return path.join(projectRoot, '.claude', 'agents');
65
+ }
66
+ async function discoverCommandFiles(projectRoot) {
67
+ const commandsRoot = getCommandsRoot(projectRoot);
68
+ if (!(await fileExists(commandsRoot)))
69
+ return [];
70
+ const results = [];
71
+ async function walk(current, depth) {
72
+ if (depth >= constants_1.MAX_RECURSION_DEPTH)
73
+ return;
74
+ let entries;
75
+ try {
76
+ entries = await fs.readdir(current, { withFileTypes: true });
77
+ }
78
+ catch {
79
+ return;
80
+ }
81
+ for (const entry of entries) {
82
+ const full = path.join(current, entry.name);
83
+ if (entry.isDirectory()) {
84
+ await walk(full, depth + 1);
85
+ continue;
86
+ }
87
+ if (!entry.isFile() || !entry.name.endsWith('.md'))
88
+ continue;
89
+ const name = sanitizeId(path.basename(entry.name, '.md'));
90
+ const rel = path.relative(projectRoot, full).replace(/\\/g, '/');
91
+ results.push({ name, file: full, rel });
92
+ }
93
+ }
94
+ await walk(commandsRoot, 0);
95
+ return results;
96
+ }
97
+ async function discoverAgentFiles(projectRoot) {
98
+ const agentsRoot = getAgentsRoot(projectRoot);
99
+ if (!(await fileExists(agentsRoot)))
100
+ return [];
101
+ const results = [];
102
+ async function walk(current, depth) {
103
+ if (depth >= constants_1.MAX_RECURSION_DEPTH)
104
+ return;
105
+ let entries;
106
+ try {
107
+ entries = await fs.readdir(current, { withFileTypes: true });
108
+ }
109
+ catch {
110
+ return;
111
+ }
112
+ for (const entry of entries) {
113
+ const full = path.join(current, entry.name);
114
+ if (entry.isDirectory()) {
115
+ await walk(full, depth + 1);
116
+ continue;
117
+ }
118
+ if (!entry.isFile() || !entry.name.endsWith('.md'))
119
+ continue;
120
+ let content;
121
+ try {
122
+ content = await fs.readFile(full, 'utf8');
123
+ }
124
+ catch {
125
+ continue;
126
+ }
127
+ const parsed = (0, FrontmatterParser_1.parseFrontmatter)(content);
128
+ const fmName = parsed.rawFrontmatter && typeof parsed.rawFrontmatter.name === 'string'
129
+ ? parsed.rawFrontmatter.name
130
+ : parsed.frontmatter?.name;
131
+ if (typeof fmName !== 'string' || fmName.trim() === '')
132
+ continue;
133
+ const name = sanitizeId(fmName.trim());
134
+ const rel = path.relative(projectRoot, full).replace(/\\/g, '/');
135
+ results.push({ name, file: full, rel });
136
+ }
137
+ }
138
+ await walk(agentsRoot, 0);
139
+ return results;
140
+ }
141
+ async function readManifestFile(targetSkillsDir) {
142
+ const manifestPath = path.join(targetSkillsDir, MANIFEST_FILENAME);
143
+ try {
144
+ const raw = JSON.parse(await fs.readFile(manifestPath, 'utf8'));
145
+ if (!raw || typeof raw !== 'object')
146
+ return null;
147
+ const obj = raw;
148
+ if (typeof obj.version !== 'number')
149
+ return null;
150
+ if (!Array.isArray(obj.entries))
151
+ return null;
152
+ const entries = [];
153
+ for (const entry of obj.entries) {
154
+ if (!entry || typeof entry !== 'object')
155
+ continue;
156
+ const e = entry;
157
+ if (e.sourceKind !== 'command' && e.sourceKind !== 'agent')
158
+ continue;
159
+ if (typeof e.sourceRelPath !== 'string')
160
+ continue;
161
+ if (typeof e.destRelPath !== 'string')
162
+ continue;
163
+ entries.push({
164
+ sourceKind: e.sourceKind,
165
+ sourceRelPath: e.sourceRelPath,
166
+ destRelPath: e.destRelPath,
167
+ });
168
+ }
169
+ return {
170
+ version: obj.version,
171
+ entries,
172
+ };
173
+ }
174
+ catch {
175
+ return null;
176
+ }
177
+ }
178
+ async function writeManifestFile(targetSkillsDir, entries, dryRun) {
179
+ const manifestPath = path.join(targetSkillsDir, MANIFEST_FILENAME);
180
+ if (entries.length === 0) {
181
+ if (dryRun)
182
+ return;
183
+ try {
184
+ await fs.unlink(manifestPath);
185
+ }
186
+ catch {
187
+ // ignore
188
+ }
189
+ return;
190
+ }
191
+ const manifest = {
192
+ version: MANIFEST_VERSION,
193
+ entries: [...entries].sort((a, b) => {
194
+ const ak = `${a.destRelPath}::${a.sourceKind}::${a.sourceRelPath}`;
195
+ const bk = `${b.destRelPath}::${b.sourceKind}::${b.sourceRelPath}`;
196
+ return ak.localeCompare(bk);
197
+ }),
198
+ };
199
+ if (dryRun)
200
+ return;
201
+ await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
202
+ }
203
+ async function ensureDir(dir, dryRun) {
204
+ if (dryRun)
205
+ return;
206
+ await fs.mkdir(dir, { recursive: true });
207
+ }
208
+ async function removeDir(dir, dryRun) {
209
+ if (dryRun)
210
+ return;
211
+ await fs.rm(dir, { recursive: true, force: true });
212
+ }
213
+ async function writeMarkdownAsSkill(srcPath, destDir, generatedName, kindLabel, dryRun) {
214
+ const content = await fs.readFile(srcPath, 'utf8');
215
+ const { rawFrontmatter, body } = (0, FrontmatterParser_1.parseFrontmatter)(content);
216
+ const fm = rawFrontmatter
217
+ ? { ...rawFrontmatter }
218
+ : {};
219
+ fm.name = generatedName;
220
+ if (typeof fm.description !== 'string' || fm.description.trim() === '') {
221
+ fm.description = `${kindLabel}: ${path.basename(srcPath, '.md')}`;
222
+ }
223
+ const next = `---\n${yaml
224
+ .dump(fm, { lineWidth: -1, noRefs: true })
225
+ .trim()}\n---\n\n${body}\n`;
226
+ if (dryRun)
227
+ return;
228
+ await fs.writeFile(path.join(destDir, 'SKILL.md'), next, 'utf8');
229
+ }
230
+ async function readPluginManagedDestNames(targetSkillsDir) {
231
+ const manifestPath = path.join(targetSkillsDir, '.skiller-plugins.json');
232
+ const names = new Set();
233
+ try {
234
+ const raw = JSON.parse(await fs.readFile(manifestPath, 'utf8'));
235
+ if (raw && typeof raw === 'object') {
236
+ const obj = raw;
237
+ if (Array.isArray(obj.entries)) {
238
+ for (const entry of obj.entries) {
239
+ if (!entry || typeof entry !== 'object')
240
+ continue;
241
+ const e = entry;
242
+ if (typeof e.destRelPath === 'string' && e.destRelPath.length > 0) {
243
+ names.add(e.destRelPath);
244
+ }
245
+ }
246
+ }
247
+ }
248
+ }
249
+ catch {
250
+ // ignore
251
+ }
252
+ // Legacy: prior versions wrote per-skill plugin marker files. Treat any
253
+ // folder containing one as plugin-managed so project items can take over.
254
+ try {
255
+ const dirents = await fs.readdir(targetSkillsDir, { withFileTypes: true });
256
+ for (const d of dirents) {
257
+ if (!d.isDirectory())
258
+ continue;
259
+ try {
260
+ await fs.access(path.join(targetSkillsDir, d.name, LEGACY_PLUGIN_MARKER_FILENAME));
261
+ names.add(d.name);
262
+ }
263
+ catch {
264
+ // ignore
265
+ }
266
+ }
267
+ }
268
+ catch {
269
+ // ignore
270
+ }
271
+ return names;
272
+ }
273
+ async function discoverLocalSkillNames(projectRoot) {
274
+ const localSkillsDir = path.join(projectRoot, '.claude', 'skills');
275
+ if (!(await fileExists(localSkillsDir)))
276
+ return new Set();
277
+ const names = new Set();
278
+ async function walk(current, depth) {
279
+ if (depth >= constants_1.MAX_RECURSION_DEPTH)
280
+ return;
281
+ let entries;
282
+ try {
283
+ entries = await fs.readdir(current, { withFileTypes: true });
284
+ }
285
+ catch {
286
+ return;
287
+ }
288
+ const hasSkillMd = entries.some((e) => e.isFile() && e.name === 'SKILL.md');
289
+ if (hasSkillMd) {
290
+ names.add(path.basename(current));
291
+ return;
292
+ }
293
+ for (const entry of entries) {
294
+ if (!entry.isDirectory())
295
+ continue;
296
+ await walk(path.join(current, entry.name), depth + 1);
297
+ }
298
+ }
299
+ await walk(localSkillsDir, 0);
300
+ return names;
301
+ }
302
+ async function syncClaudeProjectCommandsAndAgentsToSkillsDirs(args) {
303
+ const { projectRoot, targetSkillsDirs, verbose, dryRun } = args;
304
+ const localSkillNames = await discoverLocalSkillNames(projectRoot);
305
+ const commands = await discoverCommandFiles(projectRoot);
306
+ const agents = await discoverAgentFiles(projectRoot);
307
+ const expectedItems = [];
308
+ for (const cmd of commands) {
309
+ expectedItems.push({
310
+ itemKey: makeItemKey('command', cmd.rel),
311
+ sourceKind: 'command',
312
+ sourcePath: cmd.file,
313
+ sourceRelPath: cmd.rel,
314
+ baseName: cmd.name,
315
+ });
316
+ }
317
+ for (const agent of agents) {
318
+ expectedItems.push({
319
+ itemKey: makeItemKey('agent', agent.rel),
320
+ sourceKind: 'agent',
321
+ sourcePath: agent.file,
322
+ sourceRelPath: agent.rel,
323
+ baseName: agent.name,
324
+ });
325
+ }
326
+ const sortedItems = [...expectedItems].sort((a, b) => {
327
+ const ak = `${a.baseName}::${a.sourceKind}::${a.sourceRelPath}`;
328
+ const bk = `${b.baseName}::${b.sourceKind}::${b.sourceRelPath}`;
329
+ return ak.localeCompare(bk);
330
+ });
331
+ for (const targetSkillsDir of targetSkillsDirs) {
332
+ const targetExists = await fileExists(targetSkillsDir);
333
+ const managedEntries = targetExists
334
+ ? ((await readManifestFile(targetSkillsDir))?.entries ?? [])
335
+ : [];
336
+ const prevDestByItemKey = new Map();
337
+ for (const entry of managedEntries) {
338
+ prevDestByItemKey.set(makeItemKey(entry.sourceKind, entry.sourceRelPath), entry.destRelPath);
339
+ }
340
+ const managedDest = new Set(managedEntries.map((e) => e.destRelPath));
341
+ const pluginManagedDest = targetExists
342
+ ? await readPluginManagedDestNames(targetSkillsDir)
343
+ : new Set();
344
+ const reserved = new Set(localSkillNames);
345
+ if (targetExists) {
346
+ let dirents = [];
347
+ try {
348
+ dirents = await fs.readdir(targetSkillsDir, { withFileTypes: true });
349
+ }
350
+ catch {
351
+ dirents = [];
352
+ }
353
+ for (const d of dirents) {
354
+ if (!d.isDirectory())
355
+ continue;
356
+ // Reserve any existing folder we do not manage, except plugin-managed
357
+ // folders (project should be able to take those over).
358
+ if (!managedDest.has(d.name) && !pluginManagedDest.has(d.name)) {
359
+ reserved.add(d.name);
360
+ }
361
+ }
362
+ }
363
+ const taken = new Set(reserved);
364
+ const assignedDestByItemKey = new Map();
365
+ // Preserve previous destinations when they are still available.
366
+ for (const item of sortedItems) {
367
+ const prev = prevDestByItemKey.get(item.itemKey);
368
+ if (!prev)
369
+ continue;
370
+ // Migration: previous versions used `claude__<name>`.
371
+ // Don't preserve legacy namespaced destinations so we can rename to the
372
+ // new `claude-<name>` format.
373
+ if (prev.startsWith('claude__'))
374
+ continue;
375
+ if (taken.has(prev))
376
+ continue;
377
+ assignedDestByItemKey.set(item.itemKey, prev);
378
+ taken.add(prev);
379
+ }
380
+ // Assign baseName, otherwise namespace with "claude-".
381
+ for (const item of sortedItems) {
382
+ if (assignedDestByItemKey.has(item.itemKey))
383
+ continue;
384
+ const base = item.baseName;
385
+ if (!taken.has(base)) {
386
+ assignedDestByItemKey.set(item.itemKey, base);
387
+ taken.add(base);
388
+ continue;
389
+ }
390
+ const namespacedBase = `claude-${base}`;
391
+ let candidate = namespacedBase;
392
+ let i = 2;
393
+ while (taken.has(candidate)) {
394
+ candidate = `${namespacedBase}-${i++}`;
395
+ }
396
+ assignedDestByItemKey.set(item.itemKey, candidate);
397
+ taken.add(candidate);
398
+ }
399
+ const assignedItems = sortedItems.map((item) => ({
400
+ ...item,
401
+ destRelPath: assignedDestByItemKey.get(item.itemKey),
402
+ }));
403
+ if (assignedItems.length > 0) {
404
+ await ensureDir(targetSkillsDir, dryRun);
405
+ }
406
+ else if (!targetExists && managedEntries.length === 0) {
407
+ // Nothing to do.
408
+ continue;
409
+ }
410
+ // Install/update expected items
411
+ for (const item of assignedItems) {
412
+ const destRelPath = item.destRelPath;
413
+ const destDir = path.join(targetSkillsDir, destRelPath);
414
+ if (await fileExists(destDir)) {
415
+ const isManagedByProject = managedDest.has(destRelPath);
416
+ const isManagedByPlugin = pluginManagedDest.has(destRelPath);
417
+ if (!isManagedByProject && !isManagedByPlugin) {
418
+ (0, constants_1.logWarn)(`[claude] Destination exists but is not skiller-managed, skipping: ${destDir}`, dryRun);
419
+ continue;
420
+ }
421
+ (0, constants_1.logVerboseInfo)(dryRun
422
+ ? `DRY RUN: Would update claude ${item.sourceKind} '${destRelPath}' in ${targetSkillsDir}`
423
+ : `Updating claude ${item.sourceKind} '${destRelPath}' in ${targetSkillsDir}`, verbose, dryRun);
424
+ await removeDir(destDir, dryRun);
425
+ }
426
+ (0, constants_1.logVerboseInfo)(dryRun
427
+ ? `DRY RUN: Would install claude ${item.sourceKind} '${destRelPath}' to ${targetSkillsDir}`
428
+ : `Installing claude ${item.sourceKind} '${destRelPath}' to ${targetSkillsDir}`, verbose, dryRun);
429
+ await ensureDir(destDir, dryRun);
430
+ if (!dryRun) {
431
+ await writeMarkdownAsSkill(item.sourcePath, destDir, destRelPath, item.sourceKind === 'command' ? 'Command' : 'Agent', dryRun);
432
+ }
433
+ }
434
+ // Cleanup stale managed folders
435
+ const expectedDest = new Set(assignedItems.map((i) => i.destRelPath));
436
+ const nextEntries = [];
437
+ for (const entry of managedEntries) {
438
+ if (expectedDest.has(entry.destRelPath)) {
439
+ // Still expected; re-add below.
440
+ continue;
441
+ }
442
+ if (reserved.has(entry.destRelPath)) {
443
+ // User/local took over the folder name; stop managing it but don't delete.
444
+ continue;
445
+ }
446
+ (0, constants_1.logVerboseInfo)(dryRun
447
+ ? `DRY RUN: Would remove stale claude skill '${entry.destRelPath}' from ${targetSkillsDir}`
448
+ : `Removing stale claude skill '${entry.destRelPath}' from ${targetSkillsDir}`, verbose, dryRun);
449
+ await removeDir(path.join(targetSkillsDir, entry.destRelPath), dryRun);
450
+ }
451
+ for (const item of assignedItems) {
452
+ nextEntries.push({
453
+ sourceKind: item.sourceKind,
454
+ sourceRelPath: item.sourceRelPath,
455
+ destRelPath: item.destRelPath,
456
+ });
457
+ }
458
+ await writeManifestFile(targetSkillsDir, nextEntries, dryRun);
459
+ }
460
+ }
@@ -589,6 +589,17 @@ async function propagateSkills(projectRoot, agents, skillsEnabled, verbose, dryR
589
589
  }
590
590
  }
591
591
  }
592
+ // Sync project Claude commands + agents as skills into agent skills dirs.
593
+ // This intentionally does NOT write into the committed .claude/skills source-of-truth.
594
+ if (destinationPaths.size > 0) {
595
+ const { syncClaudeProjectCommandsAndAgentsToSkillsDirs } = await Promise.resolve().then(() => __importStar(require('./ClaudeProjectSync')));
596
+ await syncClaudeProjectCommandsAndAgentsToSkillsDirs({
597
+ projectRoot,
598
+ targetSkillsDirs: [...destinationPaths],
599
+ verbose,
600
+ dryRun,
601
+ });
602
+ }
592
603
  // Sync Claude plugins (skills + commands converted to skills) into agent skills dirs.
593
604
  // This intentionally does NOT write into the committed .claude/skills source-of-truth.
594
605
  if (destinationPaths.size > 0) {
package/package.json CHANGED
@@ -1,79 +1,79 @@
1
1
  {
2
- "name": "skiller",
3
- "version": "0.7.14",
4
- "description": "Sync Claude skills and plugins to all coding agents",
5
- "main": "dist/lib.js",
6
- "publishConfig": {
7
- "access": "public"
8
- },
9
- "scripts": {
10
- "postinstall": "[ -n \"$CI\" ] || npx skiller@latest apply",
11
- "lint": "eslint \"src/**/*.{ts,tsx}\"",
12
- "lint:fix": "eslint \"src/**/*.{ts,tsx}\" --fix",
13
- "format": "prettier --write \"src/**/*.{ts,tsx,json,md}\"",
14
- "test": "jest",
15
- "test:watch": "jest --watch",
16
- "test:coverage": "jest --coverage",
17
- "test:integration": "jest tests/e2e/skiller.integration.test.ts --verbose",
18
- "build": "tsc",
19
- "release": "pnpm build && pnpm changeset publish"
20
- },
21
- "repository": {
22
- "type": "git",
23
- "url": "git+https://github.com/udecode/skiller.git"
24
- },
25
- "keywords": [
26
- "ai",
27
- "developer-tools",
28
- "copilot",
29
- "codex",
30
- "claude",
31
- "cursor",
32
- "aider",
33
- "config",
34
- "rules",
35
- "automation"
36
- ],
37
- "author": "Eleanor Berger",
38
- "license": "MIT",
39
- "bugs": {
40
- "url": "https://github.com/udecode/skiller/issues"
41
- },
42
- "engines": {
43
- "node": ">=18"
44
- },
45
- "files": [
46
- "dist",
47
- "README.md",
48
- "LICENSE"
49
- ],
50
- "bin": {
51
- "skiller": "dist/cli/index.js"
52
- },
53
- "devDependencies": {
54
- "@changesets/changelog-github": "^0.5.2",
55
- "@changesets/cli": "2.29.8",
56
- "@eslint/js": "^9.39.1",
57
- "@types/iarna__toml": "^2.0.5",
58
- "@types/jest": "^29.5.14",
59
- "@types/js-yaml": "^4.0.9",
60
- "@types/node": "^24.9.2",
61
- "@types/yargs": "^17.0.34",
62
- "@typescript-eslint/eslint-plugin": "^8.46.2",
63
- "@typescript-eslint/parser": "^8.46.2",
64
- "eslint": "^9.38.0",
65
- "eslint-config-prettier": "^10.1.8",
66
- "eslint-plugin-prettier": "^5.5.4",
67
- "jest": "^29.7.0",
68
- "prettier": "^3.6.2",
69
- "ts-jest": "^29.4.5",
70
- "typescript": "^5.9.3",
71
- "typescript-eslint": "^8.46.2"
72
- },
73
- "dependencies": {
74
- "@iarna/toml": "^2.2.5",
75
- "js-yaml": "^4.1.0",
76
- "yargs": "^18.0.0",
77
- "zod": "^4.1.12"
78
- }
2
+ "name": "skiller",
3
+ "version": "0.7.16",
4
+ "description": "Skiller apply the same rules to all coding agents",
5
+ "main": "dist/lib.js",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "scripts": {
10
+ "postinstall": "[ -n \"$CI\" ] || npx skiller@latest apply",
11
+ "lint": "eslint \"src/**/*.{ts,tsx}\"",
12
+ "lint:fix": "eslint \"src/**/*.{ts,tsx}\" --fix",
13
+ "format": "prettier --write \"src/**/*.{ts,tsx,json,md}\"",
14
+ "test": "jest",
15
+ "test:watch": "jest --watch",
16
+ "test:coverage": "jest --coverage",
17
+ "test:integration": "jest tests/e2e/skiller.integration.test.ts --verbose",
18
+ "build": "tsc",
19
+ "release": "pnpm build && pnpm changeset publish"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/udecode/skiller.git"
24
+ },
25
+ "keywords": [
26
+ "ai",
27
+ "developer-tools",
28
+ "copilot",
29
+ "codex",
30
+ "claude",
31
+ "cursor",
32
+ "aider",
33
+ "config",
34
+ "rules",
35
+ "automation"
36
+ ],
37
+ "author": "Eleanor Berger",
38
+ "license": "MIT",
39
+ "bugs": {
40
+ "url": "https://github.com/udecode/skiller/issues"
41
+ },
42
+ "engines": {
43
+ "node": ">=18"
44
+ },
45
+ "files": [
46
+ "dist",
47
+ "README.md",
48
+ "LICENSE"
49
+ ],
50
+ "bin": {
51
+ "skiller": "dist/cli/index.js"
52
+ },
53
+ "devDependencies": {
54
+ "@changesets/changelog-github": "^0.5.2",
55
+ "@changesets/cli": "2.29.8",
56
+ "@eslint/js": "^9.39.1",
57
+ "@types/iarna__toml": "^2.0.5",
58
+ "@types/jest": "^29.5.14",
59
+ "@types/js-yaml": "^4.0.9",
60
+ "@types/node": "^24.9.2",
61
+ "@types/yargs": "^17.0.34",
62
+ "@typescript-eslint/eslint-plugin": "^8.46.2",
63
+ "@typescript-eslint/parser": "^8.46.2",
64
+ "eslint": "^9.38.0",
65
+ "eslint-config-prettier": "^10.1.8",
66
+ "eslint-plugin-prettier": "^5.5.4",
67
+ "jest": "^29.7.0",
68
+ "prettier": "^3.6.2",
69
+ "ts-jest": "^29.4.5",
70
+ "typescript": "^5.9.3",
71
+ "typescript-eslint": "^8.46.2"
72
+ },
73
+ "dependencies": {
74
+ "@iarna/toml": "^2.2.5",
75
+ "js-yaml": "^4.1.0",
76
+ "yargs": "^18.0.0",
77
+ "zod": "^4.1.12"
78
+ }
79
79
  }