@intellectronica/ruler 0.3.1 → 0.3.3

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
@@ -75,6 +75,7 @@ Ruler solves this by providing a **single source of truth** for all your AI agen
75
75
  | opencode | `AGENTS.md` | `opencode.json` |
76
76
  | Goose | `.goosehints` | - |
77
77
  | Qwen Code | `AGENTS.md` | `.qwen/settings.json` |
78
+ | RooCode | `AGENTS.md` | `.roo/mcp.json` |
78
79
  | Zed | `AGENTS.md` | `.zed/settings.json` (project root, never $HOME) |
79
80
  | Warp | `WARP.md` | - |
80
81
  | Kiro | `.kiro/steering/ruler_kiro_instructions.md` | - |
@@ -214,7 +215,7 @@ The `apply` command looks for `.ruler/` in the current directory tree, reading t
214
215
  | Option | Description |
215
216
  | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
216
217
  | `--project-root <path>` | Path to your project's root (default: current directory) |
217
- | `--agents <agent1,agent2,...>` | Comma-separated list of agent names to target (agentsmd, amp, copilot, claude, codex, cursor, windsurf, cline, aider, firebase, gemini-cli, junie, augmentcode, kilocode, warp) |
218
+ | `--agents <agent1,agent2,...>` | Comma-separated list of agent names to target (agentsmd, amp, copilot, claude, codex, cursor, windsurf, cline, aider, firebase, gemini-cli, junie, augmentcode, kilocode, warp, roo) |
218
219
  | `--config <path>` | Path to a custom `ruler.toml` configuration file |
219
220
  | `--mcp` / `--with-mcp` | Enable applying MCP server configurations (default: true) |
220
221
  | `--no-mcp` | Disable applying MCP server configurations |
@@ -250,6 +251,12 @@ ruler apply --agents firebase
250
251
  ruler apply --agents warp
251
252
  ```
252
253
 
254
+ **Apply rules only to RooCode:**
255
+
256
+ ```bash
257
+ ruler apply --agents roo
258
+ ```
259
+
253
260
  **Use a specific configuration file:**
254
261
 
255
262
  ```bash
