@intellectronica/ruler 0.3.18 → 0.3.20

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
@@ -84,6 +84,7 @@ Ruler solves this by providing a **single source of truth** for all your AI agen
84
84
  | Warp | `WARP.md` | - |
85
85
  | Kiro | `.kiro/steering/ruler_kiro_instructions.md` | `.kiro/settings/mcp.json` |
86
86
  | Firebender | `firebender.json` | `firebender.json` (rules and MCP in same file) |
87
+ | Mistral Vibe | `AGENTS.md` | `.vibe/config.toml` |
87
88
 
88
89
  ## Getting Started
89
90
 
@@ -319,7 +320,7 @@ ruler revert [options]
319
320
  | Option | Description |
320
321
  | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
321
322
  | `--project-root <path>` | Path to your project's root (default: current directory) |
322
- | `--agents <agent1,agent2,...>` | Comma-separated list of agent names to revert (agentsmd, aider, amazonqcli, amp, antigravity, augmentcode, claude, cline, codex, copilot, crush, cursor, firebase, firebender, gemini-cli, goose, jules, junie, kilocode, kiro, opencode, openhands, qwen, roo, trae, warp, windsurf, zed) |
323
+ | `--agents <agent1,agent2,...>` | Comma-separated list of agent names to revert (agentsmd, aider, amazonqcli, amp, antigravity, augmentcode, claude, cline, codex, copilot, crush, cursor, firebase, firebender, gemini-cli, goose, jules, junie, kilocode, kiro, mistral, opencode, openhands, qwen, roo, trae, warp, windsurf, zed) |
323
324
  | `--config <path>` | Path to a custom `ruler.toml` configuration file |
324
325
  | `--keep-backups` | Keep backup files (.bak) after restoration (default: false) |
325
326
  | `--dry-run` | Preview changes without actually reverting files |
@@ -557,15 +558,20 @@ export CODEX_HOME="$(pwd)/.codex"
557
558
 
558
559
  ## Skills Support (Experimental)
559
560
 
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
+ **⚠️ 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
562
 
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`.
563
+ 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
564
 
564
565
  ### How It Works
565
566
 
566
567
  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
568
 
568
- - **Claude Code agents**: Skills are copied to `.claude/skills/` in their native format
569
+ - **Agents with native skills support**: Skills are copied directly to each agent's native skills directory:
570
+ - **Claude Code**: `.claude/skills/`
571
+ - **GitHub Copilot**: `.claude/skills/` (shared with Claude Code)
572
+ - **OpenAI Codex CLI**: `.codex/skills/`
573
+ - **OpenCode**: `.opencode/skill/`
574
+ - **Goose**: `.agents/skills/`
569
575
  - **Other MCP-compatible agents**: Skills are copied to `.skillz/` and a Skillz MCP server is automatically configured via `uvx`
570
576
 
571
577
  ### Skills Directory Structure
@@ -615,12 +621,14 @@ enabled = true # or false to disable
615
621
 
616
622
  ### Skillz MCP Server
617
623
 
618
- For agents that support MCP but don't have native skills support (all agents except Claude Code), Ruler automatically:
624
+ For agents that support MCP but don't have native skills support, Ruler automatically:
619
625
 
620
626
  1. Copies skills to `.skillz/` directory
621
627
  2. Configures a Skillz MCP server in the agent's configuration
622
628
  3. Uses `uvx` to launch the server with the absolute path to `.skillz`
623
629
 
630
+ 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.
631
+
624
632
  Example auto-generated MCP server configuration:
625
633
 
626
634
  ```toml
@@ -633,15 +641,18 @@ args = ["skillz@latest", "/absolute/path/to/project/.skillz"]
633
641
 
634
642
  When skills support is enabled and gitignore integration is active, Ruler automatically adds:
635
643
 
636
- - `.claude/skills/` (for Claude Code agents)
637
- - `.skillz/` (for MCP-based agents)
644
+ - `.claude/skills/` (for Claude Code and GitHub Copilot)
645
+ - `.codex/skills/` (for OpenAI Codex CLI)
646
+ - `.opencode/skill/` (for OpenCode)
647
+ - `.agents/skills/` (for Goose)
648
+ - `.skillz/` (for other MCP-based agents)
638
649
 
639
650
  to your `.gitignore` file within the managed Ruler block.
640
651
 
641
652
  ### Requirements
642
653
 
