@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 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 Goose) **do not** use the Skillz MCP server and instead use their own native skills directories.
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;
@@ -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.
@@ -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: amp, copilot, claude, codex, cursor, windsurf, cline, aider, kilocode
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
- const toWrite = sanitizeForFirebase(merged);
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intellectronica/ruler",
3
- "version": "0.3.20",
3
+ "version": "0.3.22",
4
4
  "description": "Ruler — apply the same rules to all coding agents",
5
5
  "main": "dist/lib.js",
6
6
  "scripts": {