skiller 0.6.3 → 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.
package/README.md CHANGED
@@ -2,52 +2,50 @@
2
2
 
3
3
  A Claude-centric fork of [ruler](https://github.com/intellectronica/ruler) with native skills support:
4
4
 
5
- ## 1. CLAUDE.md @filename References
5
+ ## 1. Skills as Source of Truth
6
+
7
+ - `.claude/skills/` is the committed source of truth for skills
8
+ - **Bidirectional sync** between `.mdc` and `SKILL.md`:
9
+ - Create `.claude/skills/foo.mdc` → auto-generates `.claude/skills/foo/SKILL.md`
10
+ - Create `.claude/skills/foo/SKILL.md` → auto-generates `.claude/skills/foo.mdc`
11
+ - Uses `synced: true` frontmatter to track sync direction
12
+ - Edit either file, the other stays in sync on next `skiller apply`
13
+ - Skill folders from `.claude/rules/` are copied to `.claude/skills/`
14
+
15
+ ## 2. CLAUDE.md @filename References
6
16
 
7
17
  - Uses `@filename` syntax instead of merging content
8
18
  - Claude Code auto-includes referenced files
9
19
  - Reduces CLAUDE.md size and keeps sources separate
10
20
  - Other agents still get merged content
11
21
 
12
- ## 2. MDC File Support
22
+ ## 3. MDC File Support
13
23
 
14
24
  - Supports both `.md` and `.mdc` files (Nuxt Content, Vue)
15
25
  - All patterns auto-expand: `"components"` → `"components/**/*.{md,mdc}"`
16
26
 
17
- ## 3. Rules Filtering
27
+ ## 4. Rules Filtering
18
28
 
19
29
  - `include`/`exclude` glob patterns in `[rules]`
20
30
  - Directory names auto-expand to `directory/**/*.{md,mdc}`
21
31
  - Organize by team/feature, exclude drafts/internal docs
22
32
 
23
- ## 4. Claude Root Folder
33
+ ## 5. Claude Root Folder
24
34
 
25
35
  - Default directory is `.claude/` (no extra flags needed)
26
36
  - Skills already in `.claude/skills` (no copying)
27
37
  - Single directory for all Claude Code config
28
38
 
29
- ## 5. Cursor-style Rules
39
+ ## 6. Cursor-style Rules
30
40
 
31
41
  - `merge_strategy = "cursor"` parses `.mdc` frontmatter
32
42
  - Only includes rules with `alwaysApply: true`
33
43
  - Strips frontmatter, keeps body only
34
44
 
35
- ## 6. Backup Control
45
+ ## 7. Backup Control
36
46
 
37
47
  - `[backup].enabled = false` disables `.bak` files
38
48
 
39
- ## 7. Auto-Generate Skills from Rules
40
-
41
- - `[skills].generate_from_rules = true` creates skills from .mdc files
42
- - Only generates from files with `alwaysApply: false` (or undefined)
43
- - Files with `alwaysApply: true` are merged into AGENTS.md instead
44
- - Automatically removes skills when `alwaysApply` changes to `true`
45
- - Skills use @filename references to original .mdc (for Claude Code)
46
- - MCP agents (excluding Cursor) get full content in .skillz (frontmatter stripped)
47
- - Cursor uses .cursor/rules directly (no skillz MCP needed)
48
- - Globs appended to description: "Applies to files matching: ..."
49
- - **Folder support**: `rules/docx/docx.mdc` + `rules/docx/script.sh` → `skills/docx/SKILL.md` + `skills/docx/script.sh`
50
-
51
49
  ---
52
50
 
53
51
  # Skiller: Centralise Your AI Coding Assistant Instructions
@@ -555,19 +553,13 @@ Skiller uses this configuration with the `merge` (default) or `overwrite` strate
555
553
  export CODEX_HOME="$(pwd)/.codex"
556
554
  ```
557
555
 
558
- ## Skills Support (Experimental)
556
+ ## Skills Support
559
557
 
560
- **⚠️ Experimental Feature**: Skills support is currently experimental and requires `uv` (the Python package manager) to be installed on your system for MCP-based agent integration.
561
-
562
- Skiller can manage and propagate Claude Code-compatible skills to supported AI agents. Skills are stored in `.claude/skills/` and are automatically distributed to compatible agents when you run `skiller apply`.
558
+ Skiller can manage and propagate Claude Code-compatible skills to supported AI agents. Skills are stored in `.claude/skills/` as the **committed source of truth** and are automatically discovered by Claude Code.
563
559
 
564
560
  ### How It Works
565
561
 
566
- Skills are specialized knowledge packages that extend AI agent capabilities with domain-specific expertise, workflows, or tool integrations. Skiller discovers skills in your `.claude/skills/` directory and propagates them to compatible agents:
567
-
568
- - **Claude Code**: Skills copied to `.claude/skills/` with @filename references preserved
569
- - **Cursor**: Uses `.cursor/rules/` directly (copied when `merge_strategy = "cursor"`), no skillz MCP needed
570
- - **Other MCP agents**: Skills copied to `.skillz/` with @filename references expanded to full content (frontmatter stripped), Skillz MCP server auto-configured via `uvx`
562
+ Skills are specialized knowledge packages that extend AI agent capabilities with domain-specific expertise, workflows, or tool integrations. Skiller discovers skills in your `.claude/skills/` directory and keeps them in sync.
571
563
 
572
564
  ### Skills Directory Structure
573
565
 
@@ -575,66 +567,62 @@ Skills can be organized flat or nested:
575
567
 
576
568
  ```
577
569
  .claude/skills/
570
+ ├── my-skill.mdc # Standalone skill file (auto-syncs to folder)
578
571
  ├── my-skill/
579
- ├── SKILL.md # Required: skill instructions/knowledge
572
+ └── SKILL.md # Generated from my-skill.mdc (synced: true)
573
+ ├── another-skill/
574
+ │ ├── SKILL.md # Manually created skill
580
575
  │ ├── helper.py # Optional: additional resources (scripts)
581
576
  │ └── reference.md # Optional: additional resources (docs)
582
- └── another-skill/
583
- └── SKILL.md
584
577
  ```
585
578
 
586
- Each skill must contain:
579
+ Each skill can be defined in two ways:
580
+
581
+ 1. **Standalone `.mdc` file** - Simple skills can be a single `.mdc` file at the skills root
582
+ 2. **Skill folder with `SKILL.md`** - Complex skills with additional resources
587
583
 
588
- - `SKILL.md` - Primary skill file with instructions or knowledge base
584
+ ### Bidirectional Sync
589
585
 
590
- Skills can optionally include additional resources like:
586
+ Skiller provides bidirectional sync between `.mdc` files and `SKILL.md` folders:
591
587
 
592
- - Markdown files with supplementary documentation
593
- - Python, JavaScript, or other scripts
594
- - Configuration files or data
588
+ | Scenario | Sync Direction |
589
+ |----------|---------------|
590
+ | `.mdc` exists, no `SKILL.md` | → Generate `SKILL.md` with `synced: true` |
591
+ | `SKILL.md` has `synced: true` | .mdc → `SKILL.md` (regenerate from .mdc) |
592
+ | `SKILL.md` without `synced: true` | `SKILL.md` → .mdc (SKILL.md is source of truth) |
595
593
 
596
- ### Auto-Generated Skills from Rules (with `generate_from_rules = true`)
594
+ The `synced: true` frontmatter flag indicates that the `.mdc` file is the source of truth:
595
+
596
+ ```yaml
597
+ ---
598
+ name: my-skill
599
+ description: My custom skill
600
+ synced: true
601
+ ---
602
+
603
+ # Skill content here
604
+ ```
597
605
 
598
- When using `[skills].generate_from_rules = true`, skills are automatically created from `.mdc` files in your rules directory. This feature supports folder-based organization:
606
+ ### Skill Folders from Rules
599
607
 
600
- **Folder Support**: If your `.mdc` file is in a folder with the same name, all additional files in that folder are automatically copied to the generated skill:
608
+ Skill folders (directories containing `SKILL.md`) in `.claude/rules/` are automatically copied to `.claude/skills/` during `skiller apply`:
601
609
 
602
610
  ```
603
611
  .claude/rules/docx/
604
- ├── docx.mdc # Main rule (with frontmatter)
612
+ ├── SKILL.md # Makes this a skill folder
605
613
  ├── script.sh # Helper script
606
614
  └── templates/ # Subdirectory
607
615
  └── default.docx # Template file
608
616
 
609
- Generated:
617
+ Copied to:
610
618
 
611
619
  .claude/skills/docx/
612
- ├── SKILL.md # Generated from docx.mdc
620
+ ├── SKILL.md # Copied as-is
613
621
  ├── script.sh # Copied automatically
614
622
  └── templates/ # Copied automatically
615
623
  └── default.docx # Copied automatically
616
624
  ```
617
625
 
618
- **Requirements for folder copying**:
619
-
620
- - The `.mdc` file must be in a folder with the same basename (e.g., `docx/docx.mdc`)
621
- - The `.mdc` file must have frontmatter with `alwaysApply: false` (or undefined)
622
- - All files and subdirectories in that folder (except the `.mdc` file itself) are copied
623
-
624
- **Example `.mdc` file with frontmatter**:
625
-
626
- ```markdown
627
- ---
628
- description: DOCX file processing utilities
629
- globs: ['**/*.docx']
630
- alwaysApply: false
631
- ---
632
-
633
- # DOCX Processing
634
-
635
- Use script.sh to process DOCX files. Templates are in templates/ directory.
636
- ```
637
-
638
626
  ### Configuration
639
627
 
640
628
  Skills support is **enabled by default** but can be controlled via:
@@ -656,46 +644,6 @@ skiller apply --no-skills
656
644
  enabled = true # or false to disable
657
645
  ```
658
646
 
659
- ### Skillz MCP Server
660
-
661
- For agents that support MCP but don't have native skills support (excluding Claude Code and Cursor), Skiller automatically:
662
-
663
- 1. Copies skills to `.skillz/` directory with @filename references expanded to full content
664
- 2. Strips frontmatter from referenced .mdc files to avoid duplication
665
- 3. Configures a Skillz MCP server in the agent's configuration
666
- 4. Uses `uvx` to launch the server with the absolute path to `.skillz`
667
-
668
- **Note**: Cursor is excluded from Skillz MCP because it uses `.cursor/rules/` directory natively.
669
-
670
- Example auto-generated MCP server configuration:
671
-
672
- ```toml
673
- [mcp_servers.skillz]
674
- command = "uvx"
675
- args = ["skillz@latest", "/absolute/path/to/project/.skillz"]
676
- ```
677
-
678
- ### `.gitignore` Integration
679
-
680
- When skills support is enabled and gitignore integration is active, Skiller automatically adds:
681
-
682
- - `.claude/skills/` (when generated from `.claude/rules/` or copied from `.claude/skills/`)
683
- - `.skillz/` (for MCP-based agents excluding Cursor)
684
- - `.cursor/rules/` (when using `merge_strategy = "cursor"`)
685
-
686
- to your `.gitignore` file within the managed Skiller block.
687
-
688
- **Note**: If you manually create `.claude/skills/` without `.claude/rules/`, it won't be gitignored (assumed to be versioned).
689
-
690
- ### Requirements
691
-
692
- - **For Claude Code**: No additional requirements
693
- - **For MCP agents**: `uv` must be installed and available in your PATH
694
- ```bash
695
- # Install uv if needed
696
- curl -LsSf https://astral.sh/uv/install.sh | sh
697
- ```
698
-
699
647
  ### Validation
700
648
 
701
649
  Skiller validates discovered skills and issues warnings for:
@@ -713,32 +661,39 @@ Test skills propagation without making changes:
713
661
  skiller apply --dry-run
714
662
  ```
715
663
 
716
- This shows which skills would be copied and which MCP servers would be configured.
664
+ This shows which skills would be synced and validated.
717
665
 
718
666
  ### Example Workflow
719
667
 
720
668
  ```bash
721
- # 1. Add a skill to your project
722
- mkdir -p .claude/skills/my-skill
723
- cat > .claude/skills/my-skill/SKILL.md << 'EOF'
669
+ # Option 1: Create a skill using .mdc file
670
+ cat > .claude/skills/my-skill.mdc << 'EOF'
671
+ ---
672
+ description: My custom skill
673
+ ---
674
+
724
675
  # My Custom Skill
725
676
 
726
677
  This skill provides specialized knowledge for...
678
+ EOF
679
+
680
+ # Option 2: Create a skill folder directly
681
+ mkdir -p .claude/skills/my-skill
682
+ cat > .claude/skills/my-skill/SKILL.md << 'EOF'
683
+ ---
684
+ name: my-skill
685
+ description: My custom skill
686
+ ---
727
687
 
728
- ## Usage
688
+ # My Custom Skill
729
689
 
730
- When working on this project, always follow these guidelines:
731
- - Use TypeScript for all new code
732
- - Write tests for all features
733
- - Follow the existing code style
690
+ This skill provides specialized knowledge for...
734
691
  EOF
735
692
 
736
- # 2. Apply to all agents (skills enabled by default)
693
+ # Apply to sync skills (runs bidirectional sync)
737
694
  skiller apply
738
695
 
739
- # 3. Skills are now available to compatible agents:
740
- # - Claude Code: .claude/skills/my-skill/
741
- # - Other MCP agents: .skillz/my-skill/ + Skillz MCP server configured
696
+ # Skills are now available to Claude Code via .claude/skills/
742
697
  ```
743
698
 
744
699
  ## `.gitignore` Integration
package/dist/constants.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.SKILLZ_MCP_SERVER_NAME = exports.SKILL_MD_FILENAME = exports.SKILLZ_DIR = exports.CLAUDE_SKILLS_PATH = exports.SKILLS_DIR = exports.DEFAULT_RULES_FILENAME = exports.ERROR_PREFIX = void 0;
3
+ exports.MAX_RECURSION_DEPTH = exports.SKILL_MD_FILENAME = exports.CLAUDE_SKILLS_PATH = exports.SKILLS_DIR = exports.DEFAULT_RULES_FILENAME = exports.ERROR_PREFIX = void 0;
4
4
  exports.actionPrefix = actionPrefix;
5
5
  exports.createSkillerError = createSkillerError;
6
6
  exports.logVerbose = logVerbose;
@@ -51,6 +51,6 @@ function logVerboseInfo(message, isVerbose, dryRun = false) {
51
51
  // Skills-related constants
52
52
  exports.SKILLS_DIR = 'skills';
53
53
  exports.CLAUDE_SKILLS_PATH = '.claude/skills';
54
- exports.SKILLZ_DIR = '.skillz';
55
54
  exports.SKILL_MD_FILENAME = 'SKILL.md';
56
- exports.SKILLZ_MCP_SERVER_NAME = 'skillz';
55
+ // Security: Maximum recursion depth to prevent DoS via deeply nested directories
56
+ exports.MAX_RECURSION_DEPTH = 50;
@@ -78,8 +78,6 @@ const skillerConfigSchema = zod_1.z.object({
78
78
  skills: zod_1.z
79
79
  .object({
80
80
  enabled: zod_1.z.boolean().optional(),
81
- generate_from_rules: zod_1.z.boolean().optional(),
82
- prune: zod_1.z.boolean().optional(),
83
81
  })
84
82
  .optional(),
85
83
  rules: zod_1.z
@@ -233,11 +231,12 @@ async function loadConfig(options) {
233
231
  if (typeof rawSkillsSection.enabled === 'boolean') {
234
232
  skillsConfig.enabled = rawSkillsSection.enabled;
235
233
  }
236
- if (typeof rawSkillsSection.generate_from_rules === 'boolean') {
237
- skillsConfig.generate_from_rules = rawSkillsSection.generate_from_rules;
234
+ // Deprecation warnings for removed config options
235
+ if ('generate_from_rules' in rawSkillsSection) {
236
+ console.warn(`[skiller] Warning: skills.generate_from_rules is deprecated and has no effect. Skills are now edited directly in .claude/skills/`);
238
237
  }
239
- if (typeof rawSkillsSection.prune === 'boolean') {
240
- skillsConfig.prune = rawSkillsSection.prune;
238
+ if ('prune' in rawSkillsSection) {
239
+ console.warn(`[skiller] Warning: skills.prune is deprecated and has no effect. Skills in .claude/skills/ are never auto-deleted.`);
241
240
  }
242
241
  const rawRulesSection = raw.rules && typeof raw.rules === 'object' && !Array.isArray(raw.rules)
243
242
  ? raw.rules
@@ -47,6 +47,7 @@ const fs_1 = require("fs");
47
47
  const path = __importStar(require("path"));
48
48
  const os = __importStar(require("os"));
49
49
  const FrontmatterParser_1 = require("./FrontmatterParser");
50
+ const constants_1 = require("../constants");
50
51
  /**
51
52
  * Gets the XDG config directory path, falling back to ~/.config if XDG_CONFIG_HOME is not set.
52
53
  */
@@ -172,12 +173,16 @@ function matchesPattern(filePath, pattern) {
172
173
  async function readMarkdownFiles(skillerDir, options) {
173
174
  const mdFiles = [];
174
175
  // Gather all markdown files (recursive) first
175
- async function walk(dir) {
176
+ async function walk(dir, depth = 0) {
177
+ // Security: Prevent DoS via deeply nested directories
178
+ if (depth >= constants_1.MAX_RECURSION_DEPTH) {
179
+ return;
180
+ }
176
181
  const entries = await fs_1.promises.readdir(dir, { withFileTypes: true });
177
182
  for (const entry of entries) {
178
183
  const fullPath = path.join(dir, entry.name);
179
184
  if (entry.isDirectory()) {
180
- await walk(fullPath);
185
+ await walk(fullPath, depth + 1);
181
186
  }
182
187
  else if (entry.isFile() &&
183
188
  (entry.name.endsWith('.md') || entry.name.endsWith('.mdc'))) {
@@ -186,7 +191,7 @@ async function readMarkdownFiles(skillerDir, options) {
186
191
  }
187
192
  }
188
193
  }
189
- await walk(skillerDir);
194
+ await walk(skillerDir, 0);
190
195
  // Apply include/exclude filters
191
196
  let filteredFiles = mdFiles;
192
197
  if (options?.include || options?.exclude) {
@@ -358,7 +363,11 @@ exports.findGlobalRulerDir = findGlobalSkillerDir;
358
363
  async function findAllSkillerDirs(startPath) {
359
364
  const skillerDirs = [];
360
365
  // Search the entire directory tree downwards from startPath
361
- async function findSkillerDirsRecursive(dir) {
366
+ async function findSkillerDirsRecursive(dir, depth = 0) {
367
+ // Security: Prevent DoS via deeply nested directories
368
+ if (depth >= constants_1.MAX_RECURSION_DEPTH) {
369
+ return;
370
+ }
362
371
  try {
363
372
  const entries = await fs_1.promises.readdir(dir, { withFileTypes: true });
364
373
  for (const entry of entries) {
@@ -378,7 +387,7 @@ async function findAllSkillerDirs(startPath) {
378
387
  else {
379
388
  // Recursively search subdirectories (but skip hidden directories like .git)
380
389
  if (!entry.name.startsWith('.')) {
381
- await findSkillerDirsRecursive(fullPath);
390
+ await findSkillerDirsRecursive(fullPath, depth + 1);
382
391
  }
383
392
  }
384
393
  }
@@ -389,7 +398,7 @@ async function findAllSkillerDirs(startPath) {
389
398
  }
390
399
  }
391
400
  // Start searching from the startPath
392
- await findSkillerDirsRecursive(startPath);
401
+ await findSkillerDirsRecursive(startPath, 0);
393
402
  // Sort by depth (most specific first) - deeper paths come first
394
403
  skillerDirs.sort((a, b) => {
395
404
  const depthA = a.split(path.sep).length;
@@ -67,7 +67,10 @@ function parseFrontmatter(content) {
67
67
  const [, yamlContent, body] = match;
68
68
  try {
69
69
  // Try parsing YAML as-is first
70
- const parsed = yaml.load(yamlContent);
70
+ // Use JSON_SCHEMA for safety - prevents YAML-specific type coercion attacks
71
+ const parsed = yaml.load(yamlContent, {
72
+ schema: yaml.JSON_SCHEMA,
73
+ });
71
74
  return extractFrontmatter(parsed, body);
72
75
  }
73
76
  catch {
@@ -91,7 +94,9 @@ function parseFrontmatter(content) {
91
94
  });
92
95
  // Try parsing again with fixed YAML
93
96
  if (fixedYaml !== yamlContent) {
94
- const parsed = yaml.load(fixedYaml);
97
+ const parsed = yaml.load(fixedYaml, {
98
+ schema: yaml.JSON_SCHEMA,
99
+ });
95
100
  return extractFrontmatter(parsed, body);
96
101
  }
97
102
  }
@@ -134,6 +139,10 @@ function extractFrontmatter(parsed, body) {
134
139
  if (typeof parsed.alwaysApply === 'boolean') {
135
140
  frontmatter.alwaysApply = parsed.alwaysApply;
136
141
  }
142
+ // Extract name (for SKILL.md)
143
+ if (typeof parsed.name === 'string') {
144
+ frontmatter.name = parsed.name;
145
+ }
137
146
  }
138
147
  return {
139
148
  frontmatter,