@intellectronica/ruler 0.2.1 → 0.2.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
@@ -18,7 +18,7 @@
18
18
  Managing instructions across multiple AI coding tools becomes complex as your team grows. Different agents (GitHub Copilot, Claude, Cursor, Aider, etc.) require their own configuration files, leading to:
19
19
 
20
20
  - **Inconsistent guidance** across AI tools
21
- - **Duplicated effort** maintaining multiple config files
21
+ - **Duplicated effort** maintaining multiple config files
22
22
  - **Context drift** as project requirements evolve
23
23
  - **Onboarding friction** for new AI tools
24
24
 
@@ -35,15 +35,17 @@ Ruler solves this by providing a **single source of truth** for all your AI agen
35
35
 
36
36
  ## Supported AI Agents
37
37
 
38
- | Agent | File(s) Created/Updated |
39
- | ---------------------- | ----------------------------------------------------------- |
40
- | GitHub Copilot | `.github/copilot-instructions.md` |
41
- | Claude Code | `CLAUDE.md` |
42
- | OpenAI Codex CLI | `AGENTS.md` |
43
- | Cursor | `.cursor/rules/ruler_cursor_instructions.md` |
44
- | Windsurf | `.windsurf/rules/ruler_windsurf_instructions.md` |
45
- | Cline | `.clinerules` |
46
- | Aider | `ruler_aider_instructions.md` and `.aider.conf.yml` |
38
+ | Agent | File(s) Created/Updated |
39
+ | ---------------- | ------------------------------------------------------------- |
40
+ | GitHub Copilot | `.github/copilot-instructions.md` |
41
+ | Claude Code | `CLAUDE.md` |
42
+ | OpenAI Codex CLI | `AGENTS.md` |
43
+ | Cursor | `.cursor/rules/ruler_cursor_instructions.md` |
44
+ | Windsurf | `.windsurf/rules/ruler_windsurf_instructions.md` |
45
+ | Cline | `.clinerules` |
46
+ | Aider | `ruler_aider_instructions.md` and `.aider.conf.yml` |
47
+ | Firebase Studio | `.idx/airules.md` |
48
+ | Open Hands | `.openhands/microagents/repo.md` and `.openhands/config.toml` |
47
49
 
48
50
  ## Getting Started
49
51
 
@@ -54,11 +56,13 @@ Node.js 18.x or higher is required.
54
56
  ### Installation
55
57
 
56
58
  **Global Installation (Recommended for CLI use):**
59
+
57
60
  ```bash
58
61
  npm install -g @intellectronica/ruler
59
62
  ```
60
63
 
61
64
  **Using `npx` (for one-off commands):**
65
+
62
66
  ```bash
63
67
  npx @intellectronica/ruler apply
64
68
  ```
@@ -87,25 +91,30 @@ This is your central hub for all AI agent instructions:
87
91
  ### Best Practices for Rule Files
88
92
 
89
93
  **Granularity**: Break down complex instructions into focused `.md` files:
94
+
90
95
  - `coding_style.md`
91
- - `api_conventions.md`
96
+ - `api_conventions.md`
92
97
  - `project_architecture.md`
93
98
  - `security_guidelines.md`
94
99
 
95
100
  **Example rule file (`.ruler/python_guidelines.md`):**
101
+
96
102
  ```markdown
97
103
  # Python Project Guidelines
98
104
 
99
105
  ## General Style
106
+
100
107
  - Follow PEP 8 for all Python code
101
108
  - Use type hints for all function signatures and complex variables
102
109
  - Keep functions short and focused on a single task
103
110
 
104
111
  ## Error Handling
112
+
105
113
  - Use specific exception types rather than generic `Exception`
106
114
  - Log errors effectively with context
107
115
 
108
116
  ## Security
117
+
109
118
  - Always validate and sanitize user input
110
119
  - Be mindful of potential injection vulnerabilities
111
120
  ```
@@ -113,47 +122,59 @@ This is your central hub for all AI agent instructions:
113
122
  ## Usage: The `apply` Command
114
123
 
115
124
  ### Primary Command
125
+
116
126
  ```bash
117
127
  ruler apply [options]
118
128
  ```
119
129
 
120
130
  ### Options
121
131
 