643
- - **For Claude Code**: No additional requirements
644
- - **For MCP agents**: `uv` must be installed and available in your PATH
654
+ - **For agents with native skills support** (Claude Code, GitHub Copilot, OpenAI Codex CLI, OpenCode, Goose): No additional requirements
655
+ - **For other MCP agents**: `uv` must be installed and available in your PATH
645
656
  ```bash
646
657
  # Install uv if needed
647
658
  curl -LsSf https://astral.sh/uv/install.sh | sh
@@ -688,7 +699,10 @@ EOF
688
699
  ruler apply
689
700
 
690
701
  # 3. Skills are now available to compatible agents:
691
- # - Claude Code: .claude/skills/my-skill/
702
+ # - Claude Code & GitHub Copilot: .claude/skills/my-skill/
703
+ # - OpenAI Codex CLI: .codex/skills/my-skill/
704
+ # - OpenCode: .opencode/skill/my-skill/
705
+ # - Goose: .agents/skills/my-skill/
692
706
  # - Other MCP agents: .skillz/my-skill/ + Skillz MCP server configured
693
707
  ```
694
708
 
@@ -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;
@@ -0,0 +1,171 @@
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.MistralVibeAgent = void 0;
37
+ const path = __importStar(require("path"));
38
+ const fs_1 = require("fs");
39
+ const toml_1 = require("@iarna/toml");
40
+ const AgentsMdAgent_1 = require("./AgentsMdAgent");
41
+ const FileSystemUtils_1 = require("../core/FileSystemUtils");
42
+ const constants_1 = require("../constants");
43
+ /**
44
+ * Mistral Vibe CLI agent adapter.
45
+ * Propagates rules to AGENTS.md and MCP servers to .vibe/config.toml.
46
+ */
47
+ class MistralVibeAgent {
48
+ constructor() {
49
+ this.agentsMdAgent = new AgentsMdAgent_1.AgentsMdAgent();
50
+ }
51
+ getIdentifier() {
52
+ return 'mistral';
53
+ }
54
+ getName() {
55
+ return 'Mistral';
56
+ }
57
+ async applyRulerConfig(concatenatedRules, projectRoot, rulerMcpJson, agentConfig, backup = true) {
58
+ // First perform idempotent AGENTS.md write via composed AgentsMdAgent
59
+ await this.agentsMdAgent.applyRulerConfig(concatenatedRules, projectRoot, null, {
60
+ outputPath: agentConfig?.outputPath ||
61
+ agentConfig?.outputPathInstructions ||
62
+ undefined,
63
+ }, backup);
64
+ // Handle MCP configuration
65
+ const defaults = this.getDefaultOutputPath(projectRoot);
66
+ const mcpEnabled = agentConfig?.mcp?.enabled ?? true;
67
+ if (mcpEnabled && rulerMcpJson) {
68
+ // Apply MCP server filtering and transformation
69
+ const { filterMcpConfigForAgent } = await Promise.resolve().then(() => __importStar(require('../mcp/capabilities')));
70
+ const filteredMcpConfig = filterMcpConfigForAgent(rulerMcpJson, this);
71
+ if (!filteredMcpConfig) {
72
+ return; // No compatible servers found
73
+ }
74
+ const filteredRulerMcpJson = filteredMcpConfig;
75
+ // Determine the config file path
76
+ const configPath = agentConfig?.outputPathConfig ?? defaults.config;
77
+ // Ensure the parent directory exists
78
+ await fs_1.promises.mkdir(path.dirname(configPath), { recursive: true });
79
+ // Get the merge strategy
80
+ const strategy = agentConfig?.mcp?.strategy ?? 'merge';
81
+ // Transform ruler MCP servers to Vibe format
82
+ const rulerServers = filteredRulerMcpJson.mcpServers || {};
83
+ const vibeServers = [];
84
+ for (const [serverName, serverConfig] of Object.entries(rulerServers)) {
85
+ const vibeServer = {
86
+ name: serverName,
87
+ transport: this.determineTransport(serverConfig),
88
+ };
89
+ // Handle stdio servers
90
+ if (serverConfig.command) {
91
+ vibeServer.command = serverConfig.command;
92
+ if (serverConfig.args) {
93
+ vibeServer.args = serverConfig.args;
94
+ }
95
+ }
96
+ // Handle remote servers
97
+ if (serverConfig.url) {
98
+ vibeServer.url = serverConfig.url;
99
+ }
100
+ // Handle headers
101
+ if (serverConfig.headers) {
102
+ vibeServer.headers = serverConfig.headers;
103
+ }
104
+ // Handle env
105
+ if (serverConfig.env) {
106
+ vibeServer.env = serverConfig.env;
107
+ }
108
+ vibeServers.push(vibeServer);
109
+ }
110
+ // Read existing TOML config if it exists
111
+ let existingConfig = {};
112
+ try {
113
+ const existingContent = await fs_1.promises.readFile(configPath, 'utf8');
114
+ existingConfig = (0, toml_1.parse)(existingContent);
115
+ }
116
+ catch {
117
+ // File doesn't exist or can't be parsed, use empty config
118
+ }
119
+ // Create the updated config
120
+ const updatedConfig = { ...existingConfig };
121
+ if (strategy === 'overwrite') {
122
+ // For overwrite strategy, replace the entire mcp_servers array
123
+ updatedConfig.mcp_servers = vibeServers;
124
+ }
125
+ else {
126
+ // For merge strategy, merge by server name
127
+ const existingServers = updatedConfig.mcp_servers || [];
128
+ // Keep existing servers that aren't being overwritten by ruler
129
+ const mergedServers = existingServers.filter((s) => !rulerServers[s.name]);
130
+ // Add all ruler servers
131
+ mergedServers.push(...vibeServers);
132
+ updatedConfig.mcp_servers = mergedServers;
133
+ }
134
+ // Convert to TOML and write
135
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
136
+ const tomlContent = (0, toml_1.stringify)(updatedConfig);
137
+ await (0, FileSystemUtils_1.writeGeneratedFile)(configPath, tomlContent);
138
+ }
139
+ }
140
+ /**
141
+ * Determines the transport type based on server configuration.
142
+ */
143
+ determineTransport(server) {
144
+ if (server.command) {
145
+ return 'stdio';
146
+ }
147
+ if (server.url) {
148
+ // Default to http for remote servers
149
+ // Could potentially detect streamable-http based on URL patterns if needed
150
+ return 'http';
151
+ }
152
+ return 'stdio';
153
+ }
154
+ getDefaultOutputPath(projectRoot) {
155
+ return {
156
+ instructions: path.join(projectRoot, constants_1.DEFAULT_RULES_FILENAME),
157
+ config: path.join(projectRoot, '.vibe', 'config.toml'),
158
+ };
159
+ }
160
+ supportsMcpStdio() {
161
+ return true;
162
+ }
163
+ supportsMcpRemote() {
164
+ return true; // Mistral Vibe supports http and streamable-http transports
165
+ }
166
+ supportsNativeSkills() {
167
+ // Mistral Vibe supports native skills in .vibe/skills/
168
+ return true;
169
+ }
170
+ }
171
+ exports.MistralVibeAgent = MistralVibeAgent;
@@ -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;
@@ -32,6 +32,7 @@ const TraeAgent_1 = require("./TraeAgent");
32
32
  const AmazonQCliAgent_1 = require("./AmazonQCliAgent");
