@intellectronica/ruler 0.3.18 → 0.3.19

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
@@ -557,15 +557,20 @@ export CODEX_HOME="$(pwd)/.codex"
557
557
 
558
558
  ## Skills Support (Experimental)
559
559
 
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.
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 (agents without native skills support).
561
561
 
562
- Ruler can manage and propagate Claude Code-compatible skills to supported AI agents. Skills are stored in `.ruler/skills/` and are automatically distributed to compatible agents when you run `ruler apply`.
562
+ Ruler can manage and propagate skills to supported AI agents. Skills are stored in `.ruler/skills/` and are automatically distributed to compatible agents when you run `ruler apply`.
563
563
 
564
564
  ### How It Works
565
565
 
566
566
  Skills are specialized knowledge packages that extend AI agent capabilities with domain-specific expertise, workflows, or tool integrations. Ruler discovers skills in your `.ruler/skills/` directory and propagates them to compatible agents:
567
567
 
568
- - **Claude Code agents**: Skills are copied to `.claude/skills/` in their native format
568
+ - **Agents with native skills support**: Skills are copied directly to each agent's native skills directory:
569
+ - **Claude Code**: `.claude/skills/`
570
+ - **GitHub Copilot**: `.claude/skills/` (shared with Claude Code)
571
+ - **OpenAI Codex CLI**: `.codex/skills/`
572
+ - **OpenCode**: `.opencode/skill/`
573
+ - **Goose**: `.agents/skills/`
569
574
  - **Other MCP-compatible agents**: Skills are copied to `.skillz/` and a Skillz MCP server is automatically configured via `uvx`
570
575
 
571
576
  ### Skills Directory Structure
@@ -615,12 +620,14 @@ enabled = true # or false to disable
615
620
 
616
621
  ### Skillz MCP Server
617
622
 
618
- For agents that support MCP but don't have native skills support (all agents except Claude Code), Ruler automatically:
623
+ For agents that support MCP but don't have native skills support, Ruler automatically:
619
624
 
620
625
  1. Copies skills to `.skillz/` directory
621
626
  2. Configures a Skillz MCP server in the agent's configuration
622
627
  3. Uses `uvx` to launch the server with the absolute path to `.skillz`
623
628
 
629
+ Agents using native skills support (Claude Code, GitHub Copilot, OpenAI Codex CLI, OpenCode, and Goose) **do not** use the Skillz MCP server and instead use their own native skills directories.
630
+
624
631
  Example auto-generated MCP server configuration:
625
632
 
626
633
  ```toml
@@ -633,15 +640,18 @@ args = ["skillz@latest", "/absolute/path/to/project/.skillz"]
633
640
 
634
641
  When skills support is enabled and gitignore integration is active, Ruler automatically adds:
635
642
 
636
- - `.claude/skills/` (for Claude Code agents)
637
- - `.skillz/` (for MCP-based agents)
643
+ - `.claude/skills/` (for Claude Code and GitHub Copilot)
644
+ - `.codex/skills/` (for OpenAI Codex CLI)
645
+ - `.opencode/skill/` (for OpenCode)
646
+ - `.agents/skills/` (for Goose)
647
+ - `.skillz/` (for other MCP-based agents)
638
648
 
639
649
  to your `.gitignore` file within the managed Ruler block.
640
650
 
641
651
  ### Requirements
642
652
 
643
- - **For Claude Code**: No additional requirements
644
- - **For MCP agents**: `uv` must be installed and available in your PATH
653
+ - **For agents with native skills support** (Claude Code, GitHub Copilot, OpenAI Codex CLI, OpenCode, Goose): No additional requirements
654
+ - **For other MCP agents**: `uv` must be installed and available in your PATH
645
655
  ```bash
646
656
  # Install uv if needed
647
657
  curl -LsSf https://astral.sh/uv/install.sh | sh
@@ -688,7 +698,10 @@ EOF
688
698
  ruler apply
689
699
 
690
700
  # 3. Skills are now available to compatible agents:
691
- # - Claude Code: .claude/skills/my-skill/
701
+ # - Claude Code & GitHub Copilot: .claude/skills/my-skill/
702
+ # - OpenAI Codex CLI: .codex/skills/my-skill/
703
+ # - OpenCode: .opencode/skill/my-skill/
704
+ # - Goose: .agents/skills/my-skill/
692
705
  # - Other MCP agents: .skillz/my-skill/ + Skillz MCP server configured
