@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 +8 -1
- package/dist/agents/AbstractAgent.js +4 -2
- package/dist/agents/AgentsMdAgent.js +5 -3
- package/dist/agents/AiderAgent.js +5 -3
- package/dist/agents/AugmentCodeAgent.js +4 -2
- package/dist/agents/CodexCliAgent.js +27 -2
- package/dist/agents/CursorAgent.js +4 -2
- package/dist/agents/RooCodeAgent.js +139 -0
- package/dist/agents/WindsurfAgent.js +90 -3
- package/dist/agents/index.js +2 -0
- package/dist/cli/commands.js +5 -0
- package/dist/cli/handlers.js +2 -1
- package/dist/core/apply-engine.js +46 -28
- package/dist/lib.js +4 -4
- package/dist/mcp/capabilities.js +16 -2
- package/dist/mcp/propagateOpenCodeMcp.js +5 -1
- package/dist/mcp/propagateOpenHandsMcp.js +5 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
59
|
-
|
|
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;
|
package/dist/agents/index.js
CHANGED
|
@@ -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
|
];
|
package/dist/cli/commands.js
CHANGED
|
@@ -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) => {
|
package/dist/cli/handlers.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
package/dist/mcp/capabilities.js
CHANGED
|
@@ -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 (
|
|
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
|
}
|