122
- | Option | Description |
123
- |--------|-------------|
124
- | `--project-root <path>` | Path to your project's root (default: current directory) |
125
- | `--agents <agent1,agent2,...>` | Comma-separated list of agent names to target |
126
- | `--config <path>` | Path to a custom `ruler.toml` configuration file |
127
- | `--mcp` / `--with-mcp` | Enable applying MCP server configurations (default: true) |
128
- | `--no-mcp` | Disable applying MCP server configurations |
129
- | `--mcp-overwrite` | Overwrite native MCP config entirely instead of merging |
130
- | `--gitignore` | Enable automatic .gitignore updates (default: true) |
131
- | `--no-gitignore` | Disable automatic .gitignore updates |
132
- | `--verbose` / `-v` | Display detailed output during execution |
132
+ | Option | Description |
133
+ | ------------------------------ | --------------------------------------------------------- |
134
+ | `--project-root <path>` | Path to your project's root (default: current directory) |
135
+ | `--agents <agent1,agent2,...>` | Comma-separated list of agent names to target |
136
+ | `--config <path>` | Path to a custom `ruler.toml` configuration file |
137
+ | `--mcp` / `--with-mcp` | Enable applying MCP server configurations (default: true) |
138
+ | `--no-mcp` | Disable applying MCP server configurations |
139
+ | `--mcp-overwrite` | Overwrite native MCP config entirely instead of merging |
140
+ | `--gitignore` | Enable automatic .gitignore updates (default: true) |
141
+ | `--no-gitignore` | Disable automatic .gitignore updates |
142
+ | `--verbose` / `-v` | Display detailed output during execution |
133
143
 
134
144
  ### Common Examples
135
145
 
136
146
  **Apply rules to all configured agents:**
147
+
137
148
  ```bash
138
149
  ruler apply
139
150
  ```
140
151
 
141
152
  **Apply rules only to GitHub Copilot and Claude:**
153
+
142
154
  ```bash
143
155
  ruler apply --agents copilot,claude
144
156
  ```
145
157
 
158
+ **Apply rules only to Firebase Studio:**
159
+
160
+ ```bash
161
+ ruler apply --agents firebase
162
+ ```
163
+
146
164
  **Use a specific configuration file:**
165
+
147
166
  ```bash
148
167
  ruler apply --config ./team-configs/ruler.frontend.toml
149
168
  ```
150
169
 
151
170
  **Apply rules with verbose output:**
171
+
152
172
  ```bash
153
173
  ruler apply --verbose
154
174
  ```
155
175
 
156
176
  **Apply rules but skip MCP and .gitignore updates:**
177
+
157
178
  ```bash
158
179
  ruler apply --no-mcp --no-gitignore
159
180
  ```
@@ -161,9 +182,11 @@ ruler apply --no-mcp --no-gitignore
161
182
  ## Configuration (`ruler.toml`) in Detail
162
183
 
163
184
  ### Location
185
+
164
186
  Defaults to `.ruler/ruler.toml` in the project root. Override with `--config` CLI option.
165
187
 
166
188
  ### Complete Example
189
+
167
190
  ```toml
168
191
  # Default agents to run when --agents is not specified
169
192
  # Uses case-insensitive substring matching
@@ -195,6 +218,10 @@ enabled = true
195
218
  output_path_instructions = "ruler_aider_instructions.md"
196
219
  output_path_config = ".aider.conf.yml"
197
220
 
221
+ [agents.firebase]
222
+ enabled = true
223
+ output_path = ".idx/airules.md"
224
+
198
225
  # Agent-specific MCP configuration
199
226
  [agents.cursor.mcp]
200
227
  enabled = true
@@ -216,16 +243,22 @@ enabled = false
216
243
  MCP provides broader context to AI models through server configurations. Ruler can manage and distribute these settings across compatible agents.
217
244
 
218
245
  ### `.ruler/mcp.json`
246
+
219
247
  Define your project's MCP servers:
248
+
220
249
  ```json
221
250
  {
222
251
  "mcpServers": {
223
252
  "filesystem": {
224
253
  "command": "npx",
225
- "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/project"]
254
+ "args": [
255
+ "-y",
256
+ "@modelcontextprotocol/server-filesystem",
257
+ "/path/to/project"
258
+ ]
226
259
  },
227
260
  "git": {
228
- "command": "npx",
261
+ "command": "npx",
229
262
  "args": ["-y", "@modelcontextprotocol/server-git", "--repository", "."]
230
263
  }
231
264
  }
