@intellectronica/ruler 0.2.2 → 0.2.4

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,16 +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` |
47
- | Firebase Studio | `.idx/airules.md` |
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.mdc` |
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` |
48
49
 
49
50
  ## Getting Started
50
51
 
@@ -55,11 +56,13 @@ Node.js 18.x or higher is required.
55
56
  ### Installation
56
57
 
57
58
  **Global Installation (Recommended for CLI use):**
59
+
58
60
  ```bash
59
61
  npm install -g @intellectronica/ruler
60
62
  ```
61
63
 
62
64
  **Using `npx` (for one-off commands):**
65
+
63
66
  ```bash
64
67
  npx @intellectronica/ruler apply
65
68
  ```
@@ -88,25 +91,30 @@ This is your central hub for all AI agent instructions:
88
91
  ### Best Practices for Rule Files
89
92
 
90
93
  **Granularity**: Break down complex instructions into focused `.md` files:
94
+
91
95
  - `coding_style.md`
92
- - `api_conventions.md`
96
+ - `api_conventions.md`
93
97
  - `project_architecture.md`
94
98
  - `security_guidelines.md`
95
99
 
96
100
  **Example rule file (`.ruler/python_guidelines.md`):**
101
+
97
102
  ```markdown
98
103
  # Python Project Guidelines
99
104
 
100
105
  ## General Style
106
+
101
107
  - Follow PEP 8 for all Python code
102
108
  - Use type hints for all function signatures and complex variables
103
109
  - Keep functions short and focused on a single task
104
110
 
105
111
  ## Error Handling
112
+
106
113
  - Use specific exception types rather than generic `Exception`
107
114
  - Log errors effectively with context
108
115
 
109
116
  ## Security
117
+
110
118
  - Always validate and sanitize user input
111
119
  - Be mindful of potential injection vulnerabilities
112
120
  ```
@@ -114,52 +122,59 @@ This is your central hub for all AI agent instructions:
114
122
  ## Usage: The `apply` Command
115
123
 
116
124
  ### Primary Command
125
+
117
126
  ```bash
118
127
  ruler apply [options]
119
128
  ```
120
129
 
121
130
  ### Options
122
131
 
123
- | Option | Description |
124
- |--------|-------------|
125
- | `--project-root <path>` | Path to your project's root (default: current directory) |
126
- | `--agents <agent1,agent2,...>` | Comma-separated list of agent names to target |
127
- | `--config <path>` | Path to a custom `ruler.toml` configuration file |
128
- | `--mcp` / `--with-mcp` | Enable applying MCP server configurations (default: true) |
129
- | `--no-mcp` | Disable applying MCP server configurations |
130
- | `--mcp-overwrite` | Overwrite native MCP config entirely instead of merging |
131
- | `--gitignore` | Enable automatic .gitignore updates (default: true) |
132
- | `--no-gitignore` | Disable automatic .gitignore updates |
133
- | `--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 |
134
143
 
135
144
  ### Common Examples
136
145
 
137
146
  **Apply rules to all configured agents:**
147
+
138
148
  ```bash
139
149
  ruler apply
140
150
  ```
141
151
 
142
152
  **Apply rules only to GitHub Copilot and Claude:**
153
+
143
154
  ```bash
144
155
  ruler apply --agents copilot,claude
145
156
  ```
146
157
 
147
158
  **Apply rules only to Firebase Studio:**
159
+
148
160
  ```bash
149
161
  ruler apply --agents firebase
150
162
  ```
151
163
 
152
164
  **Use a specific configuration file:**
165
+
153
166
  ```bash
154
167
  ruler apply --config ./team-configs/ruler.frontend.toml
155
168
  ```
156
169
 
157
170
  **Apply rules with verbose output:**
171
+
158
172
  ```bash
159
173
  ruler apply --verbose
160
174
  ```
161
175
 
162
176
  **Apply rules but skip MCP and .gitignore updates:**
177
+
163
178
  ```bash
164
179
  ruler apply --no-mcp --no-gitignore
165
180
  ```
@@ -167,9 +182,11 @@ ruler apply --no-mcp --no-gitignore
167
182
  ## Configuration (`ruler.toml`) in Detail
168
183
 
169
184
  ### Location
185
+
170
186
  Defaults to `.ruler/ruler.toml` in the project root. Override with `--config` CLI option.
171
187
 
172
188
  ### Complete Example
