@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 +72 -31
- package/dist/agents/CopilotAgent.js +3 -0
- package/dist/agents/CursorAgent.js +1 -1
- package/dist/agents/OpenHandsAgent.js +56 -0
- package/dist/cli/commands.js +1 -1
- package/dist/core/ConfigLoader.js +1 -1
- package/dist/lib.js +34 -13
- package/dist/mcp/merge.js +15 -5
- package/dist/mcp/propagateOpenHandsMcp.js +83 -0
- package/dist/paths/mcp.js +4 -0
- package/package.json +3 -2
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
|
|
39
|
-
|
|
|
40
|
-
| GitHub Copilot
|
|
41
|
-
| Claude Code
|
|
42
|
-
| OpenAI Codex CLI
|
|
43
|
-
| Cursor
|
|
44
|
-
| Windsurf
|
|
45
|
-
| Cline
|
|
46
|
-
| Aider
|
|
47
|
-
| Firebase Studio
|
|
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
|
|
124
|
-
|
|
125
|
-
| `--project-root <path>`
|
|
126
|
-
| `--agents <agent1,agent2,...>` | Comma-separated list of agent names to target
|
|
127
|
-
| `--config <path>`
|
|
128
|
-
| `--mcp` / `--with-mcp`
|
|
129
|
-
| `--no-mcp`
|
|
130
|
-
| `--mcp-overwrite`
|
|
131
|
-
| `--gitignore`
|
|
132
|
-
| `--no-gitignore`
|
|
133
|
-
| `--verbose` / `-v`
|
|
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": [
|
|
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.
|
|
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/)
|
|
@@ -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.
|
|
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;
|
package/dist/cli/commands.js
CHANGED
|
@@ -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.
|
|
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(', ')}`,
|
|
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
|
|
204
|
+
const mcpEnabledForAgent = cliMcpEnabled &&
|
|
202
205
|
(agentConfig?.mcp?.enabled ?? config.mcp?.enabled ?? true);
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
...
|
|
19
|
-
|
|
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.
|
|
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
|
}
|