@@ -239,12 +272,14 @@ Ruler uses this file with the `merge` (default) or `overwrite` strategy, control
239
272
  Ruler automatically manages your `.gitignore` file to keep generated agent configuration files out of version control.
240
273
 
241
274
  ### How it Works
275
+
242
276
  - Creates or updates `.gitignore` in your project root
243
277
  - Adds paths to a managed block marked with `# START Ruler Generated Files` and `# END Ruler Generated Files`
244
278
  - Preserves existing content outside this block
245
279
  - Sorts paths alphabetically and uses relative POSIX-style paths
246
280
 
247
281
  ### Example `.gitignore` Section
282
+
248
283
  ```gitignore
249
284
  # Your existing rules
250
285
  node_modules/
@@ -265,6 +300,7 @@ dist/
265
300
  ```
266
301
 
267
302
  ### Control Options
303
+
268
304
  - **CLI flags**: `--gitignore` or `--no-gitignore`
269
305
  - **Configuration**: `[gitignore].enabled` in `ruler.toml`
270
306
  - **Default**: enabled
@@ -272,6 +308,7 @@ dist/
272
308
  ## Practical Usage Scenarios
273
309
 
274
310
  ### Scenario 1: Getting Started Quickly
311
+
275
312
  ```bash
276
313
  # Initialize Ruler in your project
277
314
  cd your-project
@@ -286,16 +323,19 @@ ruler apply
286
323
  ```
287
324
 
288
325
  ### Scenario 2: Team Standardization
326
+
289
327
  1. Create `.ruler/coding_standards.md`, `.ruler/api_usage.md`
290
328
  2. Commit the `.ruler` directory to your repository
291
329
  3. Team members pull changes and run `ruler apply` to update their local AI agent configurations
292
330
 
293
331
  ### Scenario 3: Project-Specific Context for AI
332
+
294
333
  1. Detail your project's architecture in `.ruler/project_overview.md`
295
- 2. Describe primary data structures in `.ruler/data_models.md`
334
+ 2. Describe primary data structures in `.ruler/data_models.md`
296
335
  3. Run `ruler apply` to help AI tools provide more relevant suggestions
297
336
 
298
337
  ### Integration with NPM Scripts
338
+
299
339
  ```json
300
340
  {
301
341
  "scripts": {
@@ -307,6 +347,7 @@ ruler apply
307
347
  ```
308
348
 
309
349
  ### Integration with GitHub Actions
350
+
310
351
  ```yaml
311
352
  # .github/workflows/ruler-check.yml
312
353
  name: Check Ruler Configuration
@@ -323,13 +364,13 @@ jobs:
323
364
  with:
324
365
  node-version: '18'
325
366
  cache: 'npm'
326
-
367
+
327
368
  - name: Install Ruler
328
369
  run: npm install -g @intellectronica/ruler
329
-
370
+
330
371
  - name: Apply Ruler configuration
331
372
  run: ruler apply --no-gitignore
332
-
373
+
333
374
  - name: Check for uncommitted changes
334
375
  run: |
335
376
  if [[ -n $(git status --porcelain) ]]; then
@@ -344,28 +385,35 @@ jobs:
344
385
  ### Common Issues
345
386
 
346
387
  **"Cannot find module" errors:**
388
+
347
389
  - Ensure Ruler is installed globally: `npm install -g @intellectronica/ruler`
348
390
  - Or use `npx @intellectronica/ruler`
349
391
 
350
392
  **Permission denied errors:**
393
+
351
394
  - On Unix systems, you may need `sudo` for global installation
352
395
 
353
396
  **Agent files not updating:**
397
+
354
398
  - Check if the agent is enabled in `ruler.toml`
355
399
  - Verify agent isn't excluded by `--agents` flag
356
400
  - Use `--verbose` to see detailed execution logs
357
401
 
358
402
  **Configuration validation errors:**
403
+
359
404
  - Ruler now validates `ruler.toml` format and will show specific error details
360
405
  - Check that all configuration values match the expected types and formats
361
406
 
362
407
  ### Debug Mode
408
+
363
409
  Use `--verbose` flag to see detailed execution logs:
410
+
364
411
  ```bash
365
412
  ruler apply --verbose
366
413
  ```
367
414
 
368
415
  This shows:
416
+
369
417
  - Configuration loading details
370
418
  - Agent selection logic
371
419
  - File processing information
@@ -391,6 +439,7 @@ A: Version 0.2.0 is backward compatible. Your existing `.ruler/` directory and `
391
439
  ## Development