189
+
173
190
  ```toml
174
191
  # Default agents to run when --agents is not specified
175
192
  # Uses case-insensitive substring matching
@@ -226,16 +243,22 @@ enabled = false
226
243
  MCP provides broader context to AI models through server configurations. Ruler can manage and distribute these settings across compatible agents.
227
244
 
228
245
  ### `.ruler/mcp.json`
246
+
229
247
  Define your project's MCP servers:
248
+
230
249
  ```json
231
250
  {
232
251
  "mcpServers": {
233
252
  "filesystem": {
234
253
  "command": "npx",
235
- "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/project"]
254
+ "args": [
255
+ "-y",
256
+ "@modelcontextprotocol/server-filesystem",
257
+ "/path/to/project"
258
+ ]
236
259
  },
237
260
  "git": {
238
- "command": "npx",
261
+ "command": "npx",
239
262
  "args": ["-y", "@modelcontextprotocol/server-git", "--repository", "."]
240
263
  }
241
264
  }
@@ -249,12 +272,14 @@ Ruler uses this file with the `merge` (default) or `overwrite` strategy, control
249
272
  Ruler automatically manages your `.gitignore` file to keep generated agent configuration files out of version control.
250
273
 
251
274
  ### How it Works
275
+
252
276
  - Creates or updates `.gitignore` in your project root
253
277
  - Adds paths to a managed block marked with `# START Ruler Generated Files` and `# END Ruler Generated Files`
254
278
  - Preserves existing content outside this block
255
279
  - Sorts paths alphabetically and uses relative POSIX-style paths
256
280
 
257
281
  ### Example `.gitignore` Section
282
+
258
283
  ```gitignore
259
284
  # Your existing rules
260
285
  node_modules/
@@ -263,7 +288,7 @@ node_modules/
263
288
  # START Ruler Generated Files
264
289
  .aider.conf.yml
265
290
  .clinerules
266
- .cursor/rules/ruler_cursor_instructions.md
291
+ .cursor/rules/ruler_cursor_instructions.mdc
267
292
  .github/copilot-instructions.md
268
293
  .windsurf/rules/ruler_windsurf_instructions.md
269
294
  AGENTS.md
@@ -275,6 +300,7 @@ dist/
275
300
  ```
276
301
 
277
302
  ### Control Options
303
+
278
304
  - **CLI flags**: `--gitignore` or `--no-gitignore`
279
305
  - **Configuration**: `[gitignore].enabled` in `ruler.toml`
280
306
  - **Default**: enabled
@@ -282,6 +308,7 @@ dist/
282
308
  ## Practical Usage Scenarios
283
309
 
284
310
  ### Scenario 1: Getting Started Quickly
311
+
285
312
  ```bash
286
313
  # Initialize Ruler in your project
287
314
  cd your-project
@@ -296,16 +323,19 @@ ruler apply
296
323
  ```
297
324
 
298
325
  ### Scenario 2: Team Standardization
326
+
299
327
  1. Create `.ruler/coding_standards.md`, `.ruler/api_usage.md`
300
328
  2. Commit the `.ruler` directory to your repository
301
329
  3. Team members pull changes and run `ruler apply` to update their local AI agent configurations
302
330
 
303
331
  ### Scenario 3: Project-Specific Context for AI
332
+
304
333
  1. Detail your project's architecture in `.ruler/project_overview.md`
305
- 2. Describe primary data structures in `.ruler/data_models.md`
334
+ 2. Describe primary data structures in `.ruler/data_models.md`
306
335
  3. Run `ruler apply` to help AI tools provide more relevant suggestions
307
336
 
308
337
  ### Integration with NPM Scripts
338
+
309
339
  ```json
310
340
  {
311
341
  "scripts": {
@@ -317,6 +347,7 @@ ruler apply
317
347
  ```
318
348
 
319
349
  ### Integration with GitHub Actions
