@intellectronica/ruler 0.3.4 → 0.3.6
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/dist/agents/CodexCliAgent.js +22 -66
- package/dist/agents/CopilotAgent.js +79 -1
- package/dist/agents/GeminiCliAgent.js +19 -3
- package/dist/core/ConfigLoader.js +2 -2
- package/dist/core/GitignoreUtils.js +11 -2
- package/dist/core/UnifiedConfigLoader.js +13 -11
- package/dist/core/apply-engine.js +16 -57
- package/dist/lib.js +4 -3
- package/dist/mcp/propagateOpenHandsMcp.js +1 -2
- package/dist/paths/mcp.js +2 -6
- package/package.json +1 -2
|
@@ -36,7 +36,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.CodexCliAgent = void 0;
|
|
37
37
|
const path = __importStar(require("path"));
|
|
38
38
|
const fs_1 = require("fs");
|
|
39
|
-
const toml = __importStar(require("toml"));
|
|
40
39
|
const toml_1 = require("@iarna/toml");
|
|
41
40
|
const AgentsMdAgent_1 = require("./AgentsMdAgent");
|
|
42
41
|
const FileSystemUtils_1 = require("../core/FileSystemUtils");
|
|
@@ -44,26 +43,26 @@ const constants_1 = require("../constants");
|
|
|
44
43
|
/**
|
|
45
44
|
* OpenAI Codex CLI agent adapter.
|
|
46
45
|
*/
|
|
47
|
-
class CodexCliAgent
|
|
46
|
+
class CodexCliAgent {
|
|
47
|
+
constructor() {
|
|
48
|
+
this.agentsMdAgent = new AgentsMdAgent_1.AgentsMdAgent();
|
|
49
|
+
}
|
|
48
50
|
getIdentifier() {
|
|
49
51
|
return 'codex';
|
|
50
52
|
}
|
|
51
53
|
getName() {
|
|
52
54
|
return 'OpenAI Codex CLI';
|
|
53
55
|
}
|
|
54
|
-
async applyRulerConfig(concatenatedRules, projectRoot, rulerMcpJson, agentConfig) {
|
|
55
|
-
// First perform idempotent AGENTS.md write via
|
|
56
|
-
await
|
|
56
|
+
async applyRulerConfig(concatenatedRules, projectRoot, rulerMcpJson, agentConfig, backup = true) {
|
|
57
|
+
// First perform idempotent AGENTS.md write via composed AgentsMdAgent
|
|
58
|
+
await this.agentsMdAgent.applyRulerConfig(concatenatedRules, projectRoot, null, {
|
|
57
59
|
// Preserve explicit outputPath precedence semantics if provided.
|
|
58
60
|
outputPath: agentConfig?.outputPath ||
|
|
59
61
|
agentConfig?.outputPathInstructions ||
|
|
60
62
|
undefined,
|
|
61
|
-
});
|
|
62
|
-
//
|
|
63
|
-
const defaults =
|
|
64
|
-
instructions: path.join(projectRoot, constants_1.DEFAULT_RULES_FILENAME),
|
|
65
|
-
config: path.join(projectRoot, '.codex', 'config.toml'),
|
|
66
|
-
};
|
|
63
|
+
}, backup);
|
|
64
|
+
// Use proper path resolution from getDefaultOutputPath and agentConfig
|
|
65
|
+
const defaults = this.getDefaultOutputPath(projectRoot);
|
|
67
66
|
const mcpEnabled = agentConfig?.mcp?.enabled ?? true;
|
|
68
67
|
if (mcpEnabled && rulerMcpJson) {
|
|
69
68
|
// Apply MCP server filtering and transformation
|
|
@@ -73,7 +72,7 @@ class CodexCliAgent extends AgentsMdAgent_1.AgentsMdAgent {
|
|
|
73
72
|
return; // No compatible servers found
|
|
74
73
|
}
|
|
75
74
|
const filteredRulerMcpJson = filteredMcpConfig;
|
|
76
|
-
// Determine the config file path
|
|
75
|
+
// Determine the config file path using proper precedence
|
|
77
76
|
const configPath = agentConfig?.outputPathConfig ?? defaults.config;
|
|
78
77
|
// Ensure the parent directory exists
|
|
79
78
|
await fs_1.promises.mkdir(path.dirname(configPath), { recursive: true });
|
|
@@ -85,7 +84,7 @@ class CodexCliAgent extends AgentsMdAgent_1.AgentsMdAgent {
|
|
|
85
84
|
let existingConfig = {};
|
|
86
85
|
try {
|
|
87
86
|
const existingContent = await fs_1.promises.readFile(configPath, 'utf8');
|
|
88
|
-
existingConfig =
|
|
87
|
+
existingConfig = (0, toml_1.parse)(existingContent);
|
|
89
88
|
}
|
|
90
89
|
catch {
|
|
91
90
|
// File doesn't exist or can't be parsed, use empty config
|
|
@@ -121,62 +120,19 @@ class CodexCliAgent extends AgentsMdAgent_1.AgentsMdAgent {
|
|
|
121
120
|
updatedConfig.mcp_servers[serverName] = mcpServer;
|
|
122
121
|
}
|
|
123
122
|
}
|
|
124
|
-
// Convert to TOML
|
|
125
|
-
|
|
126
|
-
//
|
|
127
|
-
const
|
|
128
|
-
delete configWithoutMcpServers.mcp_servers;
|
|
129
|
-
if (Object.keys(configWithoutMcpServers).length > 0) {
|
|
130
|
-
tomlContent += (0, toml_1.stringify)(configWithoutMcpServers);
|
|
131
|
-
}
|
|
132
|
-
// Now handle mcp_servers with special formatting for env
|
|
133
|
-
if (updatedConfig.mcp_servers &&
|
|
134
|
-
Object.keys(updatedConfig.mcp_servers).length > 0) {
|
|
135
|
-
for (const [serverName, serverConfig] of Object.entries(updatedConfig.mcp_servers)) {
|
|
136
|
-
tomlContent += `\n[mcp_servers.${serverName}]\n`;
|
|
137
|
-
// Add command
|
|
138
|
-
if (serverConfig.command) {
|
|
139
|
-
tomlContent += `command = "${serverConfig.command}"\n`;
|
|
140
|
-
}
|
|
141
|
-
// Add args if present
|
|
142
|
-
if (serverConfig.args && Array.isArray(serverConfig.args)) {
|
|
143
|
-
const argsStr = JSON.stringify(serverConfig.args)
|
|
144
|
-
.replace(/"/g, '"')
|
|
145
|
-
.replace(/,/g, ', ');
|
|
146
|
-
tomlContent += `args = ${argsStr}\n`;
|
|
147
|
-
}
|
|
148
|
-
// Add env as inline table if present
|
|
149
|
-
if (serverConfig.env && Object.keys(serverConfig.env).length > 0) {
|
|
150
|
-
tomlContent += `env = { `;
|
|
151
|
-
const entries = Object.entries(serverConfig.env);
|
|
152
|
-
for (let i = 0; i < entries.length; i++) {
|
|
153
|
-
const [key, value] = entries[i];
|
|
154
|
-
tomlContent += `${key} = "${value}"`;
|
|
155
|
-
if (i < entries.length - 1) {
|
|
156
|
-
tomlContent += ', ';
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
tomlContent += ` }\n`;
|
|
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
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
123
|
+
// Convert to TOML using structured objects
|
|
124
|
+
const finalConfig = { ...updatedConfig };
|
|
125
|
+
// @iarna/toml should handle the formatting properly
|
|
126
|
+
const tomlContent = (0, toml_1.stringify)(finalConfig);
|
|
177
127
|
await (0, FileSystemUtils_1.writeGeneratedFile)(configPath, tomlContent);
|
|
178
128
|
}
|
|
179
129
|
}
|
|
130
|
+
getDefaultOutputPath(projectRoot) {
|
|
131
|
+
return {
|
|
132
|
+
instructions: path.join(projectRoot, constants_1.DEFAULT_RULES_FILENAME),
|
|
133
|
+
config: path.join(projectRoot, '.codex', 'config.toml'),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
180
136
|
supportsMcpStdio() {
|
|
181
137
|
return true;
|
|
182
138
|
}
|
|
@@ -1,17 +1,95 @@
|
|
|
1
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
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
exports.CopilotAgent = void 0;
|
|
37
|
+
const path = __importStar(require("path"));
|
|
4
38
|
const AgentsMdAgent_1 = require("./AgentsMdAgent");
|
|
39
|
+
const FileSystemUtils_1 = require("../core/FileSystemUtils");
|
|
40
|
+
const fs_1 = require("fs");
|
|
5
41
|
/**
|
|
6
42
|
* GitHub Copilot agent adapter.
|
|
43
|
+
* Writes to both AGENTS.md (for web-based GitHub Copilot) and
|
|
44
|
+
* .github/copilot-instructions.md (for VS Code extension compatibility).
|
|
7
45
|
*/
|
|
8
|
-
class CopilotAgent
|
|
46
|
+
class CopilotAgent {
|
|
47
|
+
constructor() {
|
|
48
|
+
this.agentsMdAgent = new AgentsMdAgent_1.AgentsMdAgent();
|
|
49
|
+
}
|
|
9
50
|
getIdentifier() {
|
|
10
51
|
return 'copilot';
|
|
11
52
|
}
|
|
12
53
|
getName() {
|
|
13
54
|
return 'GitHub Copilot';
|
|
14
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Returns multiple output paths to ensure both files are added to .gitignore.
|
|
58
|
+
*/
|
|
59
|
+
getDefaultOutputPath(projectRoot) {
|
|
60
|
+
return {
|
|
61
|
+
instructions: path.join(projectRoot, 'AGENTS.md'),
|
|
62
|
+
legacy: path.join(projectRoot, '.github', 'copilot-instructions.md'),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
async applyRulerConfig(concatenatedRules, projectRoot, rulerMcpJson, agentConfig, backup = true) {
|
|
66
|
+
// First, write to AGENTS.md using the existing AgentsMdAgent infrastructure
|
|
67
|
+
await this.agentsMdAgent.applyRulerConfig(concatenatedRules, projectRoot, null, // No MCP config needed for the instructions file
|
|
68
|
+
{
|
|
69
|
+
// Preserve explicit outputPath precedence semantics if provided
|
|
70
|
+
outputPath: agentConfig?.outputPath || agentConfig?.outputPathInstructions,
|
|
71
|
+
}, backup);
|
|
72
|
+
// Additionally write to .github/copilot-instructions.md for VS Code extension compatibility
|
|
73
|
+
const outputPaths = this.getDefaultOutputPath(projectRoot);
|
|
74
|
+
const legacyPath = path.resolve(projectRoot, outputPaths.legacy);
|
|
75
|
+
// Add marker comment to the content to identify it as generated
|
|
76
|
+
const contentWithMarker = `<!-- Generated by Ruler -->\n${concatenatedRules}`;
|
|
77
|
+
await (0, FileSystemUtils_1.ensureDirExists)(path.dirname(legacyPath));
|
|
78
|
+
// Check if content has changed (idempotency)
|
|
79
|
+
let existingLegacy = null;
|
|
80
|
+
try {
|
|
81
|
+
existingLegacy = await fs_1.promises.readFile(legacyPath, 'utf8');
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
existingLegacy = null;
|
|
85
|
+
}
|
|
86
|
+
if (existingLegacy === null || existingLegacy !== contentWithMarker) {
|
|
87
|
+
if (backup) {
|
|
88
|
+
await (0, FileSystemUtils_1.backupFile)(legacyPath);
|
|
89
|
+
}
|
|
90
|
+
await (0, FileSystemUtils_1.writeGeneratedFile)(legacyPath, contentWithMarker);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
15
93
|
getMcpServerKey() {
|
|
16
94
|
return 'servers';
|
|
17
95
|
}
|
|
@@ -44,13 +44,12 @@ class GeminiCliAgent extends AgentsMdAgent_1.AgentsMdAgent {
|
|
|
44
44
|
getName() {
|
|
45
45
|
return 'Gemini CLI';
|
|
46
46
|
}
|
|
47
|
-
async applyRulerConfig(concatenatedRules, projectRoot, rulerMcpJson,
|
|
48
|
-
agentConfig) {
|
|
47
|
+
async applyRulerConfig(concatenatedRules, projectRoot, rulerMcpJson, agentConfig) {
|
|
49
48
|
// First, perform idempotent write of AGENTS.md via base class
|
|
50
49
|
await super.applyRulerConfig(concatenatedRules, projectRoot, null, {
|
|
51
50
|
outputPath: agentConfig?.outputPath,
|
|
52
51
|
});
|
|
53
|
-
//
|
|
52
|
+
// Prepare .gemini/settings.json with contextFileName and MCP configuration
|
|
54
53
|
const settingsPath = path.join(projectRoot, '.gemini', 'settings.json');
|
|
55
54
|
let existingSettings = {};
|
|
56
55
|
try {
|
|
@@ -66,6 +65,23 @@ class GeminiCliAgent extends AgentsMdAgent_1.AgentsMdAgent {
|
|
|
66
65
|
...existingSettings,
|
|
67
66
|
contextFileName: 'AGENTS.md',
|
|
68
67
|
};
|
|
68
|
+
// Handle MCP server configuration if provided
|
|
69
|
+
const mcpEnabled = agentConfig?.mcp?.enabled ?? true;
|
|
70
|
+
if (mcpEnabled && rulerMcpJson) {
|
|
71
|
+
const strategy = agentConfig?.mcp?.strategy ?? 'merge';
|
|
72
|
+
if (strategy === 'overwrite') {
|
|
73
|
+
// For overwrite, preserve existing settings except MCP servers
|
|
74
|
+
const incomingServers = rulerMcpJson.mcpServers || {};
|
|
75
|
+
updated[this.getMcpServerKey()] = incomingServers;
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
// For merge strategy, merge with existing MCP servers
|
|
79
|
+
const baseServers = existingSettings[this.getMcpServerKey()] || {};
|
|
80
|
+
const incomingServers = rulerMcpJson.mcpServers || {};
|
|
81
|
+
const mergedServers = { ...baseServers, ...incomingServers };
|
|
82
|
+
updated[this.getMcpServerKey()] = mergedServers;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
69
85
|
await fs_1.promises.mkdir(path.dirname(settingsPath), { recursive: true });
|
|
70
86
|
await fs_1.promises.writeFile(settingsPath, JSON.stringify(updated, null, 2));
|
|
71
87
|
}
|
|
@@ -37,7 +37,7 @@ exports.loadConfig = loadConfig;
|
|
|
37
37
|
const fs_1 = require("fs");
|
|
38
38
|
const path = __importStar(require("path"));
|
|
39
39
|
const os = __importStar(require("os"));
|
|
40
|
-
const
|
|
40
|
+
const toml_1 = require("@iarna/toml");
|
|
41
41
|
const zod_1 = require("zod");
|
|
42
42
|
const constants_1 = require("../constants");
|
|
43
43
|
const mcpConfigSchema = zod_1.z
|
|
@@ -97,7 +97,7 @@ async function loadConfig(options) {
|
|
|
97
97
|
let raw = {};
|
|
98
98
|
try {
|
|
99
99
|
const text = await fs_1.promises.readFile(configFile, 'utf8');
|
|
100
|
-
raw = text.trim() ?
|
|
100
|
+
raw = text.trim() ? (0, toml_1.parse)(text) : {};
|
|
101
101
|
// Validate the configuration with zod
|
|
102
102
|
const validationResult = rulerConfigSchema.safeParse(raw);
|
|
103
103
|
if (!validationResult.success) {
|
|
@@ -57,8 +57,9 @@ async function updateGitignore(projectRoot, paths) {
|
|
|
57
57
|
throw err;
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
|
-
// Convert paths to relative POSIX format
|
|
61
|
-
const relativePaths = paths
|
|
60
|
+
// Convert paths to repo-relative POSIX format with leading /
|
|
61
|
+
const relativePaths = paths
|
|
62
|
+
.map((p) => {
|
|
62
63
|
let relative;
|
|
63
64
|
if (path.isAbsolute(p)) {
|
|
64
65
|
relative = path.relative(projectRoot, p);
|
|
@@ -78,6 +79,14 @@ async function updateGitignore(projectRoot, paths) {
|
|
|
78
79
|
}
|
|
79
80
|
}
|
|
80
81
|
return relative.replace(/\\/g, '/'); // Convert to POSIX format
|
|
82
|
+
})
|
|
83
|
+
.filter((p) => {
|
|
84
|
+
// Never include any path that resides inside a .ruler directory (inputs, not outputs)
|
|
85
|
+
return !p.includes('/.ruler/') && !p.startsWith('.ruler/');
|
|
86
|
+
})
|
|
87
|
+
.map((p) => {
|
|
88
|
+
// Always write full repository-relative paths (prefix with leading /)
|
|
89
|
+
return p.startsWith('/') ? p : `/${p}`;
|
|
81
90
|
});
|
|
82
91
|
// Get all existing paths from .gitignore (excluding Ruler block)
|
|
83
92
|
const existingPaths = getExistingPathsExcludingRulerBlock(existingContent);
|
|
@@ -36,7 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.loadUnifiedConfig = loadUnifiedConfig;
|
|
37
37
|
const fs_1 = require("fs");
|
|
38
38
|
const path = __importStar(require("path"));
|
|
39
|
-
const
|
|
39
|
+
const toml_1 = require("@iarna/toml");
|
|
40
40
|
const hash_1 = require("./hash");
|
|
41
41
|
const RuleProcessor_1 = require("./RuleProcessor");
|
|
42
42
|
const FileSystemUtils = __importStar(require("./FileSystemUtils"));
|
|
@@ -58,7 +58,7 @@ async function loadUnifiedConfig(options) {
|
|
|
58
58
|
: path.join(meta.rulerDir, 'ruler.toml');
|
|
59
59
|
try {
|
|
60
60
|
const text = await fs_1.promises.readFile(tomlFile, 'utf8');
|
|
61
|
-
tomlRaw = text.trim() ?
|
|
61
|
+
tomlRaw = text.trim() ? (0, toml_1.parse)(text) : {};
|
|
62
62
|
meta.configFile = tomlFile;
|
|
63
63
|
}
|
|
64
64
|
catch (err) {
|
|
@@ -228,11 +228,21 @@ async function loadUnifiedConfig(options) {
|
|
|
228
228
|
try {
|
|
229
229
|
await fs_1.promises.access(mcpFile);
|
|
230
230
|
mcpJsonExists = true;
|
|
231
|
-
|
|
231
|
+
// Warning is handled by apply-engine to avoid duplication
|
|
232
232
|
}
|
|
233
233
|
catch {
|
|
234
234
|
// file not present
|
|
235
235
|
}
|
|
236
|
+
// Add deprecation warning if mcp.json exists (regardless of validity)
|
|
237
|
+
if (mcpJsonExists) {
|
|
238
|
+
meta.mcpFile = mcpFile;
|
|
239
|
+
diagnostics.push({
|
|
240
|
+
severity: 'warning',
|
|
241
|
+
code: 'MCP_JSON_DEPRECATED',
|
|
242
|
+
message: 'mcp.json detected: please migrate MCP servers to ruler.toml [mcp_servers.*] sections',
|
|
243
|
+
file: mcpFile,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
236
246
|
try {
|
|
237
247
|
if (mcpJsonExists) {
|
|
238
248
|
const raw = await fs_1.promises.readFile(mcpFile, 'utf8');
|
|
@@ -256,14 +266,6 @@ async function loadUnifiedConfig(options) {
|
|
|
256
266
|
throw e; // rethrow original error for diagnostics
|
|
257
267
|
}
|
|
258
268
|
}
|
|
259
|
-
meta.mcpFile = mcpFile;
|
|
260
|
-
// Add deprecation warning if mcp.json exists (structured diagnostic)
|
|
261
|
-
diagnostics.push({
|
|
262
|
-
severity: 'warning',
|
|
263
|
-
code: 'MCP_JSON_DEPRECATED',
|
|
264
|
-
message: 'mcp.json detected: please migrate MCP servers to ruler.toml [mcp_servers.*] sections',
|
|
265
|
-
file: mcpFile,
|
|
266
|
-
});
|
|
267
269
|
const parsedObj = parsed;
|
|
268
270
|
const serversRaw = parsedObj.mcpServers ||
|
|
269
271
|
parsedObj.servers ||
|
|
@@ -35,7 +35,6 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.loadNestedConfigurations = loadNestedConfigurations;
|
|
37
37
|
exports.loadSingleConfiguration = loadSingleConfiguration;
|
|
38
|
-
exports.selectAgentsToRun = selectAgentsToRun;
|
|
39
38
|
exports.processHierarchicalConfigurations = processHierarchicalConfigurations;
|
|
40
39
|
exports.processSingleConfiguration = processSingleConfiguration;
|
|
41
40
|
exports.applyConfigurationsToAgents = applyConfigurationsToAgents;
|
|
@@ -161,52 +160,6 @@ async function loadSingleConfiguration(projectRoot, configPath, localOnly) {
|
|
|
161
160
|
rulerMcpJson,
|
|
162
161
|
};
|
|
163
162
|
}
|
|
164
|
-
/**
|
|
165
|
-
* Selects the agents to process based on configuration.
|
|
166
|
-
* @param allAgents Array of all available agents
|
|
167
|
-
* @param config Loaded configuration
|
|
168
|
-
* @returns Array of agents to be processed
|
|
169
|
-
*/
|
|
170
|
-
function selectAgentsToRun(allAgents, config) {
|
|
171
|
-
// CLI --agents > config.default_agents > per-agent.enabled flags > default all
|
|
172
|
-
let selected = allAgents;
|
|
173
|
-
if (config.cliAgents && config.cliAgents.length > 0) {
|
|
174
|
-
const filters = config.cliAgents.map((n) => n.toLowerCase());
|
|
175
|
-
// Check if any of the specified agents don't exist
|
|
176
|
-
const validAgentIdentifiers = new Set(allAgents.map((agent) => agent.getIdentifier()));
|
|
177
|
-
const validAgentNames = new Set(allAgents.map((agent) => agent.getName().toLowerCase()));
|
|
178
|
-
const invalidAgents = filters.filter((filter) => !validAgentIdentifiers.has(filter) &&
|
|
179
|
-
![...validAgentNames].some((name) => name.includes(filter)));
|
|
180
|
-
if (invalidAgents.length > 0) {
|
|
181
|
-
throw (0, constants_1.createRulerError)(`Invalid agent specified: ${invalidAgents.join(', ')}`, `Valid agents are: ${[...validAgentIdentifiers].join(', ')}`);
|
|
182
|
-
}
|
|
183
|
-
selected = allAgents.filter((agent) => filters.some((f) => agent.getIdentifier() === f ||
|
|
184
|
-
agent.getName().toLowerCase().includes(f)));
|
|
185
|
-
}
|
|
186
|
-
else if (config.defaultAgents && config.defaultAgents.length > 0) {
|
|
187
|
-
const defaults = config.defaultAgents.map((n) => n.toLowerCase());
|
|
188
|
-
// Check if any of the default agents don't exist
|
|
189
|
-
const validAgentIdentifiers = new Set(allAgents.map((agent) => agent.getIdentifier()));
|
|
190
|
-
const validAgentNames = new Set(allAgents.map((agent) => agent.getName().toLowerCase()));
|
|
191
|
-
const invalidAgents = defaults.filter((filter) => !validAgentIdentifiers.has(filter) &&
|
|
192
|
-
![...validAgentNames].some((name) => name.includes(filter)));
|
|
193
|
-
if (invalidAgents.length > 0) {
|
|
194
|
-
throw (0, constants_1.createRulerError)(`Invalid agent specified in default_agents: ${invalidAgents.join(', ')}`, `Valid agents are: ${[...validAgentIdentifiers].join(', ')}`);
|
|
195
|
-
}
|
|
196
|
-
selected = allAgents.filter((agent) => {
|
|
197
|
-
const identifier = agent.getIdentifier();
|
|
198
|
-
const override = config.agentConfigs[identifier]?.enabled;
|
|
199
|
-
if (override !== undefined) {
|
|
200
|
-
return override;
|
|
201
|
-
}
|
|
202
|
-
return defaults.some((d) => identifier === d || agent.getName().toLowerCase().includes(d));
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
else {
|
|
206
|
-
selected = allAgents.filter((agent) => config.agentConfigs[agent.getIdentifier()]?.enabled !== false);
|
|
207
|
-
}
|
|
208
|
-
return selected;
|
|
209
|
-
}
|
|
210
163
|
/**
|
|
211
164
|
* Processes hierarchical configurations by applying rules to each .ruler directory independently.
|
|
212
165
|
* Each directory gets its own set of rules and generates its own agent files.
|
|
@@ -385,13 +338,21 @@ async function applyStandardMcpConfiguration(agent, filteredMcpJson, dest, agent
|
|
|
385
338
|
(0, constants_1.logVerbose)(`DRY RUN: Would apply MCP config to: ${dest}`, verbose);
|
|
386
339
|
}
|
|
387
340
|
else {
|
|
388
|
-
if (backup) {
|
|
389
|
-
const { backupFile } = await Promise.resolve().then(() => __importStar(require('../core/FileSystemUtils')));
|
|
390
|
-
await backupFile(dest);
|
|
391
|
-
}
|
|
392
341
|
const existing = await (0, mcp_1.readNativeMcp)(dest);
|
|
393
342
|
const merged = (0, merge_1.mergeMcp)(existing, filteredMcpJson, strategy, serverKey);
|
|
394
|
-
|
|
343
|
+
// Only backup and write if content would actually change (idempotent)
|
|
344
|
+
const currentContent = JSON.stringify(existing, null, 2);
|
|
345
|
+
const newContent = JSON.stringify(merged, null, 2);
|
|
346
|
+
if (currentContent !== newContent) {
|
|
347
|
+
if (backup) {
|
|
348
|
+
const { backupFile } = await Promise.resolve().then(() => __importStar(require('../core/FileSystemUtils')));
|
|
349
|
+
await backupFile(dest);
|
|
350
|
+
}
|
|
351
|
+
await (0, mcp_1.writeNativeMcp)(dest, merged);
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
(0, constants_1.logVerbose)(`MCP config for ${agent.getName()} is already up to date - skipping backup and write`, verbose);
|
|
355
|
+
}
|
|
395
356
|
}
|
|
396
357
|
}
|
|
397
358
|
/**
|
|
@@ -402,7 +363,7 @@ async function applyStandardMcpConfiguration(agent, filteredMcpJson, dest, agent
|
|
|
402
363
|
* @param cliGitignoreEnabled CLI gitignore setting
|
|
403
364
|
* @param dryRun Whether to perform a dry run
|
|
404
365
|
*/
|
|
405
|
-
async function updateGitignore(projectRoot, generatedPaths, config, cliGitignoreEnabled, dryRun
|
|
366
|
+
async function updateGitignore(projectRoot, generatedPaths, config, cliGitignoreEnabled, dryRun) {
|
|
406
367
|
// Configuration precedence: CLI > TOML > Default (enabled)
|
|
407
368
|
let gitignoreEnabled;
|
|
408
369
|
if (cliGitignoreEnabled !== undefined) {
|
|
@@ -416,10 +377,8 @@ async function updateGitignore(projectRoot, generatedPaths, config, cliGitignore
|
|
|
416
377
|
}
|
|
417
378
|
if (gitignoreEnabled && generatedPaths.length > 0) {
|
|
418
379
|
const uniquePaths = [...new Set(generatedPaths)];
|
|
419
|
-
//
|
|
420
|
-
|
|
421
|
-
uniquePaths.push('*.bak');
|
|
422
|
-
}
|
|
380
|
+
// Note: Individual backup patterns are added per-file in the collection phase
|
|
381
|
+
// No need to add a broad *.bak pattern here
|
|
423
382
|
if (uniquePaths.length > 0) {
|
|
424
383
|
const prefix = (0, constants_1.actionPrefix)(dryRun);
|
|
425
384
|
if (dryRun) {
|
package/dist/lib.js
CHANGED
|
@@ -7,6 +7,7 @@ Object.defineProperty(exports, "allAgents", { enumerable: true, get: function ()
|
|
|
7
7
|
const constants_1 = require("./constants");
|
|
8
8
|
const apply_engine_1 = require("./core/apply-engine");
|
|
9
9
|
const config_utils_1 = require("./core/config-utils");
|
|
10
|
+
const agent_selection_1 = require("./core/agent-selection");
|
|
10
11
|
const agents = agents_1.allAgents;
|
|
11
12
|
/**
|
|
12
13
|
* Applies ruler configurations for all supported AI agents.
|
|
@@ -38,7 +39,7 @@ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cli
|
|
|
38
39
|
(0, constants_1.logVerbose)(`Loaded ${hierarchicalConfigs.length} .ruler directory configurations`, verbose);
|
|
39
40
|
(0, constants_1.logVerbose)(`Root configuration has ${Object.keys(rootConfig.agentConfigs).length} agent configs`, verbose);
|
|
40
41
|
normalizeAgentConfigs(rootConfig, agents);
|
|
41
|
-
selectedAgents = (0,
|
|
42
|
+
selectedAgents = (0, agent_selection_1.resolveSelectedAgents)(rootConfig, agents);
|
|
42
43
|
(0, constants_1.logVerbose)(`Selected ${selectedAgents.length} agents: ${selectedAgents.map((a) => a.getName()).join(', ')}`, verbose);
|
|
43
44
|
generatedPaths = await (0, apply_engine_1.processHierarchicalConfigurations)(selectedAgents, hierarchicalConfigs, verbose, dryRun, cliMcpEnabled, cliMcpStrategy, backup);
|
|
44
45
|
}
|
|
@@ -49,11 +50,11 @@ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cli
|
|
|
49
50
|
(0, constants_1.logVerbose)(`Loaded configuration with ${Object.keys(singleConfig.config.agentConfigs).length} agent configs`, verbose);
|
|
50
51
|
(0, constants_1.logVerbose)(`Found .ruler directory with ${singleConfig.concatenatedRules.length} characters of rules`, verbose);
|
|
51
52
|
normalizeAgentConfigs(singleConfig.config, agents);
|
|
52
|
-
selectedAgents = (0,
|
|
53
|
+
selectedAgents = (0, agent_selection_1.resolveSelectedAgents)(singleConfig.config, agents);
|
|
53
54
|
(0, constants_1.logVerbose)(`Selected ${selectedAgents.length} agents: ${selectedAgents.map((a) => a.getName()).join(', ')}`, verbose);
|
|
54
55
|
generatedPaths = await (0, apply_engine_1.processSingleConfiguration)(selectedAgents, singleConfig, projectRoot, verbose, dryRun, cliMcpEnabled, cliMcpStrategy, backup);
|
|
55
56
|
}
|
|
56
|
-
await (0, apply_engine_1.updateGitignore)(projectRoot, generatedPaths, loadedConfig, cliGitignoreEnabled, dryRun
|
|
57
|
+
await (0, apply_engine_1.updateGitignore)(projectRoot, generatedPaths, loadedConfig, cliGitignoreEnabled, dryRun);
|
|
57
58
|
}
|
|
58
59
|
/**
|
|
59
60
|
* Normalizes per-agent config keys to agent identifiers for consistent lookup.
|
|
@@ -35,7 +35,6 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.propagateMcpToOpenHands = propagateMcpToOpenHands;
|
|
37
37
|
const fs = __importStar(require("fs/promises"));
|
|
38
|
-
const TOML = __importStar(require("toml"));
|
|
39
38
|
const toml_1 = require("@iarna/toml");
|
|
40
39
|
const FileSystemUtils_1 = require("../core/FileSystemUtils");
|
|
41
40
|
const path = __importStar(require("path"));
|
|
@@ -103,7 +102,7 @@ async function propagateMcpToOpenHands(rulerMcpData, openHandsConfigPath, backup
|
|
|
103
102
|
let config = {};
|
|
104
103
|
try {
|
|
105
104
|
const tomlContent = await fs.readFile(openHandsConfigPath, 'utf8');
|
|
106
|
-
config =
|
|
105
|
+
config = (0, toml_1.parse)(tomlContent);
|
|
107
106
|
}
|
|
108
107
|
catch {
|
|
109
108
|
// File doesn't exist, we'll create it.
|
package/dist/paths/mcp.js
CHANGED
|
@@ -36,12 +36,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.getNativeMcpPath = getNativeMcpPath;
|
|
37
37
|
exports.readNativeMcp = readNativeMcp;
|
|
38
38
|
exports.writeNativeMcp = writeNativeMcp;
|
|
39
|
-
const os = __importStar(require("os"));
|
|
40
39
|
const path = __importStar(require("path"));
|
|
41
40
|
const fs_1 = require("fs");
|
|
42
41
|
/** Determine the native MCP config path for a given agent. */
|
|
43
42
|
async function getNativeMcpPath(adapterName, projectRoot) {
|
|
44
|
-
const home = os.homedir();
|
|
45
43
|
const candidates = [];
|
|
46
44
|
switch (adapterName) {
|
|
47
45
|
case 'GitHub Copilot':
|
|
@@ -53,16 +51,15 @@ async function getNativeMcpPath(adapterName, projectRoot) {
|
|
|
53
51
|
break;
|
|
54
52
|
case 'Cursor':
|
|
55
53
|
candidates.push(path.join(projectRoot, '.cursor', 'mcp.json'));
|
|
56
|
-
candidates.push(path.join(home, '.cursor', 'mcp.json'));
|
|
57
54
|
break;
|
|
58
55
|
case 'Windsurf':
|
|
59
|
-
candidates.push(path.join(
|
|
56
|
+
candidates.push(path.join(projectRoot, '.windsurf', 'mcp_config.json'));
|
|
60
57
|
break;
|
|
61
58
|
case 'Claude Code':
|
|
62
59
|
candidates.push(path.join(projectRoot, '.mcp.json'));
|
|
63
60
|
break;
|
|
64
61
|
case 'OpenAI Codex CLI':
|
|
65
|
-
candidates.push(path.join(
|
|
62
|
+
candidates.push(path.join(projectRoot, '.codex', 'config.json'));
|
|
66
63
|
break;
|
|
67
64
|
case 'Aider':
|
|
68
65
|
candidates.push(path.join(projectRoot, '.mcp.json'));
|
|
@@ -82,7 +79,6 @@ async function getNativeMcpPath(adapterName, projectRoot) {
|
|
|
82
79
|
break;
|
|
83
80
|
case 'OpenCode':
|
|
84
81
|
candidates.push(path.join(projectRoot, 'opencode.json'));
|
|
85
|
-
candidates.push(path.join(home, '.config', 'opencode', 'opencode.json'));
|
|
86
82
|
break;
|
|
87
83
|
case 'Zed':
|
|
88
84
|
// Only consider project-local Zed settings (avoid writing to user home directory)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intellectronica/ruler",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
4
4
|
"description": "Ruler — apply the same rules to all coding agents",
|
|
5
5
|
"main": "dist/lib.js",
|
|
6
6
|
"scripts": {
|
|
@@ -62,7 +62,6 @@
|
|
|
62
62
|
"dependencies": {
|
|
63
63
|
"@iarna/toml": "^2.2.5",
|
|
64
64
|
"js-yaml": "^4.1.0",
|
|
65
|
-
"toml": "^3.0.0",
|
|
66
65
|
"yargs": "^17.7.2",
|
|
67
66
|
"zod": "^3.25.28"
|
|
68
67
|
}
|