@@ -50,11 +50,13 @@ class AbstractAgent {
50
50
  * 4. Writing the new content
51
51
  */
52
52
  async applyRulerConfig(concatenatedRules, projectRoot, rulerMcpJson, // eslint-disable-line @typescript-eslint/no-unused-vars
53
- agentConfig) {
53
+ agentConfig, backup = true) {
54
54
  const output = agentConfig?.outputPath ?? this.getDefaultOutputPath(projectRoot);
55
55
  const absolutePath = path.resolve(projectRoot, output);
56
56
  await (0, FileSystemUtils_1.ensureDirExists)(path.dirname(absolutePath));
57
- await (0, FileSystemUtils_1.backupFile)(absolutePath);
57
+ if (backup) {
58
+ await (0, FileSystemUtils_1.backupFile)(absolutePath);
59
+ }
58
60
  await (0, FileSystemUtils_1.writeGeneratedFile)(absolutePath, concatenatedRules);
59
61
  }
60
62
  /**
@@ -54,7 +54,7 @@ class AgentsMdAgent extends AbstractAgent_1.AbstractAgent {
54
54
  return path.join(projectRoot, 'AGENTS.md');
55
55
  }
56
56
  async applyRulerConfig(concatenatedRules, projectRoot, rulerMcpJson, // eslint-disable-line @typescript-eslint/no-unused-vars
57
- agentConfig) {
57
+ agentConfig, backup = true) {
58
58
  const output = agentConfig?.outputPath ?? this.getDefaultOutputPath(projectRoot);
59
59
  const absolutePath = path.resolve(projectRoot, output);
60
60
  await (0, FileSystemUtils_1.ensureDirExists)(path.dirname(absolutePath));
@@ -72,8 +72,10 @@ class AgentsMdAgent extends AbstractAgent_1.AbstractAgent {
72
72
  // No change; skip backup/write for idempotency
73
73
  return;
74
74
  }
75
- // Backup (only if file existed) then write new content
76
- await (0, FileSystemUtils_1.backupFile)(absolutePath);
75
+ // Backup (only if file existed and backup is enabled) then write new content
76
+ if (backup) {
77
+ await (0, FileSystemUtils_1.backupFile)(absolutePath);
78
+ }
77
79
  await (0, FileSystemUtils_1.writeGeneratedFile)(absolutePath, contentWithMarker);
78
80
  }
79
81
  getMcpServerKey() {
@@ -52,21 +52,23 @@ class AiderAgent {
52
52
  getName() {
53
53
  return 'Aider';
54
54
  }
55
- async applyRulerConfig(concatenatedRules, projectRoot, rulerMcpJson, agentConfig) {
55
+ async applyRulerConfig(concatenatedRules, projectRoot, rulerMcpJson, agentConfig, backup = true) {
56
56
  // First perform idempotent AGENTS.md write via composed AgentsMdAgent
57
57
  await this.agentsMdAgent.applyRulerConfig(concatenatedRules, projectRoot, null, {
58
58
  // Preserve explicit outputPath precedence semantics if provided.
59
59
  outputPath: agentConfig?.outputPath ||
60
60
  agentConfig?.outputPathInstructions ||
61
61
  undefined,
62
- });
62
+ }, backup);
63
63
  // Now handle .aider.conf.yml configuration
64
64
  const cfgPath = agentConfig?.outputPathConfig ??
65
65
  this.getDefaultOutputPath(projectRoot).config;
66
66
  let doc = {};
67
67
  try {
68
68
  await fs.access(cfgPath);
69
- await (0, FileSystemUtils_1.backupFile)(cfgPath);
69
+ if (backup) {
70
+ await (0, FileSystemUtils_1.backupFile)(cfgPath);
71
+ }
70
72
  const raw = await fs.readFile(cfgPath, 'utf8');
71
73
  doc = (yaml.load(raw) || {});
72
74
  }
@@ -48,9 +48,11 @@ class AugmentCodeAgent {
48
48
  return 'AugmentCode';
49
49
  }
50
50
  async applyRulerConfig(concatenatedRules, projectRoot, rulerMcpJson, // eslint-disable-line @typescript-eslint/no-unused-vars
51
- agentConfig) {
51
+ agentConfig, backup = true) {
52
52
  const output = agentConfig?.outputPath ?? this.getDefaultOutputPath(projectRoot);
53
- await (0, FileSystemUtils_1.backupFile)(output);
53
+ if (backup) {
54
+ await (0, FileSystemUtils_1.backupFile)(output);
55
+ }
54
56
  await (0, FileSystemUtils_1.writeGeneratedFile)(output, concatenatedRules);
55
57
  // AugmentCode does not support MCP servers
56
58
  // MCP configuration is ignored for this agent
@@ -66,14 +66,21 @@ class CodexCliAgent extends AgentsMdAgent_1.AgentsMdAgent {
66
66
  };
67
67
  const mcpEnabled = agentConfig?.mcp?.enabled ?? true;
68
68
  if (mcpEnabled && rulerMcpJson) {
69
+ // Apply MCP server filtering and transformation
70
+ const { filterMcpConfigForAgent } = await Promise.resolve().then(() => __importStar(require('../mcp/capabilities')));
71
+ const filteredMcpConfig = filterMcpConfigForAgent(rulerMcpJson, this);
72
+ if (!filteredMcpConfig) {
73
+ return; // No compatible servers found
74
+ }
75
+ const filteredRulerMcpJson = filteredMcpConfig;
69
76
  // Determine the config file path
70
77
  const configPath = agentConfig?.outputPathConfig ?? defaults.config;
71
78
  // Ensure the parent directory exists
72
79
  await fs_1.promises.mkdir(path.dirname(configPath), { recursive: true });
73
80
  // Get the merge strategy
74
81
  const strategy = agentConfig?.mcp?.strategy ?? 'merge';
75
- // Extract MCP servers from ruler config
76
- const rulerServers = rulerMcpJson.mcpServers || {};
82
+ // Extract MCP servers from filtered ruler config
83
+ const rulerServers = filteredRulerMcpJson.mcpServers || {};
77
84
  // Read existing TOML config if it exists
78
85
  let existingConfig = {};
79
86
  try {
@@ -106,6 +113,10 @@ class CodexCliAgent extends AgentsMdAgent_1.AgentsMdAgent {
106
113
  if (serverConfig.env) {
107
114
  mcpServer.env = serverConfig.env;
108
115
  }
116
+ // Handle additional properties from remote server transformation
117
+ if (serverConfig.headers) {
118
+ mcpServer.headers = serverConfig.headers;
119
+ }
109
120
  if (updatedConfig.mcp_servers) {
110
121
  updatedConfig.mcp_servers[serverName] = mcpServer;
111
122
  }
@@ -147,6 +158,20 @@ class CodexCliAgent extends AgentsMdAgent_1.AgentsMdAgent {
147
158
  }
148
159
  tomlContent += ` }\n`;
149
160
  }
161
+ // Add headers as inline table if present (from transformed remote servers)
162
+ if (serverConfig.headers &&
163
+ Object.keys(serverConfig.headers).length > 0) {
164
+ tomlContent += `headers = { `;
165
+ const entries = Object.entries(serverConfig.headers);
166
+ for (let i = 0; i < entries.length; i++) {
167
+ const [key, value] = entries[i];
168
+ tomlContent += `${JSON.stringify(key)} = "${value}"`;
169
+ if (i < entries.length - 1) {
170
+ tomlContent += ', ';
171
+ }
172
+ }
173
+ tomlContent += ` }\n`;
174
+ }
150
175
  }
151
176
  }
152
177
  await (0, FileSystemUtils_1.writeGeneratedFile)(configPath, tomlContent);
@@ -48,7 +48,7 @@ class CursorAgent extends AbstractAgent_1.AbstractAgent {
48
48
  return 'Cursor';
49
49
  }
50
50
  async applyRulerConfig(concatenatedRules, projectRoot, rulerMcpJson, // eslint-disable-line @typescript-eslint/no-unused-vars
51
- agentConfig) {
51
+ agentConfig, backup = true) {
52
52
  const output = agentConfig?.outputPath ?? this.getDefaultOutputPath(projectRoot);
53
53
  const absolutePath = path.resolve(projectRoot, output);
54
54
  // Cursor expects a YAML front-matter block with an `alwaysApply` flag.
@@ -56,7 +56,9 @@ class CursorAgent extends AbstractAgent_1.AbstractAgent {
56
56
  const frontMatter = ['---', 'alwaysApply: true', '---', ''].join('\n');
57
57
  const content = `${frontMatter}${concatenatedRules.trimStart()}`;
58
58
  await (0, FileSystemUtils_1.ensureDirExists)(path.dirname(absolutePath));
59
- await (0, FileSystemUtils_1.backupFile)(absolutePath);
59
+ if (backup) {
60
+ await (0, FileSystemUtils_1.backupFile)(absolutePath);
61
+ }
60
62
  await (0, FileSystemUtils_1.writeGeneratedFile)(absolutePath, content);
61
63
  }
62
64
  getDefaultOutputPath(projectRoot) {
@@ -0,0 +1,139 @@
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.RooCodeAgent = void 0;
37
+ const path = __importStar(require("path"));
38
+ const fs_1 = require("fs");
39
+ const AgentsMdAgent_1 = require("./AgentsMdAgent");
40
+ const FileSystemUtils_1 = require("../core/FileSystemUtils");
41
+ /**
42
+ * Agent for RooCode that writes to AGENTS.md and generates .roo/mcp.json
43
+ * with project-level MCP server configuration.
44
+ */
45
+ class RooCodeAgent {
46
+ constructor() {
47
+ this.agentsMdAgent = new AgentsMdAgent_1.AgentsMdAgent();
48
+ }
49
+ getIdentifier() {
50
+ return 'roo';
51
+ }
52
+ getName() {
53
+ return 'RooCode';
54
+ }
55
+ getDefaultOutputPath(projectRoot) {
56
+ return {
57
+ instructions: path.join(projectRoot, 'AGENTS.md'),
58
+ mcp: path.join(projectRoot, '.roo', 'mcp.json'),
59
+ };
60
+ }
61
+ async applyRulerConfig(concatenatedRules, projectRoot, rulerMcpJson, agentConfig, backup = true) {
62
+ // First perform idempotent AGENTS.md write via composed AgentsMdAgent
63
+ await this.agentsMdAgent.applyRulerConfig(concatenatedRules, projectRoot, null, {
64
+ // Preserve explicit outputPath precedence semantics if provided.
65
+ outputPath: agentConfig?.outputPath ||
66
+ agentConfig?.outputPathInstructions ||
67
+ undefined,
68
+ }, backup);
69
+ // Now handle .roo/mcp.json configuration
70
+ const outputPaths = this.getDefaultOutputPath(projectRoot);
71
+ const mcpPath = path.resolve(projectRoot, agentConfig?.outputPathConfig ?? outputPaths['mcp']);
72
+ await (0, FileSystemUtils_1.ensureDirExists)(path.dirname(mcpPath));
73
+ // Create base structure with mcpServers
74
+ let finalMcpConfig = {
75
+ mcpServers: {},
76
+ };
77
+ // Try to read existing .roo/mcp.json
78
+ let existingConfig = {};
79
+ try {
80
+ const existingContent = await fs_1.promises.readFile(mcpPath, 'utf-8');
81
+ const parsed = JSON.parse(existingContent);
82
+ if (parsed && typeof parsed === 'object') {
83
+ existingConfig = parsed;
84
+ }
85
+ }
86
+ catch {
87
+ // File doesn't exist or invalid JSON - start fresh
88
+ existingConfig = {};
89
+ }
90
+ // Merge MCP servers if we have ruler config
91
+ if (rulerMcpJson?.mcpServers) {
92
+ const existingServers = existingConfig.mcpServers || {};
93
+ const newServers = rulerMcpJson.mcpServers;
94
+ // Shallow merge: new servers override existing with same name
95
+ finalMcpConfig = {
96
+ mcpServers: {
97
+ ...existingServers,
98
+ ...newServers,
99
+ },
100
+ };
101
+ }
102
+ else if (existingConfig.mcpServers) {
103
+ // Keep existing servers if no new ones to add
104
+ finalMcpConfig = {
105
+ mcpServers: existingConfig.mcpServers,
106
+ };
107
+ }
108
+ // If neither condition is met, finalMcpConfig remains { mcpServers: {} }
109
+ // Write the config file with pretty JSON (2 spaces)
110
+ const newContent = JSON.stringify(finalMcpConfig, null, 2);
111
+ // Check if content has changed for idempotency
112
+ let existingContent = null;
113
+ try {
114
+ existingContent = await fs_1.promises.readFile(mcpPath, 'utf8');
115
+ }
116
+ catch {
117
+ existingContent = null;
118
+ }
119
+ if (existingContent !== null && existingContent === newContent) {
120
+ // No change; skip backup/write for idempotency
121
+ return;
122
+ }
123
+ // Backup (only if file existed and backup is enabled) then write new content
124
+ if (backup) {
125
+ await (0, FileSystemUtils_1.backupFile)(mcpPath);
126
+ }
127
+ await (0, FileSystemUtils_1.writeGeneratedFile)(mcpPath, newContent);
128
+ }
129
+ supportsMcpStdio() {
130
+ return true;
131
+ }
132
+ supportsMcpRemote() {
133
+ return true;
134
+ }
135
+ getMcpServerKey() {
136
+ return 'mcpServers';
137
+ }
138
+ }
139
+ exports.RooCodeAgent = RooCodeAgent;
@@ -48,15 +48,38 @@ class WindsurfAgent extends AbstractAgent_1.AbstractAgent {
48
48
  return 'Windsurf';
49
49
  }
50
50
  async applyRulerConfig(concatenatedRules, projectRoot, rulerMcpJson, // eslint-disable-line @typescript-eslint/no-unused-vars
51
- agentConfig) {
51
+ agentConfig, backup = true) {
52
52
  const output = agentConfig?.outputPath ?? this.getDefaultOutputPath(projectRoot);
53
53
  const absolutePath = path.resolve(projectRoot, output);
54
54
  // Windsurf expects a YAML front-matter block with a `trigger` flag.
55
55
  const frontMatter = ['---', 'trigger: always_on', '---', ''].join('\n');
56
56
  const content = `${frontMatter}${concatenatedRules.trimStart()}`;
57
+ const maxFileSize = 10000; // 10K characters
57
58
  await (0, FileSystemUtils_1.ensureDirExists)(path.dirname(absolutePath));
58
- await (0, FileSystemUtils_1.backupFile)(absolutePath);
59
- await (0, FileSystemUtils_1.writeGeneratedFile)(absolutePath, content);
59
+ if (backup) {
60
+ await (0, FileSystemUtils_1.backupFile)(absolutePath);
61
+ }
62
+ // Check if content exceeds the 10K limit
63
+ if (content.length <= maxFileSize) {
64
+ // Content fits in single file - use original behavior
65
+ await (0, FileSystemUtils_1.writeGeneratedFile)(absolutePath, content);
66
+ }
67
+ else {
68
+ // Content exceeds limit - split into multiple files
69
+ console.warn(`[ruler] Warning: Windsurf rule content exceeds ${maxFileSize} characters (${content.length}). Splitting into multiple files.`);
70
+ const files = this.splitContentIntoFiles(concatenatedRules.trimStart(), frontMatter, maxFileSize);
71
+ // Write each split file
72
+ const rulesDir = path.dirname(absolutePath);
73
+ const baseName = path.basename(absolutePath, '.md');
74
+ for (let i = 0; i < files.length; i++) {
75
+ const fileName = `${baseName}_${i.toString().padStart(2, '0')}.md`;
76
+ const filePath = path.join(rulesDir, fileName);
77
+ if (backup) {
78
+ await (0, FileSystemUtils_1.backupFile)(filePath);
79
+ }
80
+ await (0, FileSystemUtils_1.writeGeneratedFile)(filePath, files[i]);
81
+ }
82
+ }
60
83
  }
61
84
  getDefaultOutputPath(projectRoot) {
62
85
  return path.join(projectRoot, '.windsurf', 'rules', 'ruler_windsurf_instructions.md');
@@ -67,5 +90,69 @@ class WindsurfAgent extends AbstractAgent_1.AbstractAgent {
67
90
  supportsMcpRemote() {
68
91
  return true;
69
92
  }
93
+ /**
94
+ * Gets all actual output paths that will be created, including split files.
95
+ * This allows the gitignore system to know about split files before they're created.
96
+ */
97
+ getActualOutputPaths(concatenatedRules, projectRoot, agentConfig) {
98
+ const output = agentConfig?.outputPath ?? this.getDefaultOutputPath(projectRoot);
99
+ const absolutePath = path.resolve(projectRoot, output);
100
+ // Windsurf expects a YAML front-matter block with a `trigger` flag.
101
+ const frontMatter = ['---', 'trigger: always_on', '---', ''].join('\n');
102
+ const content = `${frontMatter}${concatenatedRules.trimStart()}`;
103
+ const maxFileSize = 10000; // 10K characters
104
+ // Check if content will be split
105
+ if (content.length <= maxFileSize) {
106
+ // Content fits in single file
107
+ return [absolutePath];
108
+ }
109
+ else {
110
+ // Content will be split - calculate how many files will be created
111
+ const files = this.splitContentIntoFiles(concatenatedRules.trimStart(), frontMatter, maxFileSize);
112
+ const rulesDir = path.dirname(absolutePath);
113
+ const baseName = path.basename(absolutePath, '.md');
114
+ const splitPaths = [];
115
+ for (let i = 0; i < files.length; i++) {
116
+ const fileName = `${baseName}_${i.toString().padStart(2, '0')}.md`;
117
+ const filePath = path.join(rulesDir, fileName);
118
+ splitPaths.push(filePath);
119
+ }
120
+ return splitPaths;
121
+ }
122
+ }
123
+ /**
124
+ * Splits content into multiple files, each under the specified size limit.
125
+ * Splits at the closest newline within the limit.
126
+ * Each file gets its own front-matter.
127
+ */
128
+ splitContentIntoFiles(rules, frontMatter, maxFileSize) {
129
+ const files = [];
130
+ const availableSpace = maxFileSize - frontMatter.length;
131
+ let remainingRules = rules;
132
+ while (remainingRules.length > 0) {
133
+ if (remainingRules.length <= availableSpace) {
134
+ // Remaining content fits in one file
135
+ files.push(`${frontMatter}${remainingRules}`);
136
+ break;
137
+ }
138
+ // Find the last newline within the available space
139
+ let splitIndex = availableSpace;
140
+ const searchSpace = remainingRules.substring(0, availableSpace);
141
+ const lastNewline = searchSpace.lastIndexOf('\n');
142
+ if (lastNewline > 0) {
143
+ // Split at the newline (include the newline in the current file)
144
+ splitIndex = lastNewline + 1;
145
+ }
146
+ else {
147
+ // No newline found within limit - split at the limit
148
+ // This shouldn't happen often but we handle it gracefully
149
+ splitIndex = availableSpace;
150
+ }
151
+ const chunk = remainingRules.substring(0, splitIndex);
152
+ files.push(`${frontMatter}${chunk}`);
153
+ remainingRules = remainingRules.substring(splitIndex);
154
+ }
155
+ return files;
156
+ }
70
157
  }
71
158
  exports.WindsurfAgent = WindsurfAgent;
@@ -26,6 +26,7 @@ const AgentsMdAgent_1 = require("./AgentsMdAgent");
26
26
  const QwenCodeAgent_1 = require("./QwenCodeAgent");
27
27
  const KiroAgent_1 = require("./KiroAgent");
28
28
  const WarpAgent_1 = require("./WarpAgent");
29
+ const RooCodeAgent_1 = require("./RooCodeAgent");
29
30
  exports.allAgents = [
30
31
  new CopilotAgent_1.CopilotAgent(),
31
32
  new ClaudeAgent_1.ClaudeAgent(),
@@ -50,4 +51,5 @@ exports.allAgents = [
50
51
  new AgentsMdAgent_1.AgentsMdAgent(),
51
52
  new KiroAgent_1.KiroAgent(),
52
53
  new WarpAgent_1.WarpAgent(),
54
+ new RooCodeAgent_1.RooCodeAgent(),
53
55
  ];
@@ -64,6 +64,11 @@ function run() {
64
64
  type: 'boolean',
65
65
  description: 'Enable nested rule loading from nested .ruler directories (default: disabled)',
66
66
  default: false,
67
+ })
68
+ .option('backup', {
69
+ type: 'boolean',
70
+ description: 'Enable/disable creation of .bak backup files (default: enabled)',
71
+ default: true,
67
72
  });
68
73
  }, handlers_1.applyHandler)