350
+
320
351
  ```yaml
321
352
  # .github/workflows/ruler-check.yml
322
353
  name: Check Ruler Configuration
@@ -333,13 +364,13 @@ jobs:
333
364
  with:
334
365
  node-version: '18'
335
366
  cache: 'npm'
336
-
367
+
337
368
  - name: Install Ruler
338
369
  run: npm install -g @intellectronica/ruler
339
-
370
+
340
371
  - name: Apply Ruler configuration
341
372
  run: ruler apply --no-gitignore
342
-
373
+
343
374
  - name: Check for uncommitted changes
344
375
  run: |
345
376
  if [[ -n $(git status --porcelain) ]]; then
@@ -354,28 +385,35 @@ jobs:
354
385
  ### Common Issues
355
386
 
356
387
  **"Cannot find module" errors:**
388
+
357
389
  - Ensure Ruler is installed globally: `npm install -g @intellectronica/ruler`
358
390
  - Or use `npx @intellectronica/ruler`
359
391
 
360
392
  **Permission denied errors:**
393
+
361
394
  - On Unix systems, you may need `sudo` for global installation
362
395
 
363
396
  **Agent files not updating:**
397
+
364
398
  - Check if the agent is enabled in `ruler.toml`
365
399
  - Verify agent isn't excluded by `--agents` flag
366
400
  - Use `--verbose` to see detailed execution logs
367
401
 
368
402
  **Configuration validation errors:**
403
+
369
404
  - Ruler now validates `ruler.toml` format and will show specific error details
370
405
  - Check that all configuration values match the expected types and formats
371
406
 
372
407
  ### Debug Mode
408
+
373
409
  Use `--verbose` flag to see detailed execution logs:
410
+
374
411
  ```bash
375
412
  ruler apply --verbose
376
413
  ```
377
414
 
378
415
  This shows:
416
+
379
417
  - Configuration loading details
380
418
  - Agent selection logic
381
419
  - File processing information
@@ -401,6 +439,7 @@ A: Version 0.2.0 is backward compatible. Your existing `.ruler/` directory and `
401
439
  ## Development
402
440
 
403
441
  ### Setup
442
+
404
443
  ```bash
405
444
  git clone https://github.com/intellectronica/ruler.git
406
445
  cd ruler
@@ -409,6 +448,7 @@ npm run build
409
448
  ```
410
449
 
411
450
  ### Testing
451
+
412
452
  ```bash
413
453
  # Run all tests
414
454
  npm test
@@ -421,6 +461,7 @@ npm run test:watch
421
461
  ```
422
462
 
423
463
  ### Code Quality