392
440
 
393
441
  ### Setup
442
+
394
443
  ```bash
395
444
  git clone https://github.com/intellectronica/ruler.git
396
445
  cd ruler
@@ -399,6 +448,7 @@ npm run build
399
448
  ```
400
449
 
401
450
  ### Testing
451
+
402
452
  ```bash
403
453
  # Run all tests
404
454
  npm test
@@ -411,6 +461,7 @@ npm run test:watch
411
461
  ```
412
462
 
413
463
  ### Code Quality
464
+
414
465
  ```bash
415
466
  # Run linting
416
467
  npm run lint
@@ -439,4 +490,4 @@ MIT
439
490
  ---
440
491
 
441
492
  © Eleanor Berger
442
- [ai.intellectronica.net](https://ai.intellectronica.net/)
493
+ [ai.intellectronica.net](https://ai.intellectronica.net/)
@@ -0,0 +1,58 @@
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.FirebaseAgent = void 0;
37
+ const path = __importStar(require("path"));
38
+ const FileSystemUtils_1 = require("../core/FileSystemUtils");
39
+ /**
40
+ * Firebase Studio agent adapter.
41
+ */
42
+ class FirebaseAgent {
43
+ getIdentifier() {
44
+ return 'firebase';
45
+ }
46
+ getName() {
47
+ return 'Firebase Studio';
48
+ }
49
+ async applyRulerConfig(concatenatedRules, projectRoot, agentConfig) {
50
+ const output = agentConfig?.outputPath ?? this.getDefaultOutputPath(projectRoot);
51
+ await (0, FileSystemUtils_1.backupFile)(output);
52
+ await (0, FileSystemUtils_1.writeGeneratedFile)(output, concatenatedRules);
53
+ }
54
+ getDefaultOutputPath(projectRoot) {
55
+ return path.join(projectRoot, '.idx', 'airules.md');
56
+ }
57
+ }
58
+ exports.FirebaseAgent = FirebaseAgent;
@@ -0,0 +1,56 @@
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.OpenHandsAgent = void 0;
37
+ const path = __importStar(require("path"));
38
+ const FileSystemUtils_1 = require("../core/FileSystemUtils");
39
+ class OpenHandsAgent {
40
+ getIdentifier() {
41
+ return 'openhands';
42
+ }
43
+ getName() {
44
+ return 'Open Hands';
45
+ }
46
+ async applyRulerConfig(concatenatedRules, projectRoot, agentConfig) {
47
+ const output = agentConfig?.outputPath ?? this.getDefaultOutputPath(projectRoot);
48
+ await (0, FileSystemUtils_1.ensureDirExists)(path.dirname(output));
49
+ await (0, FileSystemUtils_1.backupFile)(output);
50
+ await (0, FileSystemUtils_1.writeGeneratedFile)(output, concatenatedRules);
51
+ }
52
+ getDefaultOutputPath(projectRoot) {
53
+ return path.join(projectRoot, '.openhands', 'microagents', 'repo.md');
54
+ }
55
+ }
56
+ exports.OpenHandsAgent = OpenHandsAgent;
@@ -58,7 +58,7 @@ function run() {
58
58
  });
59
59
  y.option('agents', {
60
60
  type: 'string',
61
- description: 'Comma-separated list of agent identifiers: copilot, claude, codex, cursor, windsurf, cline, aider',
61
+ description: 'Comma-separated list of agent identifiers: copilot, claude, codex, cursor, windsurf, cline, aider, firebase',
62
62
  });
63
63
  y.option('config', {
64
64
  type: 'string',
@@ -189,6 +189,10 @@ and apply them to your configured AI coding agents.
189
189
  # enabled = true
190
190
  # output_path_instructions = "ruler_aider_instructions.md"
191
191
  # output_path_config = ".aider.conf.yml"
192
+
193
+ # [agents.firebase]
194
+ # enabled = true
195
+ # output_path = ".idx/airules.md"
192
196
  `;