693
706
  ```
694
707
 
@@ -54,5 +54,8 @@ class GooseAgent extends AbstractAgent_1.AbstractAgent {
54
54
  // Goose doesn't support MCP configuration via local config files
55
55
  return '';
56
56
  }
57
+ supportsNativeSkills() {
58
+ return true;
59
+ }
57
60
  }
58
61
  exports.GooseAgent = GooseAgent;
@@ -95,5 +95,8 @@ class OpenCodeAgent {
95
95
  supportsMcpRemote() {
96
96
  return true;
97
97
  }
98
+ supportsNativeSkills() {
99
+ return true;
100
+ }
98
101
  }
99
102
  exports.OpenCodeAgent = OpenCodeAgent;
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.CODEX_SKILLS_PATH = exports.CLAUDE_SKILLS_PATH = exports.RULER_SKILLS_PATH = exports.SKILLS_DIR = exports.DEFAULT_RULES_FILENAME = exports.ERROR_PREFIX = void 0;
3
+ exports.SKILLZ_MCP_SERVER_NAME = exports.SKILL_MD_FILENAME = exports.SKILLZ_DIR = exports.GOOSE_SKILLS_PATH = exports.OPENCODE_SKILLS_PATH = exports.CODEX_SKILLS_PATH = exports.CLAUDE_SKILLS_PATH = exports.RULER_SKILLS_PATH = exports.SKILLS_DIR = exports.DEFAULT_RULES_FILENAME = exports.ERROR_PREFIX = void 0;
4
4
  exports.actionPrefix = actionPrefix;
5
5
  exports.createRulerError = createRulerError;
6
6
  exports.logVerbose = logVerbose;
@@ -54,6 +54,8 @@ exports.SKILLS_DIR = 'skills';
54
54
  exports.RULER_SKILLS_PATH = '.ruler/skills';
55
55
  exports.CLAUDE_SKILLS_PATH = '.claude/skills';
56
56
  exports.CODEX_SKILLS_PATH = '.codex/skills';
57
+ exports.OPENCODE_SKILLS_PATH = '.opencode/skill';
58
+ exports.GOOSE_SKILLS_PATH = '.agents/skills';
57
59
  exports.SKILLZ_DIR = '.skillz';
58
60
  exports.SKILL_MD_FILENAME = 'SKILL.md';
59
61
  exports.SKILLZ_MCP_SERVER_NAME = 'skillz';
@@ -38,6 +38,8 @@ exports.getSkillsGitignorePaths = getSkillsGitignorePaths;
38
38
  exports.propagateSkills = propagateSkills;
39
39
  exports.propagateSkillsForClaude = propagateSkillsForClaude;
40
40
  exports.propagateSkillsForCodex = propagateSkillsForCodex;
41
+ exports.propagateSkillsForOpenCode = propagateSkillsForOpenCode;
42
+ exports.propagateSkillsForGoose = propagateSkillsForGoose;
41
43
  exports.propagateSkillsForSkillz = propagateSkillsForSkillz;
42
44
  exports.buildSkillzMcpConfig = buildSkillzMcpConfig;
43
45
  const path = __importStar(require("path"));
@@ -75,10 +77,12 @@ async function getSkillsGitignorePaths(projectRoot) {
75
77
  return [];
76
78
  }
77
79
  // Import here to avoid circular dependency
78
- const { CLAUDE_SKILLS_PATH, CODEX_SKILLS_PATH, SKILLZ_DIR } = await Promise.resolve().then(() => __importStar(require('../constants')));
80
+ const { CLAUDE_SKILLS_PATH, CODEX_SKILLS_PATH, OPENCODE_SKILLS_PATH, GOOSE_SKILLS_PATH, SKILLZ_DIR, } = await Promise.resolve().then(() => __importStar(require('../constants')));
79
81
  return [
80
82
  path.join(projectRoot, CLAUDE_SKILLS_PATH),
81
83
  path.join(projectRoot, CODEX_SKILLS_PATH),
84
+ path.join(projectRoot, OPENCODE_SKILLS_PATH),
85
+ path.join(projectRoot, GOOSE_SKILLS_PATH),
82
86
  path.join(projectRoot, SKILLZ_DIR),
83
87
  ];
84
88
  }
@@ -102,12 +106,14 @@ function warnOnceExperimentalAndUv(verbose, dryRun) {
102
106
  (0, constants_1.logWarn)('Skills MCP server (Skillz) requires uv. Install: https://github.com/astral-sh/uv', dryRun);
103
107
  }
104
108
  /**
105
- * Cleans up skills directories (.claude/skills, .codex/skills and .skillz) when skills are disabled.
109
+ * Cleans up skills directories when skills are disabled.
106
110
  * This ensures that stale skills from previous runs don't persist when skills are turned off.
107
111
  */
108
112
  async function cleanupSkillsDirectories(projectRoot, dryRun, verbose) {
109
113
  const claudeSkillsPath = path.join(projectRoot, constants_1.CLAUDE_SKILLS_PATH);
110
114
  const codexSkillsPath = path.join(projectRoot, constants_1.CODEX_SKILLS_PATH);
115
+ const opencodeSkillsPath = path.join(projectRoot, constants_1.OPENCODE_SKILLS_PATH);
116
+ const gooseSkillsPath = path.join(projectRoot, constants_1.GOOSE_SKILLS_PATH);
111
117
  const skillzPath = path.join(projectRoot, constants_1.SKILLZ_DIR);
112
118
  // Clean up .claude/skills
113
119
  try {
@@ -137,6 +143,34 @@ async function cleanupSkillsDirectories(projectRoot, dryRun, verbose) {
137
143
  catch {
138
144
  // Directory doesn't exist, nothing to clean
139
145
  }
146
+ // Clean up .opencode/skill
147
+ try {
148
+ await fs.access(opencodeSkillsPath);
149
+ if (dryRun) {
150
+ (0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${constants_1.OPENCODE_SKILLS_PATH}`, verbose, dryRun);
151
+ }
152
+ else {
153
+ await fs.rm(opencodeSkillsPath, { recursive: true, force: true });
154
+ (0, constants_1.logVerboseInfo)(`Removed ${constants_1.OPENCODE_SKILLS_PATH} (skills disabled)`, verbose, dryRun);
155
+ }
156
+ }
157
+ catch {
158
+ // Directory doesn't exist, nothing to clean
159
+ }
160
+ // Clean up .agents/skills
161
+ try {
162
+ await fs.access(gooseSkillsPath);
163
+ if (dryRun) {
164
+ (0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${constants_1.GOOSE_SKILLS_PATH}`, verbose, dryRun);
165
+ }
166
+ else {
167
+ await fs.rm(gooseSkillsPath, { recursive: true, force: true });
168
+ (0, constants_1.logVerboseInfo)(`Removed ${constants_1.GOOSE_SKILLS_PATH} (skills disabled)`, verbose, dryRun);
169
+ }
170
+ }
171
+ catch {
172
+ // Directory doesn't exist, nothing to clean
173
+ }
140
174
  // Clean up .skillz
141
175
  try {
142
176
  await fs.access(skillzPath);
@@ -199,6 +233,10 @@ async function propagateSkills(projectRoot, agents, skillsEnabled, verbose, dryR
199
233
  await propagateSkillsForClaude(projectRoot, { dryRun });
200
234
  (0, constants_1.logVerboseInfo)(`Copying skills to ${constants_1.CODEX_SKILLS_PATH} for OpenAI Codex CLI`, verbose, dryRun);
201
235
  await propagateSkillsForCodex(projectRoot, { dryRun });
236
+ (0, constants_1.logVerboseInfo)(`Copying skills to ${constants_1.OPENCODE_SKILLS_PATH} for OpenCode`, verbose, dryRun);
237
+ await propagateSkillsForOpenCode(projectRoot, { dryRun });
238
+ (0, constants_1.logVerboseInfo)(`Copying skills to ${constants_1.GOOSE_SKILLS_PATH} for Goose`, verbose, dryRun);
239
+ await propagateSkillsForGoose(projectRoot, { dryRun });
202
240
  }
203
241
  // Copy to .skillz directory if needed
204
242
  if (hasMcpAgent) {
@@ -306,6 +344,106 @@ async function propagateSkillsForCodex(projectRoot, options) {
306
344
  }
307
345
  return [];
308
346
  }
347
+ /**
348
+ * Propagates skills for OpenCode by copying .ruler/skills to .opencode/skill.
349
+ * Uses atomic replace to ensure safe overwriting of existing skills.
350
+ * Returns dry-run steps if dryRun is true, otherwise returns empty array.
351
+ */
352
+ async function propagateSkillsForOpenCode(projectRoot, options) {
353
+ const skillsDir = path.join(projectRoot, constants_1.RULER_SKILLS_PATH);
354
+ const opencodeSkillsPath = path.join(projectRoot, constants_1.OPENCODE_SKILLS_PATH);
355
+ const opencodeDir = path.dirname(opencodeSkillsPath);
356
+ // Check if source skills directory exists
357
+ try {
358
+ await fs.access(skillsDir);
359
+ }
360
+ catch {
361
+ // No skills directory - return empty
362
+ return [];
363
+ }
364
+ if (options.dryRun) {
365
+ return [`Copy skills from ${constants_1.RULER_SKILLS_PATH} to ${constants_1.OPENCODE_SKILLS_PATH}`];
366
+ }
367
+ // Ensure .opencode directory exists
368
+ await fs.mkdir(opencodeDir, { recursive: true });
369
+ // Use atomic replace: copy to temp, then rename
370
+ const tempDir = path.join(opencodeDir, `skill.tmp-${Date.now()}`);
371
+ try {
372
+ // Copy to temp directory
373
+ await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
374
+ // Atomically replace the target
375
+ // First, remove existing target if it exists
376
+ try {
377
+ await fs.rm(opencodeSkillsPath, { recursive: true, force: true });
378
+ }
379
+ catch {
380
+ // Target didn't exist, that's fine
381
+ }
382
+ // Rename temp to target
383
+ await fs.rename(tempDir, opencodeSkillsPath);
384
+ }
385
+ catch (error) {
386
+ // Clean up temp directory on error
387
+ try {
388
+ await fs.rm(tempDir, { recursive: true, force: true });
389
+ }
390
+ catch {
391
+ // Ignore cleanup errors
392
+ }
393
+ throw error;
394
+ }
395
+ return [];
396
+ }
397
+ /**
398
+ * Propagates skills for Goose by copying .ruler/skills to .agents/skills.
399
+ * Uses atomic replace to ensure safe overwriting of existing skills.
400
+ * Returns dry-run steps if dryRun is true, otherwise returns empty array.
401
+ */
402
+ async function propagateSkillsForGoose(projectRoot, options) {
403
+ const skillsDir = path.join(projectRoot, constants_1.RULER_SKILLS_PATH);
404
+ const gooseSkillsPath = path.join(projectRoot, constants_1.GOOSE_SKILLS_PATH);
405
+ const gooseDir = path.dirname(gooseSkillsPath);
406
+ // Check if source skills directory exists
407
+ try {
408
+ await fs.access(skillsDir);
409
+ }
410
+ catch {
411
+ // No skills directory - return empty
412
+ return [];
413
+ }
414
+ if (options.dryRun) {
415
+ return [`Copy skills from ${constants_1.RULER_SKILLS_PATH} to ${constants_1.GOOSE_SKILLS_PATH}`];
416
+ }
417
+ // Ensure .agents directory exists
418
+ await fs.mkdir(gooseDir, { recursive: true });
419
+ // Use atomic replace: copy to temp, then rename
420
+ const tempDir = path.join(gooseDir, `skills.tmp-${Date.now()}`);
421
+ try {
422
+ // Copy to temp directory
423
+ await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
424
+ // Atomically replace the target
425
+ // First, remove existing target if it exists
426
+ try {
427
+ await fs.rm(gooseSkillsPath, { recursive: true, force: true });
428
+ }
429
+ catch {
430
+ // Target didn't exist, that's fine
431
+ }
432
+ // Rename temp to target
433
+ await fs.rename(tempDir, gooseSkillsPath);
434
+ }
435
+ catch (error) {
436
+ // Clean up temp directory on error
437
+ try {
438
+ await fs.rm(tempDir, { recursive: true, force: true });
439
+ }
440
+ catch {
441
+ // Ignore cleanup errors
442
+ }
443
+ throw error;
444
+ }
445
+ return [];
446
+ }
309
447
  /**
310
448
  * Propagates skills for MCP agents by copying .ruler/skills to .skillz.
311
449
  * Uses atomic replace to ensure safe overwriting of existing skills.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intellectronica/ruler",
3
- "version": "0.3.18",
3
+ "version": "0.3.19",
4
4
  "description": "Ruler — apply the same rules to all coding agents",
5
5
  "main": "dist/lib.js",
6
6
  "scripts": {