69
74
  .command('init', 'Scaffold a .ruler directory with default files', (y) => {
@@ -59,6 +59,7 @@ async function applyHandler(argv) {
59
59
  const dryRun = argv['dry-run'];
60
60
  const localOnly = argv['local-only'];
61
61
  const nested = argv.nested;
62
+ const backup = argv.backup;
62
63
  // Determine gitignore preference: CLI > TOML > Default (enabled)
63
64
  // yargs handles --no-gitignore by setting gitignore to false
64
65
  let gitignorePreference;
@@ -69,7 +70,7 @@ async function applyHandler(argv) {
69
70
  gitignorePreference = undefined; // Let TOML/default decide
70
71
  }
71
72
  try {
72
- await (0, lib_1.applyAllAgentConfigs)(projectRoot, agents, configPath, mcpEnabled, mcpStrategy, gitignorePreference, verbose, dryRun, localOnly, nested);
73
+ await (0, lib_1.applyAllAgentConfigs)(projectRoot, agents, configPath, mcpEnabled, mcpStrategy, gitignorePreference, verbose, dryRun, localOnly, nested, backup);
73
74
  console.log('Ruler apply completed successfully.');
74
75
  }
75
76
  catch (err) {
@@ -218,12 +218,12 @@ function selectAgentsToRun(allAgents, config) {
218
218
  * @param cliMcpStrategy MCP strategy from CLI
219
219
  * @returns Promise resolving to array of generated file paths
220
220
  */
221
- async function processHierarchicalConfigurations(agents, configurations, verbose, dryRun, cliMcpEnabled, cliMcpStrategy) {
221
+ async function processHierarchicalConfigurations(agents, configurations, verbose, dryRun, cliMcpEnabled, cliMcpStrategy, backup = true) {
222
222
  const allGeneratedPaths = [];
223
223
  for (const config of configurations) {
224
224
  console.log(`[ruler] Processing .ruler directory: ${config.rulerDir}`);
225
225
  const rulerRoot = path.dirname(config.rulerDir);
226
- const paths = await applyConfigurationsToAgents(agents, config.concatenatedRules, config.rulerMcpJson, config.config, rulerRoot, verbose, dryRun, cliMcpEnabled, cliMcpStrategy);
226
+ const paths = await applyConfigurationsToAgents(agents, config.concatenatedRules, config.rulerMcpJson, config.config, rulerRoot, verbose, dryRun, cliMcpEnabled, cliMcpStrategy, backup);
227
227
  allGeneratedPaths.push(...paths);
228
228
  }
229
229
  return allGeneratedPaths;
@@ -240,8 +240,8 @@ async function processHierarchicalConfigurations(agents, configurations, verbose
240
240
  * @param cliMcpStrategy MCP strategy from CLI
241
241
  * @returns Promise resolving to array of generated file paths
242
242
  */
243
- async function processSingleConfiguration(agents, configuration, projectRoot, verbose, dryRun, cliMcpEnabled, cliMcpStrategy) {
244
- return await applyConfigurationsToAgents(agents, configuration.concatenatedRules, configuration.rulerMcpJson, configuration.config, projectRoot, verbose, dryRun, cliMcpEnabled, cliMcpStrategy);
243
+ async function processSingleConfiguration(agents, configuration, projectRoot, verbose, dryRun, cliMcpEnabled, cliMcpStrategy, backup = true) {
244
+ return await applyConfigurationsToAgents(agents, configuration.concatenatedRules, configuration.rulerMcpJson, configuration.config, projectRoot, verbose, dryRun, cliMcpEnabled, cliMcpStrategy, backup);
245
245
  }
246
246
  /**
247
247
  * Applies configurations to the selected agents (internal function).
@@ -254,7 +254,7 @@ async function processSingleConfiguration(agents, configuration, projectRoot, ve
254
254
  * @param dryRun Whether to perform a dry run
255
255
  * @returns Promise resolving to array of generated file paths
256
256
  */
257
- async function applyConfigurationsToAgents(agents, concatenatedRules, rulerMcpJson, config, projectRoot, verbose, dryRun, cliMcpEnabled = true, cliMcpStrategy) {
257
+ async function applyConfigurationsToAgents(agents, concatenatedRules, rulerMcpJson, config, projectRoot, verbose, dryRun, cliMcpEnabled = true, cliMcpStrategy, backup = true) {
258
258
  const generatedPaths = [];
259
259
  let agentsMdWritten = false;
260
260
  for (const agent of agents) {
@@ -263,12 +263,22 @@ async function applyConfigurationsToAgents(agents, concatenatedRules, rulerMcpJs
263
263
  (0, constants_1.logVerbose)(`Processing agent: ${agent.getName()}`, verbose);
264
264
  const agentConfig = config.agentConfigs[agent.getIdentifier()];
265
265
  // Collect output paths for .gitignore
266
- const outputPaths = (0, agent_utils_1.getAgentOutputPaths)(agent, projectRoot, agentConfig);
266
+ let outputPaths;
267
+ // Special handling for Windsurf agent to account for file splitting
268
+ if (agent.getIdentifier() === 'windsurf' &&
269
+ 'getActualOutputPaths' in agent) {
270
+ outputPaths = agent.getActualOutputPaths(concatenatedRules, projectRoot, agentConfig);
271
+ }
272
+ else {
273
+ outputPaths = (0, agent_utils_1.getAgentOutputPaths)(agent, projectRoot, agentConfig);
274
+ }
267
275
  (0, constants_1.logVerbose)(`Agent ${agent.getName()} output paths: ${outputPaths.join(', ')}`, verbose);
268
276
  generatedPaths.push(...outputPaths);
269
- // Also add the backup file paths to the gitignore list
270
- const backupPaths = outputPaths.map((p) => `${p}.bak`);
271
- generatedPaths.push(...backupPaths);
277
+ // Only add the backup file paths to the gitignore list if backups are enabled
278
+ if (backup) {
279
+ const backupPaths = outputPaths.map((p) => `${p}.bak`);
280
+ generatedPaths.push(...backupPaths);
281
+ }
272
282
  if (dryRun) {
273
283
  (0, constants_1.logVerbose)(`DRY RUN: Would write rules to: ${outputPaths.join(', ')}`, verbose);
274
284
  }
@@ -299,15 +309,15 @@ async function applyConfigurationsToAgents(agents, concatenatedRules, rulerMcpJs
299
309
  };
300
310
  }
301
311
  if (!skipApplyForThisAgent) {
302
- await agent.applyRulerConfig(concatenatedRules, projectRoot, rulerMcpJson, finalAgentConfig);
312
+ await agent.applyRulerConfig(concatenatedRules, projectRoot, rulerMcpJson, finalAgentConfig, backup);
303
313
  }
304
314
  }
305
315
  // Handle MCP configuration
306
- await handleMcpConfiguration(agent, agentConfig, config, rulerMcpJson, projectRoot, generatedPaths, verbose, dryRun, cliMcpEnabled, cliMcpStrategy);
316
+ await handleMcpConfiguration(agent, agentConfig, config, rulerMcpJson, projectRoot, generatedPaths, verbose, dryRun, cliMcpEnabled, cliMcpStrategy, backup);
307
317
  }
308
318
  return generatedPaths;
309
319
  }
310
- async function handleMcpConfiguration(agent, agentConfig, config, rulerMcpJson, projectRoot, generatedPaths, verbose, dryRun, cliMcpEnabled = true, cliMcpStrategy) {
320
+ async function handleMcpConfiguration(agent, agentConfig, config, rulerMcpJson, projectRoot, generatedPaths, verbose, dryRun, cliMcpEnabled = true, cliMcpStrategy, backup = true) {
311
321
  if (!(0, capabilities_1.agentSupportsMcp)(agent)) {
312
322
  (0, constants_1.logVerbose)(`Agent ${agent.getName()} does not support MCP - skipping MCP configuration`, verbose);
313
323
  return;
@@ -322,47 +332,49 @@ async function handleMcpConfiguration(agent, agentConfig, config, rulerMcpJson,
322
332
  (0, constants_1.logVerbose)(`No compatible MCP servers found for ${agent.getName()} - skipping MCP configuration`, verbose);
323
333
  return;
324
334
  }
325
- await updateGitignoreForMcpFile(dest, projectRoot, generatedPaths);
326
- await applyMcpConfiguration(agent, filteredMcpJson, dest, agentConfig, config, projectRoot, cliMcpStrategy, dryRun, verbose);
335
+ await updateGitignoreForMcpFile(dest, projectRoot, generatedPaths, backup);
336
+ await applyMcpConfiguration(agent, filteredMcpJson, dest, agentConfig, config, projectRoot, cliMcpStrategy, dryRun, verbose, backup);
327
337
  }
328
- async function updateGitignoreForMcpFile(dest, projectRoot, generatedPaths) {
338
+ async function updateGitignoreForMcpFile(dest, projectRoot, generatedPaths, backup = true) {
329
339
  if (dest.startsWith(projectRoot)) {
330
340
  const relativeDest = path.relative(projectRoot, dest);
331
341
  generatedPaths.push(relativeDest);
332
- generatedPaths.push(`${relativeDest}.bak`);
342
+ if (backup) {
343
+ generatedPaths.push(`${relativeDest}.bak`);
344
+ }
333
345
  }
334
346
  }
335
- async function applyMcpConfiguration(agent, filteredMcpJson, dest, agentConfig, config, projectRoot, cliMcpStrategy, dryRun, verbose) {
347
+ async function applyMcpConfiguration(agent, filteredMcpJson, dest, agentConfig, config, projectRoot, cliMcpStrategy, dryRun, verbose, backup = true) {
336
348
  // Prevent writing MCP configs outside the project root (e.g., legacy home-directory targets)
337
349
  if (!dest.startsWith(projectRoot)) {
338
350
  (0, constants_1.logVerbose)(`Skipping MCP config for ${agent.getName()} because target path is outside project: ${dest}`, verbose);
339
351
  return;
340
352
  }
341
353
  if (agent.getIdentifier() === 'openhands') {
342
- return await applyOpenHandsMcpConfiguration(filteredMcpJson, dest, dryRun, verbose);
354
+ return await applyOpenHandsMcpConfiguration(filteredMcpJson, dest, dryRun, verbose, backup);
343
355
  }
344
356
  if (agent.getIdentifier() === 'opencode') {
345
- return await applyOpenCodeMcpConfiguration(filteredMcpJson, dest, dryRun, verbose);
357
+ return await applyOpenCodeMcpConfiguration(filteredMcpJson, dest, dryRun, verbose, backup);
346
358
  }
347
- return await applyStandardMcpConfiguration(agent, filteredMcpJson, dest, agentConfig, config, cliMcpStrategy, dryRun, verbose);
359
+ return await applyStandardMcpConfiguration(agent, filteredMcpJson, dest, agentConfig, config, cliMcpStrategy, dryRun, verbose, backup);
348
360
  }
349
- async function applyOpenHandsMcpConfiguration(filteredMcpJson, dest, dryRun, verbose) {
361
+ async function applyOpenHandsMcpConfiguration(filteredMcpJson, dest, dryRun, verbose, backup = true) {
350
362
  if (dryRun) {
351
363
  (0, constants_1.logVerbose)(`DRY RUN: Would apply MCP config by updating TOML file: ${dest}`, verbose);
352
364
  }
353
365
  else {
354
- await (0, propagateOpenHandsMcp_1.propagateMcpToOpenHands)(filteredMcpJson, dest);
366
+ await (0, propagateOpenHandsMcp_1.propagateMcpToOpenHands)(filteredMcpJson, dest, backup);
355
367
  }
356
368
  }
357
- async function applyOpenCodeMcpConfiguration(filteredMcpJson, dest, dryRun, verbose) {
369
+ async function applyOpenCodeMcpConfiguration(filteredMcpJson, dest, dryRun, verbose, backup = true) {
358
370
  if (dryRun) {
359
371
  (0, constants_1.logVerbose)(`DRY RUN: Would apply MCP config by updating OpenCode config file: ${dest}`, verbose);
360
372
  }
361
373
  else {
362
- await (0, propagateOpenCodeMcp_1.propagateMcpToOpenCode)(filteredMcpJson, dest);
374
+ await (0, propagateOpenCodeMcp_1.propagateMcpToOpenCode)(filteredMcpJson, dest, backup);
363
375
  }
364
376
  }
365
- async function applyStandardMcpConfiguration(agent, filteredMcpJson, dest, agentConfig, config, cliMcpStrategy, dryRun, verbose) {
377
+ async function applyStandardMcpConfiguration(agent, filteredMcpJson, dest, agentConfig, config, cliMcpStrategy, dryRun, verbose, backup = true) {
366
378
  const strategy = cliMcpStrategy ??
367
379
  agentConfig?.mcp?.strategy ??
368
380
  config.mcp?.strategy ??
@@ -373,6 +385,10 @@ async function applyStandardMcpConfiguration(agent, filteredMcpJson, dest, agent
373
385
  (0, constants_1.logVerbose)(`DRY RUN: Would apply MCP config to: ${dest}`, verbose);
374
386
  }
375
387
  else {
388
+ if (backup) {
389
+ const { backupFile } = await Promise.resolve().then(() => __importStar(require('../core/FileSystemUtils')));
390
+ await backupFile(dest);
391
+ }
376
392
  const existing = await (0, mcp_1.readNativeMcp)(dest);
377
393
  const merged = (0, merge_1.mergeMcp)(existing, filteredMcpJson, strategy, serverKey);
378
394
  await (0, mcp_1.writeNativeMcp)(dest, merged);
@@ -386,7 +402,7 @@ async function applyStandardMcpConfiguration(agent, filteredMcpJson, dest, agent
386
402
  * @param cliGitignoreEnabled CLI gitignore setting
387
403
  * @param dryRun Whether to perform a dry run
388
404
  */
389
- async function updateGitignore(projectRoot, generatedPaths, config, cliGitignoreEnabled, dryRun) {
405
+ async function updateGitignore(projectRoot, generatedPaths, config, cliGitignoreEnabled, dryRun, backup = true) {
390
406
  // Configuration precedence: CLI > TOML > Default (enabled)
391
407
  let gitignoreEnabled;
392
408
  if (cliGitignoreEnabled !== undefined) {
@@ -400,8 +416,10 @@ async function updateGitignore(projectRoot, generatedPaths, config, cliGitignore
400
416
  }
401
417
  if (gitignoreEnabled && generatedPaths.length > 0) {
402
418
  const uniquePaths = [...new Set(generatedPaths)];
403
- // Add wildcard pattern for backup files
404
- uniquePaths.push('*.bak');
419
+ // Add wildcard pattern for backup files only if backup is enabled
420
+ if (backup) {
421
+ uniquePaths.push('*.bak');
422
+ }
405
423
  if (uniquePaths.length > 0) {
406
424
  const prefix = (0, constants_1.actionPrefix)(dryRun);
407
425
  if (dryRun) {
package/dist/lib.js CHANGED
@@ -17,7 +17,7 @@ const agents = agents_1.allAgents;
17
17
  * @param projectRoot Root directory of the project
18
18
  * @param includedAgents Optional list of agent name filters (case-insensitive substrings)
19
19
  */
20
- async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cliMcpEnabled = true, cliMcpStrategy, cliGitignoreEnabled, verbose = false, dryRun = false, localOnly = false, nested = false) {
20
+ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cliMcpEnabled = true, cliMcpStrategy, cliGitignoreEnabled, verbose = false, dryRun = false, localOnly = false, nested = false, backup = true) {
21
21
  // Load configuration and rules
22
22
  (0, constants_1.logVerbose)(`Loading configuration from project root: ${projectRoot}`, verbose);
23
23
  if (configPath) {
@@ -40,7 +40,7 @@ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cli
40
40
  normalizeAgentConfigs(rootConfig, agents);
41
41
  selectedAgents = (0, apply_engine_1.selectAgentsToRun)(agents, rootConfig);
42
42
  (0, constants_1.logVerbose)(`Selected ${selectedAgents.length} agents: ${selectedAgents.map((a) => a.getName()).join(', ')}`, verbose);
43
- generatedPaths = await (0, apply_engine_1.processHierarchicalConfigurations)(selectedAgents, hierarchicalConfigs, verbose, dryRun, cliMcpEnabled, cliMcpStrategy);
43
+ generatedPaths = await (0, apply_engine_1.processHierarchicalConfigurations)(selectedAgents, hierarchicalConfigs, verbose, dryRun, cliMcpEnabled, cliMcpStrategy, backup);
44
44
  }
45
45
  else {
46
46
  const singleConfig = await (0, apply_engine_1.loadSingleConfiguration)(projectRoot, configPath, localOnly);
@@ -51,9 +51,9 @@ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cli
51
51
  normalizeAgentConfigs(singleConfig.config, agents);
52
52
  selectedAgents = (0, apply_engine_1.selectAgentsToRun)(agents, singleConfig.config);
53
53
  (0, constants_1.logVerbose)(`Selected ${selectedAgents.length} agents: ${selectedAgents.map((a) => a.getName()).join(', ')}`, verbose);
54
- generatedPaths = await (0, apply_engine_1.processSingleConfiguration)(selectedAgents, singleConfig, projectRoot, verbose, dryRun, cliMcpEnabled, cliMcpStrategy);
54
+ generatedPaths = await (0, apply_engine_1.processSingleConfiguration)(selectedAgents, singleConfig, projectRoot, verbose, dryRun, cliMcpEnabled, cliMcpStrategy, backup);
55
55
  }
56
- await (0, apply_engine_1.updateGitignore)(projectRoot, generatedPaths, loadedConfig, cliGitignoreEnabled, dryRun);
56
+ await (0, apply_engine_1.updateGitignore)(projectRoot, generatedPaths, loadedConfig, cliGitignoreEnabled, dryRun, backup);
57
57
  }
58
58
  /**
59
59
  * Normalizes per-agent config keys to agent identifiers for consistent lookup.
@@ -40,10 +40,24 @@ function filterMcpConfigForAgent(mcpConfig, agent) {
40
40
  const isStdio = hasCommand && !hasUrl;
41
41
  const isRemote = hasUrl && !hasCommand;
42
42
  // Include server if agent supports its type
43
- if ((isStdio && capabilities.supportsStdio) ||
44
- (isRemote && capabilities.supportsRemote)) {
43
+ if (isStdio && capabilities.supportsStdio) {
45
44
  filteredServers[serverName] = serverConfig;
46
45
  }
46
+ else if (isRemote && capabilities.supportsRemote) {
47
+ filteredServers[serverName] = serverConfig;
48
+ }
49
+ else if (isRemote &&
50
+ !capabilities.supportsRemote &&
51
+ capabilities.supportsStdio) {
52
+ // Transform remote server to stdio server using mcp-remote
53
+ const transformedConfig = {
54
+ command: 'npx',
55
+ args: ['-y', 'mcp-remote@latest', config.url],
56
+ ...Object.fromEntries(Object.entries(config).filter(([key]) => key !== 'url')),
57
+ };
58
+ filteredServers[serverName] = transformedConfig;
59
+ }
60
+ // Note: Mixed servers (both command and url) are excluded
47
61
  }
48
62
  return Object.keys(filteredServers).length > 0
49
63
  ? { mcpServers: filteredServers }
@@ -85,7 +85,7 @@ function transformToOpenCodeFormat(rulerMcp) {
85
85
  mcp: openCodeServers,
86
86
  };
87
87
  }
88
- async function propagateMcpToOpenCode(rulerMcpData, openCodeConfigPath) {
88
+ async function propagateMcpToOpenCode(rulerMcpData, openCodeConfigPath, backup = true) {
89
89
  const rulerMcp = rulerMcpData || {};
90
90
  // Read existing OpenCode config if it exists
91
91
  let existingConfig = {};
@@ -108,5 +108,9 @@ async function propagateMcpToOpenCode(rulerMcpData, openCodeConfigPath) {
108
108
  },
109
109
  };
110
110
  await (0, FileSystemUtils_1.ensureDirExists)(path.dirname(openCodeConfigPath));
111
+ if (backup) {
112
+ const { backupFile } = await Promise.resolve().then(() => __importStar(require('../core/FileSystemUtils')));
113
+ await backupFile(openCodeConfigPath);
114
+ }
111
115
  await fs.writeFile(openCodeConfigPath, JSON.stringify(finalConfig, null, 2) + '\n');
112
116
  }
@@ -90,7 +90,7 @@ function normalizeRemoteServerArray(entries) {
90
90
  // All entries are strings, keep as is
91
91
  return entries;
92
92
  }
93
- async function propagateMcpToOpenHands(rulerMcpData, openHandsConfigPath) {
93
+ async function propagateMcpToOpenHands(rulerMcpData, openHandsConfigPath, backup = true) {
94
94
  const rulerMcp = rulerMcpData || {};
95
95
  // Always use the legacy Ruler MCP config format as input (top-level "mcpServers" key)
96
96
  const rulerServers = rulerMcp.mcpServers || {};
@@ -162,5 +162,9 @@ async function propagateMcpToOpenHands(rulerMcpData, openHandsConfigPath) {
162
162
  config.mcp.sse_servers = normalizeRemoteServerArray(Array.from(existingSseServers.values()));
163
163
  config.mcp.shttp_servers = normalizeRemoteServerArray(Array.from(existingShttpServers.values()));
164
164
  await (0, FileSystemUtils_1.ensureDirExists)(path.dirname(openHandsConfigPath));
165
+ if (backup) {
166
+ const { backupFile } = await Promise.resolve().then(() => __importStar(require('../core/FileSystemUtils')));
167
+ await backupFile(openHandsConfigPath);
168
+ }
165
169
  await fs.writeFile(openHandsConfigPath, (0, toml_1.stringify)(config));
166
170
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intellectronica/ruler",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "Ruler — apply the same rules to all coding agents",
5
5
  "main": "dist/lib.js",
6
6
  "scripts": {