@intellectronica/ruler 0.3.22 → 0.3.24
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 +17 -6
- package/dist/agents/AbstractAgent.js +7 -0
- package/dist/agents/AmpAgent.js +3 -0
- package/dist/agents/CursorAgent.js +3 -0
- package/dist/agents/GeminiCliAgent.js +3 -0
- package/dist/agents/KiloCodeAgent.js +3 -0
- package/dist/agents/OpenCodeAgent.js +3 -0
- package/dist/agents/RooCodeAgent.js +3 -0
- package/dist/constants.js +4 -1
- package/dist/core/SkillsProcessor.js +210 -3
- package/dist/core/UnifiedConfigLoader.js +6 -0
- package/dist/core/apply-engine.js +35 -3
- package/dist/mcp/propagateOpenCodeMcp.js +6 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -570,11 +570,16 @@ Skills are specialized knowledge packages that extend AI agent capabilities with
|
|
|
570
570
|
- **Agents with native skills support**: Skills are copied directly to each agent's native skills directory:
|
|
571
571
|
- **Claude Code**: `.claude/skills/`
|
|
572
572
|
- **GitHub Copilot**: `.claude/skills/` (shared with Claude Code)
|
|
573
|
+
- **Kilo Code**: `.claude/skills/` (shared with Claude Code)
|
|
573
574
|
- **OpenAI Codex CLI**: `.codex/skills/`
|
|
574
575
|
- **OpenCode**: `.opencode/skill/`
|
|
575
576
|
- **Pi Coding Agent**: `.pi/skills/`
|
|
576
577
|
- **Goose**: `.agents/skills/`
|
|
578
|
+
- **Amp**: `.agents/skills/` (shared with Goose)
|
|
577
579
|
- **Mistral Vibe**: `.vibe/skills/`
|
|
580
|
+
- **Roo Code**: `.roo/skills/`
|
|
581
|
+
- **Gemini CLI**: `.gemini/skills/`
|
|
582
|
+
- **Cursor**: `.cursor/skills/`
|
|
578
583
|
- **Other MCP-compatible agents**: Skills are copied to `.skillz/` and a Skillz MCP server is automatically configured via `uvx`
|
|
579
584
|
|
|
580
585
|
### Skills Directory Structure
|
|
@@ -630,7 +635,7 @@ For agents that support MCP but don't have native skills support, Ruler automati
|
|
|
630
635
|
2. Configures a Skillz MCP server in the agent's configuration
|
|
631
636
|
3. Uses `uvx` to launch the server with the absolute path to `.skillz`
|
|
632
637
|
|
|
633
|
-
Agents using native skills support (Claude Code, GitHub Copilot, OpenAI Codex CLI, OpenCode, Pi Coding Agent, Goose,
|
|
638
|
+
Agents using native skills support (Claude Code, GitHub Copilot, Kilo Code, OpenAI Codex CLI, OpenCode, Pi Coding Agent, Goose, Amp, Mistral Vibe, Roo Code, Gemini CLI, and Cursor) **do not** use the Skillz MCP server and instead use their own native skills directories.
|
|
634
639
|
|
|
635
640
|
Example auto-generated MCP server configuration:
|
|
636
641
|
|
|
@@ -644,19 +649,22 @@ args = ["skillz@latest", "/absolute/path/to/project/.skillz"]
|
|
|
644
649
|
|
|
645
650
|
When skills support is enabled and gitignore integration is active, Ruler automatically adds:
|
|
646
651
|
|
|
647
|
-
- `.claude/skills/` (for Claude Code and
|
|
652
|
+
- `.claude/skills/` (for Claude Code, GitHub Copilot, and Kilo Code)
|
|
648
653
|
- `.codex/skills/` (for OpenAI Codex CLI)
|
|
649
654
|
- `.opencode/skill/` (for OpenCode)
|
|
650
655
|
- `.pi/skills/` (for Pi Coding Agent)
|
|
651
|
-
- `.agents/skills/` (for Goose)
|
|
656
|
+
- `.agents/skills/` (for Goose and Amp)
|
|
652
657
|
- `.vibe/skills/` (for Mistral Vibe)
|
|
658
|
+
- `.roo/skills/` (for Roo Code)
|
|
659
|
+
- `.gemini/skills/` (for Gemini CLI)
|
|
660
|
+
- `.cursor/skills/` (for Cursor)
|
|
653
661
|
- `.skillz/` (for other MCP-based agents)
|
|
654
662
|
|
|
655
663
|
to your `.gitignore` file within the managed Ruler block.
|
|
656
664
|
|
|
657
665
|
### Requirements
|
|
658
666
|
|
|
659
|
-
- **For agents with native skills support** (Claude Code, GitHub Copilot, OpenAI Codex CLI, OpenCode, Pi Coding Agent, Goose, Mistral Vibe): No additional requirements
|
|
667
|
+
- **For agents with native skills support** (Claude Code, GitHub Copilot, Kilo Code, OpenAI Codex CLI, OpenCode, Pi Coding Agent, Goose, Amp, Mistral Vibe, Roo Code, Gemini CLI, Cursor): No additional requirements
|
|
660
668
|
- **For other MCP agents**: `uv` must be installed and available in your PATH
|
|
661
669
|
```bash
|
|
662
670
|
# Install uv if needed
|
|
@@ -704,12 +712,15 @@ EOF
|
|
|
704
712
|
ruler apply
|
|
705
713
|
|
|
706
714
|
# 3. Skills are now available to compatible agents:
|
|
707
|
-
# - Claude Code &
|
|
715
|
+
# - Claude Code, GitHub Copilot & Kilo Code: .claude/skills/my-skill/
|
|
708
716
|
# - OpenAI Codex CLI: .codex/skills/my-skill/
|
|
709
717
|
# - OpenCode: .opencode/skill/my-skill/
|
|
710
718
|
# - Pi Coding Agent: .pi/skills/my-skill/
|
|
711
|
-
# - Goose: .agents/skills/my-skill/
|
|
719
|
+
# - Goose & Amp: .agents/skills/my-skill/
|
|
712
720
|
# - Mistral Vibe: .vibe/skills/my-skill/
|
|
721
|
+
# - Roo Code: .roo/skills/my-skill/
|
|
722
|
+
# - Gemini CLI: .gemini/skills/my-skill/
|
|
723
|
+
# - Cursor: .cursor/skills/my-skill/
|
|
713
724
|
# - Other MCP agents: .skillz/my-skill/ + Skillz MCP server configured
|
|
714
725
|
```
|
|
715
726
|
|
|
@@ -79,6 +79,13 @@ class AbstractAgent {
|
|
|
79
79
|
supportsMcpRemote() {
|
|
80
80
|
return false;
|
|
81
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Returns whether this agent supports MCP server timeout configuration.
|
|
84
|
+
* Defaults to false if not overridden.
|
|
85
|
+
*/
|
|
86
|
+
supportsMcpTimeout() {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
82
89
|
/**
|
|
83
90
|
* Returns whether this agent has native skills support.
|
|
84
91
|
* Defaults to false if not overridden.
|
package/dist/agents/AmpAgent.js
CHANGED
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.VIBE_SKILLS_PATH = exports.GOOSE_SKILLS_PATH = exports.PI_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;
|
|
3
|
+
exports.SKILLZ_MCP_SERVER_NAME = exports.SKILL_MD_FILENAME = exports.SKILLZ_DIR = exports.CURSOR_SKILLS_PATH = exports.GEMINI_SKILLS_PATH = exports.ROO_SKILLS_PATH = exports.VIBE_SKILLS_PATH = exports.GOOSE_SKILLS_PATH = exports.PI_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;
|
|
@@ -58,6 +58,9 @@ exports.OPENCODE_SKILLS_PATH = '.opencode/skill';
|
|
|
58
58
|
exports.PI_SKILLS_PATH = '.pi/skills';
|
|
59
59
|
exports.GOOSE_SKILLS_PATH = '.agents/skills';
|
|
60
60
|
exports.VIBE_SKILLS_PATH = '.vibe/skills';
|
|
61
|
+
exports.ROO_SKILLS_PATH = '.roo/skills';
|
|
62
|
+
exports.GEMINI_SKILLS_PATH = '.gemini/skills';
|
|
63
|
+
exports.CURSOR_SKILLS_PATH = '.cursor/skills';
|
|
61
64
|
exports.SKILLZ_DIR = '.skillz';
|
|
62
65
|
exports.SKILL_MD_FILENAME = 'SKILL.md';
|
|
63
66
|
exports.SKILLZ_MCP_SERVER_NAME = 'skillz';
|
|
@@ -42,6 +42,9 @@ exports.propagateSkillsForOpenCode = propagateSkillsForOpenCode;
|
|
|
42
42
|
exports.propagateSkillsForPi = propagateSkillsForPi;
|
|
43
43
|
exports.propagateSkillsForGoose = propagateSkillsForGoose;
|
|
44
44
|
exports.propagateSkillsForVibe = propagateSkillsForVibe;
|
|
45
|
+
exports.propagateSkillsForRoo = propagateSkillsForRoo;
|
|
46
|
+
exports.propagateSkillsForGemini = propagateSkillsForGemini;
|
|
47
|
+
exports.propagateSkillsForCursor = propagateSkillsForCursor;
|
|
45
48
|
exports.propagateSkillsForSkillz = propagateSkillsForSkillz;
|
|
46
49
|
exports.buildSkillzMcpConfig = buildSkillzMcpConfig;
|
|
47
50
|
const path = __importStar(require("path"));
|
|
@@ -79,7 +82,7 @@ async function getSkillsGitignorePaths(projectRoot) {
|
|
|
79
82
|
return [];
|
|
80
83
|
}
|
|
81
84
|
// Import here to avoid circular dependency
|
|
82
|
-
const { CLAUDE_SKILLS_PATH, CODEX_SKILLS_PATH, OPENCODE_SKILLS_PATH, PI_SKILLS_PATH, GOOSE_SKILLS_PATH, VIBE_SKILLS_PATH, SKILLZ_DIR, } = await Promise.resolve().then(() => __importStar(require('../constants')));
|
|
85
|
+
const { CLAUDE_SKILLS_PATH, CODEX_SKILLS_PATH, OPENCODE_SKILLS_PATH, PI_SKILLS_PATH, GOOSE_SKILLS_PATH, VIBE_SKILLS_PATH, ROO_SKILLS_PATH, GEMINI_SKILLS_PATH, CURSOR_SKILLS_PATH, SKILLZ_DIR, } = await Promise.resolve().then(() => __importStar(require('../constants')));
|
|
83
86
|
return [
|
|
84
87
|
path.join(projectRoot, CLAUDE_SKILLS_PATH),
|
|
85
88
|
path.join(projectRoot, CODEX_SKILLS_PATH),
|
|
@@ -87,6 +90,9 @@ async function getSkillsGitignorePaths(projectRoot) {
|
|
|
87
90
|
path.join(projectRoot, PI_SKILLS_PATH),
|
|
88
91
|
path.join(projectRoot, GOOSE_SKILLS_PATH),
|
|
89
92
|
path.join(projectRoot, VIBE_SKILLS_PATH),
|
|
93
|
+
path.join(projectRoot, ROO_SKILLS_PATH),
|
|
94
|
+
path.join(projectRoot, GEMINI_SKILLS_PATH),
|
|
95
|
+
path.join(projectRoot, CURSOR_SKILLS_PATH),
|
|
90
96
|
path.join(projectRoot, SKILLZ_DIR),
|
|
91
97
|
];
|
|
92
98
|
}
|
|
@@ -120,6 +126,9 @@ async function cleanupSkillsDirectories(projectRoot, dryRun, verbose) {
|
|
|
120
126
|
const piSkillsPath = path.join(projectRoot, constants_1.PI_SKILLS_PATH);
|
|
121
127
|
const gooseSkillsPath = path.join(projectRoot, constants_1.GOOSE_SKILLS_PATH);
|
|
122
128
|
const vibeSkillsPath = path.join(projectRoot, constants_1.VIBE_SKILLS_PATH);
|
|
129
|
+
const rooSkillsPath = path.join(projectRoot, constants_1.ROO_SKILLS_PATH);
|
|
130
|
+
const geminiSkillsPath = path.join(projectRoot, constants_1.GEMINI_SKILLS_PATH);
|
|
131
|
+
const cursorSkillsPath = path.join(projectRoot, constants_1.CURSOR_SKILLS_PATH);
|
|
123
132
|
const skillzPath = path.join(projectRoot, constants_1.SKILLZ_DIR);
|
|
124
133
|
// Clean up .claude/skills
|
|
125
134
|
try {
|
|
@@ -205,6 +214,48 @@ async function cleanupSkillsDirectories(projectRoot, dryRun, verbose) {
|
|
|
205
214
|
catch {
|
|
206
215
|
// Directory doesn't exist, nothing to clean
|
|
207
216
|
}
|
|
217
|
+
// Clean up .roo/skills
|
|
218
|
+
try {
|
|
219
|
+
await fs.access(rooSkillsPath);
|
|
220
|
+
if (dryRun) {
|
|
221
|
+
(0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${constants_1.ROO_SKILLS_PATH}`, verbose, dryRun);
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
await fs.rm(rooSkillsPath, { recursive: true, force: true });
|
|
225
|
+
(0, constants_1.logVerboseInfo)(`Removed ${constants_1.ROO_SKILLS_PATH} (skills disabled)`, verbose, dryRun);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
// Directory doesn't exist, nothing to clean
|
|
230
|
+
}
|
|
231
|
+
// Clean up .gemini/skills
|
|
232
|
+
try {
|
|
233
|
+
await fs.access(geminiSkillsPath);
|
|
234
|
+
if (dryRun) {
|
|
235
|
+
(0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${constants_1.GEMINI_SKILLS_PATH}`, verbose, dryRun);
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
await fs.rm(geminiSkillsPath, { recursive: true, force: true });
|
|
239
|
+
(0, constants_1.logVerboseInfo)(`Removed ${constants_1.GEMINI_SKILLS_PATH} (skills disabled)`, verbose, dryRun);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
// Directory doesn't exist, nothing to clean
|
|
244
|
+
}
|
|
245
|
+
// Clean up .cursor/skills
|
|
246
|
+
try {
|
|
247
|
+
await fs.access(cursorSkillsPath);
|
|
248
|
+
if (dryRun) {
|
|
249
|
+
(0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${constants_1.CURSOR_SKILLS_PATH}`, verbose, dryRun);
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
await fs.rm(cursorSkillsPath, { recursive: true, force: true });
|
|
253
|
+
(0, constants_1.logVerboseInfo)(`Removed ${constants_1.CURSOR_SKILLS_PATH} (skills disabled)`, verbose, dryRun);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
// Directory doesn't exist, nothing to clean
|
|
258
|
+
}
|
|
208
259
|
// Clean up .skillz
|
|
209
260
|
try {
|
|
210
261
|
await fs.access(skillzPath);
|
|
@@ -263,7 +314,7 @@ async function propagateSkills(projectRoot, agents, skillsEnabled, verbose, dryR
|
|
|
263
314
|
}
|
|
264
315
|
// Copy to Claude skills directory if needed
|
|
265
316
|
if (hasNativeSkillsAgent) {
|
|
266
|
-
(0, constants_1.logVerboseInfo)(`Copying skills to ${constants_1.CLAUDE_SKILLS_PATH} for Claude Code and
|
|
317
|
+
(0, constants_1.logVerboseInfo)(`Copying skills to ${constants_1.CLAUDE_SKILLS_PATH} for Claude Code, GitHub Copilot, and Kilo Code`, verbose, dryRun);
|
|
267
318
|
await propagateSkillsForClaude(projectRoot, { dryRun });
|
|
268
319
|
(0, constants_1.logVerboseInfo)(`Copying skills to ${constants_1.CODEX_SKILLS_PATH} for OpenAI Codex CLI`, verbose, dryRun);
|
|
269
320
|
await propagateSkillsForCodex(projectRoot, { dryRun });
|
|
@@ -271,10 +322,16 @@ async function propagateSkills(projectRoot, agents, skillsEnabled, verbose, dryR
|
|
|
271
322
|
await propagateSkillsForOpenCode(projectRoot, { dryRun });
|
|
272
323
|
(0, constants_1.logVerboseInfo)(`Copying skills to ${constants_1.PI_SKILLS_PATH} for Pi Coding Agent`, verbose, dryRun);
|
|
273
324
|
await propagateSkillsForPi(projectRoot, { dryRun });
|
|
274
|
-
(0, constants_1.logVerboseInfo)(`Copying skills to ${constants_1.GOOSE_SKILLS_PATH} for Goose`, verbose, dryRun);
|
|
325
|
+
(0, constants_1.logVerboseInfo)(`Copying skills to ${constants_1.GOOSE_SKILLS_PATH} for Goose and Amp`, verbose, dryRun);
|
|
275
326
|
await propagateSkillsForGoose(projectRoot, { dryRun });
|
|
276
327
|
(0, constants_1.logVerboseInfo)(`Copying skills to ${constants_1.VIBE_SKILLS_PATH} for Mistral Vibe`, verbose, dryRun);
|
|
277
328
|
await propagateSkillsForVibe(projectRoot, { dryRun });
|
|
329
|
+
(0, constants_1.logVerboseInfo)(`Copying skills to ${constants_1.ROO_SKILLS_PATH} for Roo Code`, verbose, dryRun);
|
|
330
|
+
await propagateSkillsForRoo(projectRoot, { dryRun });
|
|
331
|
+
(0, constants_1.logVerboseInfo)(`Copying skills to ${constants_1.GEMINI_SKILLS_PATH} for Gemini CLI`, verbose, dryRun);
|
|
332
|
+
await propagateSkillsForGemini(projectRoot, { dryRun });
|
|
333
|
+
(0, constants_1.logVerboseInfo)(`Copying skills to ${constants_1.CURSOR_SKILLS_PATH} for Cursor`, verbose, dryRun);
|
|
334
|
+
await propagateSkillsForCursor(projectRoot, { dryRun });
|
|
278
335
|
}
|
|
279
336
|
// Copy to .skillz directory if needed
|
|
280
337
|
if (hasMcpAgent) {
|
|
@@ -582,6 +639,156 @@ async function propagateSkillsForVibe(projectRoot, options) {
|
|
|
582
639
|
}
|
|
583
640
|
return [];
|
|
584
641
|
}
|
|
642
|
+
/**
|
|
643
|
+
* Propagates skills for Roo Code by copying .ruler/skills to .roo/skills.
|
|
644
|
+
* Uses atomic replace to ensure safe overwriting of existing skills.
|
|
645
|
+
* Returns dry-run steps if dryRun is true, otherwise returns empty array.
|
|
646
|
+
*/
|
|
647
|
+
async function propagateSkillsForRoo(projectRoot, options) {
|
|
648
|
+
const skillsDir = path.join(projectRoot, constants_1.RULER_SKILLS_PATH);
|
|
649
|
+
const rooSkillsPath = path.join(projectRoot, constants_1.ROO_SKILLS_PATH);
|
|
650
|
+
const rooDir = path.dirname(rooSkillsPath);
|
|
651
|
+
// Check if source skills directory exists
|
|
652
|
+
try {
|
|
653
|
+
await fs.access(skillsDir);
|
|
654
|
+
}
|
|
655
|
+
catch {
|
|
656
|
+
// No skills directory - return empty
|
|
657
|
+
return [];
|
|
658
|
+
}
|
|
659
|
+
if (options.dryRun) {
|
|
660
|
+
return [`Copy skills from ${constants_1.RULER_SKILLS_PATH} to ${constants_1.ROO_SKILLS_PATH}`];
|
|
661
|
+
}
|
|
662
|
+
// Ensure .roo directory exists
|
|
663
|
+
await fs.mkdir(rooDir, { recursive: true });
|
|
664
|
+
// Use atomic replace: copy to temp, then rename
|
|
665
|
+
const tempDir = path.join(rooDir, `skills.tmp-${Date.now()}`);
|
|
666
|
+
try {
|
|
667
|
+
// Copy to temp directory
|
|
668
|
+
await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
|
|
669
|
+
// Atomically replace the target
|
|
670
|
+
// First, remove existing target if it exists
|
|
671
|
+
try {
|
|
672
|
+
await fs.rm(rooSkillsPath, { recursive: true, force: true });
|
|
673
|
+
}
|
|
674
|
+
catch {
|
|
675
|
+
// Target didn't exist, that's fine
|
|
676
|
+
}
|
|
677
|
+
// Rename temp to target
|
|
678
|
+
await fs.rename(tempDir, rooSkillsPath);
|
|
679
|
+
}
|
|
680
|
+
catch (error) {
|
|
681
|
+
// Clean up temp directory on error
|
|
682
|
+
try {
|
|
683
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
684
|
+
}
|
|
685
|
+
catch {
|
|
686
|
+
// Ignore cleanup errors
|
|
687
|
+
}
|
|
688
|
+
throw error;
|
|
689
|
+
}
|
|
690
|
+
return [];
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Propagates skills for Gemini CLI by copying .ruler/skills to .gemini/skills.
|
|
694
|
+
* Uses atomic replace to ensure safe overwriting of existing skills.
|
|
695
|
+
* Returns dry-run steps if dryRun is true, otherwise returns empty array.
|
|
696
|
+
*/
|
|
697
|
+
async function propagateSkillsForGemini(projectRoot, options) {
|
|
698
|
+
const skillsDir = path.join(projectRoot, constants_1.RULER_SKILLS_PATH);
|
|
699
|
+
const geminiSkillsPath = path.join(projectRoot, constants_1.GEMINI_SKILLS_PATH);
|
|
700
|
+
const geminiDir = path.dirname(geminiSkillsPath);
|
|
701
|
+
// Check if source skills directory exists
|
|
702
|
+
try {
|
|
703
|
+
await fs.access(skillsDir);
|
|
704
|
+
}
|
|
705
|
+
catch {
|
|
706
|
+
// No skills directory - return empty
|
|
707
|
+
return [];
|
|
708
|
+
}
|
|
709
|
+
if (options.dryRun) {
|
|
710
|
+
return [`Copy skills from ${constants_1.RULER_SKILLS_PATH} to ${constants_1.GEMINI_SKILLS_PATH}`];
|
|
711
|
+
}
|
|
712
|
+
// Ensure .gemini directory exists
|
|
713
|
+
await fs.mkdir(geminiDir, { recursive: true });
|
|
714
|
+
// Use atomic replace: copy to temp, then rename
|
|
715
|
+
const tempDir = path.join(geminiDir, `skills.tmp-${Date.now()}`);
|
|
716
|
+
try {
|
|
717
|
+
// Copy to temp directory
|
|
718
|
+
await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
|
|
719
|
+
// Atomically replace the target
|
|
720
|
+
// First, remove existing target if it exists
|
|
721
|
+
try {
|
|
722
|
+
await fs.rm(geminiSkillsPath, { recursive: true, force: true });
|
|
723
|
+
}
|
|
724
|
+
catch {
|
|
725
|
+
// Target didn't exist, that's fine
|
|
726
|
+
}
|
|
727
|
+
// Rename temp to target
|
|
728
|
+
await fs.rename(tempDir, geminiSkillsPath);
|
|
729
|
+
}
|
|
730
|
+
catch (error) {
|
|
731
|
+
// Clean up temp directory on error
|
|
732
|
+
try {
|
|
733
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
734
|
+
}
|
|
735
|
+
catch {
|
|
736
|
+
// Ignore cleanup errors
|
|
737
|
+
}
|
|
738
|
+
throw error;
|
|
739
|
+
}
|
|
740
|
+
return [];
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* Propagates skills for Cursor by copying .ruler/skills to .cursor/skills.
|
|
744
|
+
* Uses atomic replace to ensure safe overwriting of existing skills.
|
|
745
|
+
* Returns dry-run steps if dryRun is true, otherwise returns empty array.
|
|
746
|
+
*/
|
|
747
|
+
async function propagateSkillsForCursor(projectRoot, options) {
|
|
748
|
+
const skillsDir = path.join(projectRoot, constants_1.RULER_SKILLS_PATH);
|
|
749
|
+
const cursorSkillsPath = path.join(projectRoot, constants_1.CURSOR_SKILLS_PATH);
|
|
750
|
+
const cursorDir = path.dirname(cursorSkillsPath);
|
|
751
|
+
// Check if source skills directory exists
|
|
752
|
+
try {
|
|
753
|
+
await fs.access(skillsDir);
|
|
754
|
+
}
|
|
755
|
+
catch {
|
|
756
|
+
// No skills directory - return empty
|
|
757
|
+
return [];
|
|
758
|
+
}
|
|
759
|
+
if (options.dryRun) {
|
|
760
|
+
return [`Copy skills from ${constants_1.RULER_SKILLS_PATH} to ${constants_1.CURSOR_SKILLS_PATH}`];
|
|
761
|
+
}
|
|
762
|
+
// Ensure .cursor directory exists
|
|
763
|
+
await fs.mkdir(cursorDir, { recursive: true });
|
|
764
|
+
// Use atomic replace: copy to temp, then rename
|
|
765
|
+
const tempDir = path.join(cursorDir, `skills.tmp-${Date.now()}`);
|
|
766
|
+
try {
|
|
767
|
+
// Copy to temp directory
|
|
768
|
+
await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
|
|
769
|
+
// Atomically replace the target
|
|
770
|
+
// First, remove existing target if it exists
|
|
771
|
+
try {
|
|
772
|
+
await fs.rm(cursorSkillsPath, { recursive: true, force: true });
|
|
773
|
+
}
|
|
774
|
+
catch {
|
|
775
|
+
// Target didn't exist, that's fine
|
|
776
|
+
}
|
|
777
|
+
// Rename temp to target
|
|
778
|
+
await fs.rename(tempDir, cursorSkillsPath);
|
|
779
|
+
}
|
|
780
|
+
catch (error) {
|
|
781
|
+
// Clean up temp directory on error
|
|
782
|
+
try {
|
|
783
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
784
|
+
}
|
|
785
|
+
catch {
|
|
786
|
+
// Ignore cleanup errors
|
|
787
|
+
}
|
|
788
|
+
throw error;
|
|
789
|
+
}
|
|
790
|
+
return [];
|
|
791
|
+
}
|
|
585
792
|
/**
|
|
586
793
|
* Propagates skills for MCP agents by copying .ruler/skills to .skillz.
|
|
587
794
|
* Uses atomic replace to ensure safe overwriting of existing skills.
|
|
@@ -182,6 +182,9 @@ async function loadUnifiedConfig(options) {
|
|
|
182
182
|
if (serverDef.headers && typeof serverDef.headers === 'object') {
|
|
183
183
|
server.headers = Object.fromEntries(Object.entries(serverDef.headers).filter(([, v]) => typeof v === 'string'));
|
|
184
184
|
}
|
|
185
|
+
if (typeof serverDef.timeout === 'number') {
|
|
186
|
+
server.timeout = serverDef.timeout;
|
|
187
|
+
}
|
|
185
188
|
// Validate server configuration
|
|
186
189
|
const hasCommand = !!server.command;
|
|
187
190
|
const hasUrl = !!server.url;
|
|
@@ -301,6 +304,9 @@ async function loadUnifiedConfig(options) {
|
|
|
301
304
|
if (def.headers && typeof def.headers === 'object') {
|
|
302
305
|
server.headers = Object.fromEntries(Object.entries(def.headers).filter(([, v]) => typeof v === 'string'));
|
|
303
306
|
}
|
|
307
|
+
if (typeof def.timeout === 'number') {
|
|
308
|
+
server.timeout = def.timeout;
|
|
309
|
+
}
|
|
304
310
|
// Derive type
|
|
305
311
|
if (server.url)
|
|
306
312
|
server.type = 'remote';
|
|
@@ -427,17 +427,49 @@ async function updateGitignoreForMcpFile(dest, projectRoot, generatedPaths, back
|
|
|
427
427
|
}
|
|
428
428
|
}
|
|
429
429
|
}
|
|
430
|
+
function sanitizeMcpTimeoutsForAgent(agent, mcpJson, dryRun) {
|
|
431
|
+
if (agent.supportsMcpTimeout?.()) {
|
|
432
|
+
return mcpJson;
|
|
433
|
+
}
|
|
434
|
+
if (!mcpJson.mcpServers || typeof mcpJson.mcpServers !== 'object') {
|
|
435
|
+
return mcpJson;
|
|
436
|
+
}
|
|
437
|
+
const servers = mcpJson.mcpServers;
|
|
438
|
+
const sanitizedServers = {};
|
|
439
|
+
const strippedTimeouts = [];
|
|
440
|
+
for (const [name, serverDef] of Object.entries(servers)) {
|
|
441
|
+
if (serverDef && typeof serverDef === 'object') {
|
|
442
|
+
const copy = { ...serverDef };
|
|
443
|
+
if ('timeout' in copy) {
|
|
444
|
+
delete copy.timeout;
|
|
445
|
+
strippedTimeouts.push(name);
|
|
446
|
+
}
|
|
447
|
+
sanitizedServers[name] = copy;
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
sanitizedServers[name] = serverDef;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
if (strippedTimeouts.length > 0) {
|
|
454
|
+
(0, constants_1.logWarn)(`${agent.getName()} does not support MCP server timeout configuration; ignoring timeout for: ${strippedTimeouts.join(', ')}`, dryRun);
|
|
455
|
+
}
|
|
456
|
+
return {
|
|
457
|
+
...mcpJson,
|
|
458
|
+
mcpServers: sanitizedServers,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
430
461
|
async function applyMcpConfiguration(agent, filteredMcpJson, dest, agentConfig, config, projectRoot, cliMcpStrategy, dryRun, verbose, backup = true) {
|
|
431
462
|
// Prevent writing MCP configs outside the project root (e.g., legacy home-directory targets)
|
|
432
463
|
if (!dest.startsWith(projectRoot)) {
|
|
433
464
|
(0, constants_1.logVerbose)(`Skipping MCP config for ${agent.getName()} because target path is outside project: ${dest}`, verbose);
|
|
434
465
|
return;
|
|
435
466
|
}
|
|
467
|
+
const agentMcpJson = sanitizeMcpTimeoutsForAgent(agent, filteredMcpJson, dryRun);
|
|
436
468
|
if (agent.getIdentifier() === 'openhands') {
|
|
437
|
-
return await applyOpenHandsMcpConfiguration(
|
|
469
|
+
return await applyOpenHandsMcpConfiguration(agentMcpJson, dest, dryRun, verbose, backup);
|
|
438
470
|
}
|
|
439
471
|
if (agent.getIdentifier() === 'opencode') {
|
|
440
|
-
return await applyOpenCodeMcpConfiguration(
|
|
472
|
+
return await applyOpenCodeMcpConfiguration(agentMcpJson, dest, dryRun, verbose, backup);
|
|
441
473
|
}
|
|
442
474
|
// Agents that handle MCP configuration internally should not have external MCP handling
|
|
443
475
|
if (agent.getIdentifier() === 'zed' ||
|
|
@@ -447,7 +479,7 @@ async function applyMcpConfiguration(agent, filteredMcpJson, dest, agentConfig,
|
|
|
447
479
|
(0, constants_1.logVerbose)(`Skipping external MCP config for ${agent.getName()} - handled internally by agent`, verbose);
|
|
448
480
|
return;
|
|
449
481
|
}
|
|
450
|
-
return await applyStandardMcpConfiguration(agent,
|
|
482
|
+
return await applyStandardMcpConfiguration(agent, agentMcpJson, dest, agentConfig, config, cliMcpStrategy, dryRun, verbose, backup);
|
|
451
483
|
}
|
|
452
484
|
async function applyOpenHandsMcpConfiguration(filteredMcpJson, dest, dryRun, verbose, backup = true) {
|
|
453
485
|
if (dryRun) {
|
|
@@ -63,6 +63,9 @@ function transformToOpenCodeFormat(rulerMcp) {
|
|
|
63
63
|
if (serverDef.headers) {
|
|
64
64
|
openCodeServer.headers = serverDef.headers;
|
|
65
65
|
}
|
|
66
|
+
if (typeof serverDef.timeout === 'number') {
|
|
67
|
+
openCodeServer.timeout = serverDef.timeout;
|
|
68
|
+
}
|
|
66
69
|
}
|
|
67
70
|
else if (isLocalServer(serverDef)) {
|
|
68
71
|
openCodeServer.type = 'local';
|
|
@@ -74,6 +77,9 @@ function transformToOpenCodeFormat(rulerMcp) {
|
|
|
74
77
|
if (serverDef.env) {
|
|
75
78
|
openCodeServer.environment = serverDef.env;
|
|
76
79
|
}
|
|
80
|
+
if (typeof serverDef.timeout === 'number') {
|
|
81
|
+
openCodeServer.timeout = serverDef.timeout;
|
|
82
|
+
}
|
|
77
83
|
}
|
|
78
84
|
else {
|
|
79
85
|
continue;
|