193
197
  if (!(await exists(instructionsPath))) {
194
198
  await fs_1.promises.writeFile(instructionsPath, DEFAULT_INSTRUCTIONS);
@@ -39,7 +39,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.loadConfig = loadConfig;
40
40
  const fs_1 = require("fs");
41
41
  const path = __importStar(require("path"));
42
- const toml_1 = __importDefault(require("toml"));
42
+ const toml_1 = __importDefault(require("@iarna/toml"));
43
43
  const zod_1 = require("zod");
44
44
  const constants_1 = require("../constants");
45
45
  const mcpConfigSchema = zod_1.z
package/dist/lib.js CHANGED
@@ -47,9 +47,12 @@ const CursorAgent_1 = require("./agents/CursorAgent");
47
47
  const WindsurfAgent_1 = require("./agents/WindsurfAgent");
48
48
  const ClineAgent_1 = require("./agents/ClineAgent");
49
49
  const AiderAgent_1 = require("./agents/AiderAgent");
50
+ const FirebaseAgent_1 = require("./agents/FirebaseAgent");
51
+ const OpenHandsAgent_1 = require("./agents/OpenHandsAgent");
50
52
  const merge_1 = require("./mcp/merge");
51
53
  const validate_1 = require("./mcp/validate");
52
54
  const mcp_1 = require("./paths/mcp");
55
+ const propagateOpenHandsMcp_1 = require("./mcp/propagateOpenHandsMcp");
53
56
  const constants_1 = require("./constants");
54
57
  /**
55
58
  * Gets all output paths for an agent, taking into account any config overrides.
@@ -93,6 +96,8 @@ const agents = [
93
96
  new WindsurfAgent_1.WindsurfAgent(),
94
97
  new ClineAgent_1.ClineAgent(),
95
98
  new AiderAgent_1.AiderAgent(),
99
+ new FirebaseAgent_1.FirebaseAgent(),
100
+ new OpenHandsAgent_1.OpenHandsAgent(),
96
101
  ];
97
102
  /**
98
103
  * Applies ruler configurations for all supported AI agents.
@@ -190,27 +195,43 @@ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cli
190
195
  (0, constants_1.logVerbose)(`Agent ${agent.getName()} output paths: ${outputPaths.join(', ')}`, verbose);
191
196
  generatedPaths.push(...outputPaths);
192
197
  if (dryRun) {
193
- (0, constants_1.logVerbose)(`DRY RUN: Would write rules to: ${outputPaths.join(', ')}`, true);
198
+ (0, constants_1.logVerbose)(`DRY RUN: Would write rules to: ${outputPaths.join(', ')}`, verbose);
194
199
  }
195
200
  else {
196
201
  await agent.applyRulerConfig(concatenated, projectRoot, agentConfig);
197
202
  }
198
203
  const dest = await (0, mcp_1.getNativeMcpPath)(agent.getName(), projectRoot);
199
- const enabled = cliMcpEnabled &&
204
+ const mcpEnabledForAgent = cliMcpEnabled &&
200
205
  (agentConfig?.mcp?.enabled ?? config.mcp?.enabled ?? true);
201
- if (dest && rulerMcpJson != null && enabled) {
202
- const strategy = cliMcpStrategy ??
203
- agentConfig?.mcp?.strategy ??
204
- config.mcp?.strategy ??
205
- 'merge';
206
- (0, constants_1.logVerbose)(`Applying MCP config for ${agent.getName()} with strategy: ${strategy}`, verbose);
207
- if (dryRun) {
208
- (0, constants_1.logVerbose)(`DRY RUN: Would apply MCP config to: ${dest}`, true);
206
+ const rulerMcpFile = path.join(rulerDir, 'mcp.json');
207
+ if (dest && mcpEnabledForAgent) {
208
+ if (agent.getIdentifier() === 'openhands') {
209
+ // *** Special handling for Open Hands ***
210
+ if (dryRun) {
211
+ (0, constants_1.logVerbose)(`DRY RUN: Would apply MCP config by updating TOML file: ${dest}`, verbose);
212
+ }
213
+ else {
214
+ await (0, propagateOpenHandsMcp_1.propagateMcpToOpenHands)(rulerMcpFile, dest);
215
+ }
216
+ // Include Open Hands config file in .gitignore
217
+ generatedPaths.push(dest);
209
218
  }
210
219
  else {
211
- const existing = await (0, mcp_1.readNativeMcp)(dest);
212
- const merged = (0, merge_1.mergeMcp)(existing, rulerMcpJson, strategy);
213
- await (0, mcp_1.writeNativeMcp)(dest, merged);
220
+ if (rulerMcpJson) {
221
+ const strategy = cliMcpStrategy ??
222
+ agentConfig?.mcp?.strategy ??
223
+ config.mcp?.strategy ??
224
+ 'merge';
225
+ (0, constants_1.logVerbose)(`Applying MCP config for ${agent.getName()} with strategy: ${strategy}`, verbose);
226
+ if (dryRun) {
227
+ (0, constants_1.logVerbose)(`DRY RUN: Would apply MCP config to: ${dest}`, true);
228
+ }
229
+ else {
230
+ const existing = await (0, mcp_1.readNativeMcp)(dest);
231
+ const merged = (0, merge_1.mergeMcp)(existing, rulerMcpJson, strategy);
232
+ await (0, mcp_1.writeNativeMcp)(dest, merged);
233
+ }
234
+ }
214
235
  }
215
236
  }
216
237
  }
@@ -0,0 +1,83 @@
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
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.propagateMcpToOpenHands = propagateMcpToOpenHands;
40
+ const fs = __importStar(require("fs/promises"));
41
+ const toml_1 = __importDefault(require("@iarna/toml"));
42
+ const FileSystemUtils_1 = require("../core/FileSystemUtils");
43
+ const path = __importStar(require("path"));
44
+ async function propagateMcpToOpenHands(rulerMcpPath, openHandsConfigPath) {
45
+ let rulerMcp;
46
+ try {
47
+ const rulerJsonContent = await fs.readFile(rulerMcpPath, 'utf8');
48
+ rulerMcp = JSON.parse(rulerJsonContent);
49
+ }
50
+ catch {
51
+ return;
52
+ }
53
+ const rulerServers = rulerMcp.mcpServers || {};
54
+ let config = {};
55
+ try {
56
+ const tomlContent = await fs.readFile(openHandsConfigPath, 'utf8');
57
+ config = toml_1.default.parse(tomlContent);
58
+ }
59
+ catch {
60
+ // File doesn't exist, we'll create it.
61
+ }
62
+ if (!config.mcp) {
63
+ config.mcp = {};
64
+ }
65
+ if (!config.mcp.stdio_servers) {
66
+ config.mcp.stdio_servers = [];
67
+ }
68
+ const existingServers = new Map(config.mcp.stdio_servers.map((s) => [s.name, s]));
69
+ for (const [name, serverDef] of Object.entries(rulerServers)) {
70
+ const { command, args, env } = serverDef;
71
+ if (command) {
72
+ const newServer = { name, command };
73
+ if (args)
74
+ newServer.args = args;
75
+ if (env)
76
+ newServer.env = env;
77
+ existingServers.set(name, newServer);
78
+ }
79
+ }
80
+ config.mcp.stdio_servers = Array.from(existingServers.values());
81
+ await (0, FileSystemUtils_1.ensureDirExists)(path.dirname(openHandsConfigPath));
82
+ await fs.writeFile(openHandsConfigPath, toml_1.default.stringify(config));
83
+ }
package/dist/paths/mcp.js CHANGED
@@ -67,6 +67,10 @@ async function getNativeMcpPath(adapterName, projectRoot) {
67
67
  case 'Aider':
68
68
  candidates.push(path.join(projectRoot, '.mcp.json'));
69
69
  break;
70
+ case 'Open Hands':
71
+ // For Open Hands, we target the main config file, not a separate mcp.json
72
+ candidates.push(path.join(projectRoot, '.openhands', 'config.toml'));
73
+ break;
70
74
  default:
71
75
  return null;
72
76
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intellectronica/ruler",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Ruler — apply the same rules to all coding agents",
5
5
  "main": "dist/lib.js",
6
6
  "scripts": {
@@ -43,6 +43,7 @@
43
43
  "ruler": "dist/cli/index.js"
44
44
  },
45
45
  "devDependencies": {
46
+ "@types/iarna__toml": "^2.0.5",
46
47
  "@types/jest": "^29.5.14",
47
48
  "@types/js-yaml": "^4.0.9",
48
49
  "@types/node": "^22.15.24",
@@ -58,8 +59,8 @@
58
59
  "typescript": "^5.8.3"
59
60
  },
60
61
  "dependencies": {
62
+ "@iarna/toml": "^2.2.5",
61
63
  "js-yaml": "^4.1.0",
62
- "toml": "^3.0.0",
63
64
  "yargs": "^17.7.2",
64
65
  "zod": "^3.25.28"
65
66
  }