33
33
  const FirebenderAgent_1 = require("./FirebenderAgent");
34
34
  const AntigravityAgent_1 = require("./AntigravityAgent");
35
+ const MistralVibeAgent_1 = require("./MistralVibeAgent");
35
36
  exports.allAgents = [
36
37
  new CopilotAgent_1.CopilotAgent(),
37
38
  new ClaudeAgent_1.ClaudeAgent(),
@@ -61,6 +62,7 @@ exports.allAgents = [
61
62
  new AmazonQCliAgent_1.AmazonQCliAgent(),
62
63
  new FirebenderAgent_1.FirebenderAgent(),
63
64
  new AntigravityAgent_1.AntigravityAgent(),
65
+ new MistralVibeAgent_1.MistralVibeAgent(),
64
66
  ];
65
67
  /**
66
68
  * Generates a comma-separated list of agent identifiers for CLI help text.
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.VIBE_SKILLS_PATH = 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,9 @@ 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';
59
+ exports.VIBE_SKILLS_PATH = '.vibe/skills';
57
60
  exports.SKILLZ_DIR = '.skillz';
58
61
  exports.SKILL_MD_FILENAME = 'SKILL.md';
59
62
  exports.SKILLZ_MCP_SERVER_NAME = 'skillz';
@@ -38,6 +38,9 @@ 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;
43
+ exports.propagateSkillsForVibe = propagateSkillsForVibe;
41
44
  exports.propagateSkillsForSkillz = propagateSkillsForSkillz;
42
45
  exports.buildSkillzMcpConfig = buildSkillzMcpConfig;
43
46
  const path = __importStar(require("path"));
@@ -75,10 +78,13 @@ async function getSkillsGitignorePaths(projectRoot) {
75
78
  return [];
76
79
  }
77
80
  // Import here to avoid circular dependency
78
- const { CLAUDE_SKILLS_PATH, CODEX_SKILLS_PATH, SKILLZ_DIR } = await Promise.resolve().then(() => __importStar(require('../constants')));
81
+ const { CLAUDE_SKILLS_PATH, CODEX_SKILLS_PATH, OPENCODE_SKILLS_PATH, GOOSE_SKILLS_PATH, VIBE_SKILLS_PATH, SKILLZ_DIR, } = await Promise.resolve().then(() => __importStar(require('../constants')));
79
82
  return [
80
83
  path.join(projectRoot, CLAUDE_SKILLS_PATH),
81
84
  path.join(projectRoot, CODEX_SKILLS_PATH),
85
+ path.join(projectRoot, OPENCODE_SKILLS_PATH),
86
+ path.join(projectRoot, GOOSE_SKILLS_PATH),
87
+ path.join(projectRoot, VIBE_SKILLS_PATH),
82
88
  path.join(projectRoot, SKILLZ_DIR),
83
89
  ];
84
90
  }
@@ -102,12 +108,15 @@ function warnOnceExperimentalAndUv(verbose, dryRun) {
102
108
  (0, constants_1.logWarn)('Skills MCP server (Skillz) requires uv. Install: https://github.com/astral-sh/uv', dryRun);
103
109
  }
104
110
  /**
105
- * Cleans up skills directories (.claude/skills, .codex/skills and .skillz) when skills are disabled.
111
+ * Cleans up skills directories when skills are disabled.
106
112
  * This ensures that stale skills from previous runs don't persist when skills are turned off.
107
113
  */
108
114
  async function cleanupSkillsDirectories(projectRoot, dryRun, verbose) {
109
115
  const claudeSkillsPath = path.join(projectRoot, constants_1.CLAUDE_SKILLS_PATH);
110
116
  const codexSkillsPath = path.join(projectRoot, constants_1.CODEX_SKILLS_PATH);
117
+ const opencodeSkillsPath = path.join(projectRoot, constants_1.OPENCODE_SKILLS_PATH);
118
+ const gooseSkillsPath = path.join(projectRoot, constants_1.GOOSE_SKILLS_PATH);
119
+ const vibeSkillsPath = path.join(projectRoot, constants_1.VIBE_SKILLS_PATH);
111
120
  const skillzPath = path.join(projectRoot, constants_1.SKILLZ_DIR);
112
121
  // Clean up .claude/skills
113
122
  try {
@@ -137,6 +146,48 @@ async function cleanupSkillsDirectories(projectRoot, dryRun, verbose) {
137
146
  catch {
138
147
  // Directory doesn't exist, nothing to clean
139
148
  }
149
+ // Clean up .opencode/skill
150
+ try {
151
+ await fs.access(opencodeSkillsPath);
152
+ if (dryRun) {
153
+ (0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${constants_1.OPENCODE_SKILLS_PATH}`, verbose, dryRun);
154
+ }
155
+ else {
156
+ await fs.rm(opencodeSkillsPath, { recursive: true, force: true });
157
+ (0, constants_1.logVerboseInfo)(`Removed ${constants_1.OPENCODE_SKILLS_PATH} (skills disabled)`, verbose, dryRun);
158
+ }
159
+ }
160
+ catch {
161
+ // Directory doesn't exist, nothing to clean
162
+ }
163
+ // Clean up .agents/skills
164
+ try {
165
+ await fs.access(gooseSkillsPath);
166
+ if (dryRun) {
167
+ (0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${constants_1.GOOSE_SKILLS_PATH}`, verbose, dryRun);
168
+ }
169
+ else {
170
+ await fs.rm(gooseSkillsPath, { recursive: true, force: true });
171
+ (0, constants_1.logVerboseInfo)(`Removed ${constants_1.GOOSE_SKILLS_PATH} (skills disabled)`, verbose, dryRun);
172
+ }
173
+ }
174
+ catch {
175
+ // Directory doesn't exist, nothing to clean
176
+ }
177
+ // Clean up .vibe/skills
178
+ try {
179
+ await fs.access(vibeSkillsPath);
180
+ if (dryRun) {
181
+ (0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${constants_1.VIBE_SKILLS_PATH}`, verbose, dryRun);
182
+ }
183
+ else {
184
+ await fs.rm(vibeSkillsPath, { recursive: true, force: true });
185
+ (0, constants_1.logVerboseInfo)(`Removed ${constants_1.VIBE_SKILLS_PATH} (skills disabled)`, verbose, dryRun);
186
+ }
187
+ }
188
+ catch {
189
+ // Directory doesn't exist, nothing to clean
190
+ }
140
191
  // Clean up .skillz
141
192
  try {
142
193
  await fs.access(skillzPath);
@@ -199,6 +250,12 @@ async function propagateSkills(projectRoot, agents, skillsEnabled, verbose, dryR
199
250
  await propagateSkillsForClaude(projectRoot, { dryRun });
200
251
  (0, constants_1.logVerboseInfo)(`Copying skills to ${constants_1.CODEX_SKILLS_PATH} for OpenAI Codex CLI`, verbose, dryRun);
201
252
  await propagateSkillsForCodex(projectRoot, { dryRun });
253
+ (0, constants_1.logVerboseInfo)(`Copying skills to ${constants_1.OPENCODE_SKILLS_PATH} for OpenCode`, verbose, dryRun);
254
+ await propagateSkillsForOpenCode(projectRoot, { dryRun });
255
+ (0, constants_1.logVerboseInfo)(`Copying skills to ${constants_1.GOOSE_SKILLS_PATH} for Goose`, verbose, dryRun);
256
+ await propagateSkillsForGoose(projectRoot, { dryRun });
257
+ (0, constants_1.logVerboseInfo)(`Copying skills to ${constants_1.VIBE_SKILLS_PATH} for Mistral Vibe`, verbose, dryRun);
258
+ await propagateSkillsForVibe(projectRoot, { dryRun });
202
259
  }
203
260
  // Copy to .skillz directory if needed
204
261
  if (hasMcpAgent) {
@@ -306,6 +363,156 @@ async function propagateSkillsForCodex(projectRoot, options) {
306
363
  }
307
364
  return [];
308
365
  }
366
+ /**
367
+ * Propagates skills for OpenCode by copying .ruler/skills to .opencode/skill.
368
+ * Uses atomic replace to ensure safe overwriting of existing skills.
369
+ * Returns dry-run steps if dryRun is true, otherwise returns empty array.
370
+ */
371
+ async function propagateSkillsForOpenCode(projectRoot, options) {
372
+ const skillsDir = path.join(projectRoot, constants_1.RULER_SKILLS_PATH);
373
+ const opencodeSkillsPath = path.join(projectRoot, constants_1.OPENCODE_SKILLS_PATH);
374
+ const opencodeDir = path.dirname(opencodeSkillsPath);
375
+ // Check if source skills directory exists
376
+ try {
377
+ await fs.access(skillsDir);
378
+ }
379
+ catch {
380
+ // No skills directory - return empty
381
+ return [];
382
+ }
383
+ if (options.dryRun) {
384
+ return [`Copy skills from ${constants_1.RULER_SKILLS_PATH} to ${constants_1.OPENCODE_SKILLS_PATH}`];
385
+ }
386
+ // Ensure .opencode directory exists
387
+ await fs.mkdir(opencodeDir, { recursive: true });
388
+ // Use atomic replace: copy to temp, then rename
389
+ const tempDir = path.join(opencodeDir, `skill.tmp-${Date.now()}`);
390
+ try {
391
+ // Copy to temp directory
392
+ await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
393
+ // Atomically replace the target
394
+ // First, remove existing target if it exists
395
+ try {
396
+ await fs.rm(opencodeSkillsPath, { recursive: true, force: true });
397
+ }
398
+ catch {
399
+ // Target didn't exist, that's fine
400
+ }
401
+ // Rename temp to target
402
+ await fs.rename(tempDir, opencodeSkillsPath);
403
+ }
404
+ catch (error) {
405
+ // Clean up temp directory on error
406
+ try {
407
+ await fs.rm(tempDir, { recursive: true, force: true });
408
+ }
409
+ catch {
410
+ // Ignore cleanup errors
411
+ }
412
+ throw error;
413
+ }
414
+ return [];
415
+ }
416
+ /**
417
+ * Propagates skills for Goose by copying .ruler/skills to .agents/skills.
418
+ * Uses atomic replace to ensure safe overwriting of existing skills.
419
+ * Returns dry-run steps if dryRun is true, otherwise returns empty array.
420
+ */
421
+ async function propagateSkillsForGoose(projectRoot, options) {
422
+ const skillsDir = path.join(projectRoot, constants_1.RULER_SKILLS_PATH);
423
+ const gooseSkillsPath = path.join(projectRoot, constants_1.GOOSE_SKILLS_PATH);
424
+ const gooseDir = path.dirname(gooseSkillsPath);
425
+ // Check if source skills directory exists
426
+ try {
427
+ await fs.access(skillsDir);
428
+ }
429
+ catch {
430
+ // No skills directory - return empty
431
+ return [];
432
+ }
433
+ if (options.dryRun) {
434
+ return [`Copy skills from ${constants_1.RULER_SKILLS_PATH} to ${constants_1.GOOSE_SKILLS_PATH}`];
435
+ }
436
+ // Ensure .agents directory exists
437
+ await fs.mkdir(gooseDir, { recursive: true });
438
+ // Use atomic replace: copy to temp, then rename
439
+ const tempDir = path.join(gooseDir, `skills.tmp-${Date.now()}`);
440
+ try {
441
+ // Copy to temp directory
442
+ await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
443
+ // Atomically replace the target
444
+ // First, remove existing target if it exists
445
+ try {
446
+ await fs.rm(gooseSkillsPath, { recursive: true, force: true });
447
+ }
448
+ catch {
449
+ // Target didn't exist, that's fine
450
+ }
451
+ // Rename temp to target
452
+ await fs.rename(tempDir, gooseSkillsPath);
453
+ }
454
+ catch (error) {
455
+ // Clean up temp directory on error
456
+ try {
457
+ await fs.rm(tempDir, { recursive: true, force: true });
458
+ }
459
+ catch {
460
+ // Ignore cleanup errors
461
+ }
462
+ throw error;
463
+ }
464
+ return [];
465
+ }
466
+ /**
467
+ * Propagates skills for Mistral Vibe by copying .ruler/skills to .vibe/skills.
468
+ * Uses atomic replace to ensure safe overwriting of existing skills.
469
+ * Returns dry-run steps if dryRun is true, otherwise returns empty array.
470
+ */
471
+ async function propagateSkillsForVibe(projectRoot, options) {
472
+ const skillsDir = path.join(projectRoot, constants_1.RULER_SKILLS_PATH);
473
+ const vibeSkillsPath = path.join(projectRoot, constants_1.VIBE_SKILLS_PATH);
474
+ const vibeDir = path.dirname(vibeSkillsPath);
475
+ // Check if source skills directory exists
476
+ try {
477
+ await fs.access(skillsDir);
478
+ }
479
+ catch {
480
+ // No skills directory - return empty
481
+ return [];
482
+ }
483
+ if (options.dryRun) {
484
+ return [`Copy skills from ${constants_1.RULER_SKILLS_PATH} to ${constants_1.VIBE_SKILLS_PATH}`];
485
+ }
486
+ // Ensure .vibe directory exists
487
+ await fs.mkdir(vibeDir, { recursive: true });
488
+ // Use atomic replace: copy to temp, then rename
489
+ const tempDir = path.join(vibeDir, `skills.tmp-${Date.now()}`);
490
+ try {
491
+ // Copy to temp directory
492
+ await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
493
+ // Atomically replace the target
494
+ // First, remove existing target if it exists
495
+ try {
496
+ await fs.rm(vibeSkillsPath, { recursive: true, force: true });
497
+ }
498
+ catch {
499
+ // Target didn't exist, that's fine
500
+ }
501
+ // Rename temp to target
502
+ await fs.rename(tempDir, vibeSkillsPath);
503
+ }
504
+ catch (error) {
505
+ // Clean up temp directory on error
506
+ try {
507
+ await fs.rm(tempDir, { recursive: true, force: true });
508
+ }
509
+ catch {
510
+ // Ignore cleanup errors
511
+ }
512
+ throw error;
513
+ }
514
+ return [];
515
+ }
309
516
  /**
310
517
  * Propagates skills for MCP agents by copying .ruler/skills to .skillz.
311
518
  * 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.20",
4
4
  "description": "Ruler — apply the same rules to all coding agents",
5
5
  "main": "dist/lib.js",
6
6
  "scripts": {