464
+
424
465
  ```bash
425
466
  # Run linting
426
467
  npm run lint
@@ -449,4 +490,4 @@ MIT
449
490
  ---
450
491
 
451
492
  © Eleanor Berger
452
- [ai.intellectronica.net](https://ai.intellectronica.net/)
493
+ [ai.intellectronica.net](https://ai.intellectronica.net/)
@@ -55,5 +55,8 @@ class CopilotAgent {
55
55
  getDefaultOutputPath(projectRoot) {
56
56
  return path.join(projectRoot, '.github', 'copilot-instructions.md');
57
57
  }
58
+ getMcpServerKey() {
59
+ return 'servers';
60
+ }
58
61
  }
59
62
  exports.CopilotAgent = CopilotAgent;
@@ -53,7 +53,7 @@ class CursorAgent {
53
53
  await (0, FileSystemUtils_1.writeGeneratedFile)(output, concatenatedRules);
54
54
  }
55
55
  getDefaultOutputPath(projectRoot) {
56
- return path.join(projectRoot, '.cursor', 'rules', 'ruler_cursor_instructions.md');
56
+ return path.join(projectRoot, '.cursor', 'rules', 'ruler_cursor_instructions.mdc');
57
57
  }
58
58
  }
59
59
  exports.CursorAgent = CursorAgent;
@@ -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;
@@ -175,7 +175,7 @@ and apply them to your configured AI coding agents.
175
175
 
176
176
  # [agents.cursor]
177
177
  # enabled = true
178
- # output_path = ".cursor/rules/ruler_cursor_instructions.md"
178
+ # output_path = ".cursor/rules/ruler_cursor_instructions.mdc"
179
179
 
180
180
  # [agents.windsurf]
181
181
  # enabled = true
@@ -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
@@ -48,9 +48,11 @@ const WindsurfAgent_1 = require("./agents/WindsurfAgent");
48
48
  const ClineAgent_1 = require("./agents/ClineAgent");
49
49
  const AiderAgent_1 = require("./agents/AiderAgent");
50
50
  const FirebaseAgent_1 = require("./agents/FirebaseAgent");
51
+ const OpenHandsAgent_1 = require("./agents/OpenHandsAgent");
51
52
  const merge_1 = require("./mcp/merge");
52
53
  const validate_1 = require("./mcp/validate");
53
54
  const mcp_1 = require("./paths/mcp");
55
+ const propagateOpenHandsMcp_1 = require("./mcp/propagateOpenHandsMcp");
54
56
  const constants_1 = require("./constants");
55
57
  /**
56
58
  * Gets all output paths for an agent, taking into account any config overrides.
@@ -95,6 +97,7 @@ const agents = [
95
97
  new ClineAgent_1.ClineAgent(),
96
98
  new AiderAgent_1.AiderAgent(),
97
99
  new FirebaseAgent_1.FirebaseAgent(),
100
+ new OpenHandsAgent_1.OpenHandsAgent(),
98
101
  ];
99
102
  /**
100
103
  * Applies ruler configurations for all supported AI agents.
@@ -192,27 +195,45 @@ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cli
192
195
  (0, constants_1.logVerbose)(`Agent ${agent.getName()} output paths: ${outputPaths.join(', ')}`, verbose);
193
196
  generatedPaths.push(...outputPaths);
194
197
  if (dryRun) {
195
- (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);
196
199
  }
197
200
  else {
198
201
  await agent.applyRulerConfig(concatenated, projectRoot, agentConfig);
199
202
  }
200
203
  const dest = await (0, mcp_1.getNativeMcpPath)(agent.getName(), projectRoot);
201
- const enabled = cliMcpEnabled &&
204
+ const mcpEnabledForAgent = cliMcpEnabled &&
202
205
  (agentConfig?.mcp?.enabled ?? config.mcp?.enabled ?? true);
203
- if (dest && rulerMcpJson != null && enabled) {
204
- const strategy = cliMcpStrategy ??
205
- agentConfig?.mcp?.strategy ??
206
- config.mcp?.strategy ??
207
- 'merge';
208
- (0, constants_1.logVerbose)(`Applying MCP config for ${agent.getName()} with strategy: ${strategy}`, verbose);
209
- if (dryRun) {
210
- (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);
211
218
  }
212
219
  else {
213
- const existing = await (0, mcp_1.readNativeMcp)(dest);
214
- const merged = (0, merge_1.mergeMcp)(existing, rulerMcpJson, strategy);
215
- 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
+ // Determine the correct server key for the agent
226
+ const serverKey = agent.getMcpServerKey?.() ?? 'mcpServers';
227
+ (0, constants_1.logVerbose)(`Applying MCP config for ${agent.getName()} with strategy: ${strategy} and key: ${serverKey}`, verbose);
228
+ if (dryRun) {
229
+ (0, constants_1.logVerbose)(`DRY RUN: Would apply MCP config to: ${dest}`, true);
230
+ }
231
+ else {
232
+ const existing = await (0, mcp_1.readNativeMcp)(dest);
233
+ const merged = (0, merge_1.mergeMcp)(existing, rulerMcpJson, strategy, serverKey);
234
+ await (0, mcp_1.writeNativeMcp)(dest, merged);
235
+ }
236
+ }
216
237
  }
217
238
  }
218
239
  }
package/dist/mcp/merge.js CHANGED
@@ -6,16 +6,26 @@ exports.mergeMcp = mergeMcp;
6
6
  * @param base Existing native MCP config object.
7
7
  * @param incoming Ruler MCP config object.
8
8
  * @param strategy Merge strategy: 'merge' to union servers, 'overwrite' to replace.
9
+ * @param serverKey The key to use for servers in the output (e.g., 'servers' for Copilot, 'mcpServers' for others).
9
10
  * @returns Merged MCP config object.
10
11
  */
11
- function mergeMcp(base, incoming, strategy) {
12
+ function mergeMcp(base, incoming, strategy, serverKey) {
12
13
  if (strategy === 'overwrite') {
13
- return incoming;
14
+ // Ensure the incoming object uses the correct server key.
15
+ const incomingServers = incoming.mcpServers || {};
16
+ return {
17
+ [serverKey]: incomingServers,
18
+ };
14
19
  }
15
- const baseServers = base.mcpServers || {};
20
+ const baseServers = base[serverKey] ||
21
+ base.mcpServers ||
22
+ {}; // Handle legacy key in existing files
16
23
  const incomingServers = incoming.mcpServers || {};
24
+ const mergedServers = { ...baseServers, ...incomingServers };
25
+ const newBase = { ...base };
26
+ delete newBase.mcpServers; // Remove old key if present
17
27
  return {
18
- ...base,
19
- mcpServers: { ...baseServers, ...incomingServers },
28
+ ...newBase,
29
+ [serverKey]: mergedServers,
20
30
  };
21
31
  }
@@ -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.2",
3
+ "version": "0.2.4",
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
  }