@intellectronica/ruler 0.3.20 → 0.3.22
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 +18 -11
- package/dist/agents/GeminiCliAgent.js +20 -2
- package/dist/agents/PiAgent.js +19 -0
- package/dist/agents/index.js +2 -0
- package/dist/cli/handlers.js +11 -1
- package/dist/constants.js +2 -1
- package/dist/core/SkillsProcessor.js +70 -1
- package/dist/core/apply-engine.js +25 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -60,6 +60,7 @@ Ruler solves this by providing a **single source of truth** for all your AI agen
|
|
|
60
60
|
| GitHub Copilot | `AGENTS.md` | `.vscode/mcp.json` |
|
|
61
61
|
| Claude Code | `CLAUDE.md` | `.mcp.json` |
|
|
62
62
|
| OpenAI Codex CLI | `AGENTS.md` | `.codex/config.toml` |
|
|
63
|
+
| Pi Coding Agent | `AGENTS.md` | - |
|
|
63
64
|
| Jules | `AGENTS.md` | - |
|
|
64
65
|
| Cursor | `AGENTS.md` | `.cursor/mcp.json` |
|
|
65
66
|
| Windsurf | `AGENTS.md` | `.windsurf/mcp_config.json` |
|
|
@@ -317,15 +318,15 @@ ruler revert [options]
|
|
|
317
318
|
|
|
318
319
|
### Options
|
|
319
320
|
|
|
320
|
-
| Option | Description
|
|
321
|
-
| ------------------------------ |
|
|
322
|
-
| `--project-root <path>` | Path to your project's root (default: current directory)
|
|
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) |
|
|
324
|
-
| `--config <path>` | Path to a custom `ruler.toml` configuration file
|
|
325
|
-
| `--keep-backups` | Keep backup files (.bak) after restoration (default: false)
|
|
326
|
-
| `--dry-run` | Preview changes without actually reverting files
|
|
327
|
-
| `--verbose` / `-v` | Display detailed output during execution
|
|
328
|
-
| `--local-only` | Only search for local .ruler directories, ignore global config
|
|
321
|
+
| Option | Description |
|
|
322
|
+
| ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
323
|
+
| `--project-root <path>` | Path to your project's root (default: current directory) |
|
|
324
|
+
| `--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, pi, qwen, roo, trae, warp, windsurf, zed) |
|
|
325
|
+
| `--config <path>` | Path to a custom `ruler.toml` configuration file |
|
|
326
|
+
| `--keep-backups` | Keep backup files (.bak) after restoration (default: false) |
|
|
327
|
+
| `--dry-run` | Preview changes without actually reverting files |
|
|
328
|
+
| `--verbose` / `-v` | Display detailed output during execution |
|
|
329
|
+
| `--local-only` | Only search for local .ruler directories, ignore global config |
|
|
329
330
|
|
|
330
331
|
### Common Examples
|
|
331
332
|
|
|
@@ -571,7 +572,9 @@ Skills are specialized knowledge packages that extend AI agent capabilities with
|
|
|
571
572
|
- **GitHub Copilot**: `.claude/skills/` (shared with Claude Code)
|
|
572
573
|
- **OpenAI Codex CLI**: `.codex/skills/`
|
|
573
574
|
- **OpenCode**: `.opencode/skill/`
|
|
575
|
+
- **Pi Coding Agent**: `.pi/skills/`
|
|
574
576
|
- **Goose**: `.agents/skills/`
|
|
577
|
+
- **Mistral Vibe**: `.vibe/skills/`
|
|
575
578
|
- **Other MCP-compatible agents**: Skills are copied to `.skillz/` and a Skillz MCP server is automatically configured via `uvx`
|
|
576
579
|
|
|
577
580
|
### Skills Directory Structure
|
|
@@ -627,7 +630,7 @@ For agents that support MCP but don't have native skills support, Ruler automati
|
|
|
627
630
|
2. Configures a Skillz MCP server in the agent's configuration
|
|
628
631
|
3. Uses `uvx` to launch the server with the absolute path to `.skillz`
|
|
629
632
|
|
|
630
|
-
Agents using native skills support (Claude Code, GitHub Copilot, OpenAI Codex CLI, OpenCode, and
|
|
633
|
+
Agents using native skills support (Claude Code, GitHub Copilot, OpenAI Codex CLI, OpenCode, Pi Coding Agent, Goose, and Mistral Vibe) **do not** use the Skillz MCP server and instead use their own native skills directories.
|
|
631
634
|
|
|
632
635
|
Example auto-generated MCP server configuration:
|
|
633
636
|
|
|
@@ -644,14 +647,16 @@ When skills support is enabled and gitignore integration is active, Ruler automa
|
|
|
644
647
|
- `.claude/skills/` (for Claude Code and GitHub Copilot)
|
|
645
648
|
- `.codex/skills/` (for OpenAI Codex CLI)
|
|
646
649
|
- `.opencode/skill/` (for OpenCode)
|
|
650
|
+
- `.pi/skills/` (for Pi Coding Agent)
|
|
647
651
|
- `.agents/skills/` (for Goose)
|
|
652
|
+
- `.vibe/skills/` (for Mistral Vibe)
|
|
648
653
|
- `.skillz/` (for other MCP-based agents)
|
|
649
654
|
|
|
650
655
|
to your `.gitignore` file within the managed Ruler block.
|
|
651
656
|
|
|
652
657
|
### Requirements
|
|
653
658
|
|
|
654
|
-
- **For agents with native skills support** (Claude Code, GitHub Copilot, OpenAI Codex CLI, OpenCode, Goose): No additional requirements
|
|
659
|
+
- **For agents with native skills support** (Claude Code, GitHub Copilot, OpenAI Codex CLI, OpenCode, Pi Coding Agent, Goose, Mistral Vibe): No additional requirements
|
|
655
660
|
- **For other MCP agents**: `uv` must be installed and available in your PATH
|
|
656
661
|
```bash
|
|
657
662
|
# Install uv if needed
|
|
@@ -702,7 +707,9 @@ ruler apply
|
|
|
702
707
|
# - Claude Code & GitHub Copilot: .claude/skills/my-skill/
|
|
703
708
|
# - OpenAI Codex CLI: .codex/skills/my-skill/
|
|
704
709
|
# - OpenCode: .opencode/skill/my-skill/
|
|
710
|
+
# - Pi Coding Agent: .pi/skills/my-skill/
|
|
705
711
|
# - Goose: .agents/skills/my-skill/
|
|
712
|
+
# - Mistral Vibe: .vibe/skills/my-skill/
|
|
706
713
|
# - Other MCP agents: .skillz/my-skill/ + Skillz MCP server configured
|
|
707
714
|
```
|
|
708
715
|
|
|
@@ -69,17 +69,35 @@ class GeminiCliAgent extends AgentsMdAgent_1.AgentsMdAgent {
|
|
|
69
69
|
const mcpEnabled = agentConfig?.mcp?.enabled ?? true;
|
|
70
70
|
if (mcpEnabled && rulerMcpJson) {
|
|
71
71
|
const strategy = agentConfig?.mcp?.strategy ?? 'merge';
|
|
72
|
+
// Gemini CLI (since v0.21.0) no longer accepts the "type" field in MCP server entries.
|
|
73
|
+
// Following the MCP spec update from Nov 25, 2025, the transport type is now inferred
|
|
74
|
+
// from the presence of specific keys (command/args -> stdio, url -> sse/http).
|
|
75
|
+
// Strip 'type' field from all incoming servers before merging.
|
|
76
|
+
const stripTypeField = (servers) => {
|
|
77
|
+
const cleaned = {};
|
|
78
|
+
for (const [name, def] of Object.entries(servers)) {
|
|
79
|
+
if (def && typeof def === 'object') {
|
|
80
|
+
const copy = { ...def };
|
|
81
|
+
delete copy.type;
|
|
82
|
+
cleaned[name] = copy;
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
cleaned[name] = def;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return cleaned;
|
|
89
|
+
};
|
|
72
90
|
if (strategy === 'overwrite') {
|
|
73
91
|
// For overwrite, preserve existing settings except MCP servers
|
|
74
92
|
const incomingServers = rulerMcpJson.mcpServers || {};
|
|
75
|
-
updated[this.getMcpServerKey()] = incomingServers;
|
|
93
|
+
updated[this.getMcpServerKey()] = stripTypeField(incomingServers);
|
|
76
94
|
}
|
|
77
95
|
else {
|
|
78
96
|
// For merge strategy, merge with existing MCP servers
|
|
79
97
|
const baseServers = existingSettings[this.getMcpServerKey()] || {};
|
|
80
98
|
const incomingServers = rulerMcpJson.mcpServers || {};
|
|
81
99
|
const mergedServers = { ...baseServers, ...incomingServers };
|
|
82
|
-
updated[this.getMcpServerKey()] = mergedServers;
|
|
100
|
+
updated[this.getMcpServerKey()] = stripTypeField(mergedServers);
|
|
83
101
|
}
|
|
84
102
|
}
|
|
85
103
|
await fs_1.promises.mkdir(path.dirname(settingsPath), { recursive: true });
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PiAgent = void 0;
|
|
4
|
+
const AgentsMdAgent_1 = require("./AgentsMdAgent");
|
|
5
|
+
/**
|
|
6
|
+
* Pi Coding Agent adapter.
|
|
7
|
+
*/
|
|
8
|
+
class PiAgent extends AgentsMdAgent_1.AgentsMdAgent {
|
|
9
|
+
getIdentifier() {
|
|
10
|
+
return 'pi';
|
|
11
|
+
}
|
|
12
|
+
getName() {
|
|
13
|
+
return 'Pi Coding Agent';
|
|
14
|
+
}
|
|
15
|
+
supportsNativeSkills() {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
exports.PiAgent = PiAgent;
|
package/dist/agents/index.js
CHANGED
|
@@ -33,6 +33,7 @@ const AmazonQCliAgent_1 = require("./AmazonQCliAgent");
|
|
|
33
33
|
const FirebenderAgent_1 = require("./FirebenderAgent");
|
|
34
34
|
const AntigravityAgent_1 = require("./AntigravityAgent");
|
|
35
35
|
const MistralVibeAgent_1 = require("./MistralVibeAgent");
|
|
36
|
+
const PiAgent_1 = require("./PiAgent");
|
|
36
37
|
exports.allAgents = [
|
|
37
38
|
new CopilotAgent_1.CopilotAgent(),
|
|
38
39
|
new ClaudeAgent_1.ClaudeAgent(),
|
|
@@ -63,6 +64,7 @@ exports.allAgents = [
|
|
|
63
64
|
new FirebenderAgent_1.FirebenderAgent(),
|
|
64
65
|
new AntigravityAgent_1.AntigravityAgent(),
|
|
65
66
|
new MistralVibeAgent_1.MistralVibeAgent(),
|
|
67
|
+
new PiAgent_1.PiAgent(),
|
|
66
68
|
];
|
|
67
69
|
/**
|
|
68
70
|
* Generates a comma-separated list of agent identifiers for CLI help text.
|
package/dist/cli/handlers.js
CHANGED
|
@@ -43,11 +43,20 @@ const os = __importStar(require("os"));
|
|
|
43
43
|
const fs = __importStar(require("fs/promises"));
|
|
44
44
|
const constants_1 = require("../constants");
|
|
45
45
|
const ConfigLoader_1 = require("../core/ConfigLoader");
|
|
46
|
+
function assertNotInsideRulerDir(projectRoot) {
|
|
47
|
+
const normalized = path.resolve(projectRoot);
|
|
48
|
+
const segments = normalized.split(path.sep);
|
|
49
|
+
if (segments.includes('.ruler')) {
|
|
50
|
+
console.error(`${constants_1.ERROR_PREFIX} Cannot run from inside a .ruler directory. Please run from your project root.`);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
46
54
|
/**
|
|
47
55
|
* Handler for the 'apply' command.
|
|
48
56
|
*/
|
|
49
57
|
async function applyHandler(argv) {
|
|
50
58
|
const projectRoot = argv['project-root'];
|
|
59
|
+
assertNotInsideRulerDir(projectRoot);
|
|
51
60
|
const agents = argv.agents
|
|
52
61
|
? argv.agents.split(',').map((a) => a.trim())
|
|
53
62
|
: undefined;
|
|
@@ -143,7 +152,7 @@ async function initHandler(argv) {
|
|
|
143
152
|
|
|
144
153
|
# --- Agent Specific Configurations ---
|
|
145
154
|
# You can enable/disable agents and override their default output paths here.
|
|
146
|
-
# Use lowercase agent identifiers:
|
|
155
|
+
# Use lowercase agent identifiers: aider, amp, claude, cline, codex, copilot, cursor, kilocode, pi, windsurf
|
|
147
156
|
|
|
148
157
|
# [agents.copilot]
|
|
149
158
|
# enabled = true
|
|
@@ -192,6 +201,7 @@ async function initHandler(argv) {
|
|
|
192
201
|
*/
|
|
193
202
|
async function revertHandler(argv) {
|
|
194
203
|
const projectRoot = argv['project-root'];
|
|
204
|
+
assertNotInsideRulerDir(projectRoot);
|
|
195
205
|
const agents = argv.agents
|
|
196
206
|
? argv.agents.split(',').map((a) => a.trim())
|
|
197
207
|
: undefined;
|
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.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.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;
|
|
@@ -55,6 +55,7 @@ exports.RULER_SKILLS_PATH = '.ruler/skills';
|
|
|
55
55
|
exports.CLAUDE_SKILLS_PATH = '.claude/skills';
|
|
56
56
|
exports.CODEX_SKILLS_PATH = '.codex/skills';
|
|
57
57
|
exports.OPENCODE_SKILLS_PATH = '.opencode/skill';
|
|
58
|
+
exports.PI_SKILLS_PATH = '.pi/skills';
|
|
58
59
|
exports.GOOSE_SKILLS_PATH = '.agents/skills';
|
|
59
60
|
exports.VIBE_SKILLS_PATH = '.vibe/skills';
|
|
60
61
|
exports.SKILLZ_DIR = '.skillz';
|
|
@@ -39,6 +39,7 @@ exports.propagateSkills = propagateSkills;
|
|
|
39
39
|
exports.propagateSkillsForClaude = propagateSkillsForClaude;
|
|
40
40
|
exports.propagateSkillsForCodex = propagateSkillsForCodex;
|
|
41
41
|
exports.propagateSkillsForOpenCode = propagateSkillsForOpenCode;
|
|
42
|
+
exports.propagateSkillsForPi = propagateSkillsForPi;
|
|
42
43
|
exports.propagateSkillsForGoose = propagateSkillsForGoose;
|
|
43
44
|
exports.propagateSkillsForVibe = propagateSkillsForVibe;
|
|
44
45
|
exports.propagateSkillsForSkillz = propagateSkillsForSkillz;
|
|
@@ -78,11 +79,12 @@ async function getSkillsGitignorePaths(projectRoot) {
|
|
|
78
79
|
return [];
|
|
79
80
|
}
|
|
80
81
|
// Import here to avoid circular dependency
|
|
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')));
|
|
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')));
|
|
82
83
|
return [
|
|
83
84
|
path.join(projectRoot, CLAUDE_SKILLS_PATH),
|
|
84
85
|
path.join(projectRoot, CODEX_SKILLS_PATH),
|
|
85
86
|
path.join(projectRoot, OPENCODE_SKILLS_PATH),
|
|
87
|
+
path.join(projectRoot, PI_SKILLS_PATH),
|
|
86
88
|
path.join(projectRoot, GOOSE_SKILLS_PATH),
|
|
87
89
|
path.join(projectRoot, VIBE_SKILLS_PATH),
|
|
88
90
|
path.join(projectRoot, SKILLZ_DIR),
|
|
@@ -115,6 +117,7 @@ async function cleanupSkillsDirectories(projectRoot, dryRun, verbose) {
|
|
|
115
117
|
const claudeSkillsPath = path.join(projectRoot, constants_1.CLAUDE_SKILLS_PATH);
|
|
116
118
|
const codexSkillsPath = path.join(projectRoot, constants_1.CODEX_SKILLS_PATH);
|
|
117
119
|
const opencodeSkillsPath = path.join(projectRoot, constants_1.OPENCODE_SKILLS_PATH);
|
|
120
|
+
const piSkillsPath = path.join(projectRoot, constants_1.PI_SKILLS_PATH);
|
|
118
121
|
const gooseSkillsPath = path.join(projectRoot, constants_1.GOOSE_SKILLS_PATH);
|
|
119
122
|
const vibeSkillsPath = path.join(projectRoot, constants_1.VIBE_SKILLS_PATH);
|
|
120
123
|
const skillzPath = path.join(projectRoot, constants_1.SKILLZ_DIR);
|
|
@@ -160,6 +163,20 @@ async function cleanupSkillsDirectories(projectRoot, dryRun, verbose) {
|
|
|
160
163
|
catch {
|
|
161
164
|
// Directory doesn't exist, nothing to clean
|
|
162
165
|
}
|
|
166
|
+
// Clean up .pi/skills
|
|
167
|
+
try {
|
|
168
|
+
await fs.access(piSkillsPath);
|
|
169
|
+
if (dryRun) {
|
|
170
|
+
(0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${constants_1.PI_SKILLS_PATH}`, verbose, dryRun);
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
await fs.rm(piSkillsPath, { recursive: true, force: true });
|
|
174
|
+
(0, constants_1.logVerboseInfo)(`Removed ${constants_1.PI_SKILLS_PATH} (skills disabled)`, verbose, dryRun);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
// Directory doesn't exist, nothing to clean
|
|
179
|
+
}
|
|
163
180
|
// Clean up .agents/skills
|
|
164
181
|
try {
|
|
165
182
|
await fs.access(gooseSkillsPath);
|
|
@@ -252,6 +269,8 @@ async function propagateSkills(projectRoot, agents, skillsEnabled, verbose, dryR
|
|
|
252
269
|
await propagateSkillsForCodex(projectRoot, { dryRun });
|
|
253
270
|
(0, constants_1.logVerboseInfo)(`Copying skills to ${constants_1.OPENCODE_SKILLS_PATH} for OpenCode`, verbose, dryRun);
|
|
254
271
|
await propagateSkillsForOpenCode(projectRoot, { dryRun });
|
|
272
|
+
(0, constants_1.logVerboseInfo)(`Copying skills to ${constants_1.PI_SKILLS_PATH} for Pi Coding Agent`, verbose, dryRun);
|
|
273
|
+
await propagateSkillsForPi(projectRoot, { dryRun });
|
|
255
274
|
(0, constants_1.logVerboseInfo)(`Copying skills to ${constants_1.GOOSE_SKILLS_PATH} for Goose`, verbose, dryRun);
|
|
256
275
|
await propagateSkillsForGoose(projectRoot, { dryRun });
|
|
257
276
|
(0, constants_1.logVerboseInfo)(`Copying skills to ${constants_1.VIBE_SKILLS_PATH} for Mistral Vibe`, verbose, dryRun);
|
|
@@ -413,6 +432,56 @@ async function propagateSkillsForOpenCode(projectRoot, options) {
|
|
|
413
432
|
}
|
|
414
433
|
return [];
|
|
415
434
|
}
|
|
435
|
+
/**
|
|
436
|
+
* Propagates skills for Pi Coding Agent by copying .ruler/skills to .pi/skills.
|
|
437
|
+
* Uses atomic replace to ensure safe overwriting of existing skills.
|
|
438
|
+
* Returns dry-run steps if dryRun is true, otherwise returns empty array.
|
|
439
|
+
*/
|
|
440
|
+
async function propagateSkillsForPi(projectRoot, options) {
|
|
441
|
+
const skillsDir = path.join(projectRoot, constants_1.RULER_SKILLS_PATH);
|
|
442
|
+
const piSkillsPath = path.join(projectRoot, constants_1.PI_SKILLS_PATH);
|
|
443
|
+
const piDir = path.dirname(piSkillsPath);
|
|
444
|
+
// Check if source skills directory exists
|
|
445
|
+
try {
|
|
446
|
+
await fs.access(skillsDir);
|
|
447
|
+
}
|
|
448
|
+
catch {
|
|
449
|
+
// No skills directory - return empty
|
|
450
|
+
return [];
|
|
451
|
+
}
|
|
452
|
+
if (options.dryRun) {
|
|
453
|
+
return [`Copy skills from ${constants_1.RULER_SKILLS_PATH} to ${constants_1.PI_SKILLS_PATH}`];
|
|
454
|
+
}
|
|
455
|
+
// Ensure .pi directory exists
|
|
456
|
+
await fs.mkdir(piDir, { recursive: true });
|
|
457
|
+
// Use atomic replace: copy to temp, then rename
|
|
458
|
+
const tempDir = path.join(piDir, `skills.tmp-${Date.now()}`);
|
|
459
|
+
try {
|
|
460
|
+
// Copy to temp directory
|
|
461
|
+
await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
|
|
462
|
+
// Atomically replace the target
|
|
463
|
+
// First, remove existing target if it exists
|
|
464
|
+
try {
|
|
465
|
+
await fs.rm(piSkillsPath, { recursive: true, force: true });
|
|
466
|
+
}
|
|
467
|
+
catch {
|
|
468
|
+
// Target didn't exist, that's fine
|
|
469
|
+
}
|
|
470
|
+
// Rename temp to target
|
|
471
|
+
await fs.rename(tempDir, piSkillsPath);
|
|
472
|
+
}
|
|
473
|
+
catch (error) {
|
|
474
|
+
// Clean up temp directory on error
|
|
475
|
+
try {
|
|
476
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
477
|
+
}
|
|
478
|
+
catch {
|
|
479
|
+
// Ignore cleanup errors
|
|
480
|
+
}
|
|
481
|
+
throw error;
|
|
482
|
+
}
|
|
483
|
+
return [];
|
|
484
|
+
}
|
|
416
485
|
/**
|
|
417
486
|
* Propagates skills for Goose by copying .ruler/skills to .agents/skills.
|
|
418
487
|
* Uses atomic replace to ensure safe overwriting of existing skills.
|
|
@@ -577,7 +577,31 @@ async function applyStandardMcpConfiguration(agent, filteredMcpJson, dest, agent
|
|
|
577
577
|
out[serverKey] = cleanedServers;
|
|
578
578
|
return out;
|
|
579
579
|
};
|
|
580
|
-
|
|
580
|
+
// Gemini CLI (since v0.21.0) no longer accepts the "type" field in MCP server entries.
|
|
581
|
+
// Following the MCP spec update from Nov 25, 2025, the transport type is now inferred
|
|
582
|
+
// from the presence of specific keys (command/args -> stdio, url -> sse/http).
|
|
583
|
+
// Sanitize merged config by stripping 'type' from each server when targeting Gemini.
|
|
584
|
+
const sanitizeForGemini = (obj) => {
|
|
585
|
+
if (agent.getIdentifier() !== 'gemini-cli')
|
|
586
|
+
return obj;
|
|
587
|
+
const out = { ...obj };
|
|
588
|
+
const servers = out[serverKey] || {};
|
|
589
|
+
const cleanedServers = {};
|
|
590
|
+
for (const [name, def] of Object.entries(servers)) {
|
|
591
|
+
if (def && typeof def === 'object') {
|
|
592
|
+
const copy = { ...def };
|
|
593
|
+
delete copy.type;
|
|
594
|
+
cleanedServers[name] = copy;
|
|
595
|
+
}
|
|
596
|
+
else {
|
|
597
|
+
cleanedServers[name] = def;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
out[serverKey] = cleanedServers;
|
|
601
|
+
return out;
|
|
602
|
+
};
|
|
603
|
+
let toWrite = sanitizeForFirebase(merged);
|
|
604
|
+
toWrite = sanitizeForGemini(toWrite);
|
|
581
605
|
// Only backup and write if content would actually change (idempotent)
|
|
582
606
|
const currentContent = JSON.stringify(existing, null, 2);
|
|
583
607
|
const newContent = JSON.stringify(toWrite, null, 2);
|