@intellectronica/ruler 0.2.13 → 0.2.15
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 +18 -1
- package/dist/agents/CodexCliAgent.js +104 -7
- package/dist/core/ConfigLoader.js +2 -5
- package/dist/lib.js +10 -0
- package/dist/mcp/propagateOpenCodeMcp.js +113 -0
- package/dist/mcp/propagateOpenHandsMcp.js +4 -6
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ Ruler solves this by providing a **single source of truth** for all your AI agen
|
|
|
40
40
|
| ---------------- | ------------------------------------------------ | --------------------------------------------------- |
|
|
41
41
|
| GitHub Copilot | `.github/copilot-instructions.md` | `.vscode/mcp.json` |
|
|
42
42
|
| Claude Code | `CLAUDE.md` | `claude_desktop_config.json` |
|
|
43
|
-
| OpenAI Codex CLI | `AGENTS.md` | `~/.codex/config.json`
|
|
43
|
+
| OpenAI Codex CLI | `AGENTS.md` | `.codex/config.toml`, `~/.codex/config.json` |
|
|
44
44
|
| Jules | `AGENTS.md` | - |
|
|
45
45
|
| Cursor | `.cursor/rules/ruler_cursor_instructions.mdc` | `.cursor/mcp.json`, `~/.cursor/mcp.json` |
|
|
46
46
|
| Windsurf | `.windsurf/rules/ruler_windsurf_instructions.md` | `~/.codeium/windsurf/mcp_config.json` |
|
|
@@ -299,6 +299,17 @@ enabled = true
|
|
|
299
299
|
output_path_instructions = "ruler_aider_instructions.md"
|
|
300
300
|
output_path_config = ".aider.conf.yml"
|
|
301
301
|
|
|
302
|
+
# OpenAI Codex CLI agent and MCP config
|
|
303
|
+
[agents.codex]
|
|
304
|
+
enabled = true
|
|
305
|
+
output_path = "AGENTS.md"
|
|
306
|
+
output_path_config = ".codex/config.toml"
|
|
307
|
+
|
|
308
|
+
# Agent-specific MCP configuration for Codex CLI
|
|
309
|
+
[agents.codex.mcp]
|
|
310
|
+
enabled = true
|
|
311
|
+
merge_strategy = "merge"
|
|
312
|
+
|
|
302
313
|
[agents.firebase]
|
|
303
314
|
enabled = true
|
|
304
315
|
output_path = ".idx/airules.md"
|
|
@@ -360,8 +371,14 @@ Define your project's MCP servers:
|
|
|
360
371
|
}
|
|
361
372
|
```
|
|
362
373
|
|
|
374
|
+
|
|
363
375
|
Ruler uses this file with the `merge` (default) or `overwrite` strategy, controlled by `ruler.toml` or CLI flags.
|
|
364
376
|
|
|
377
|
+
**Note for OpenAI Codex CLI:** To apply the local Codex CLI MCP configuration, set the `CODEX_HOME` environment variable to your project’s `.codex` directory:
|
|
378
|
+
```bash
|
|
379
|
+
export CODEX_HOME="$(pwd)/.codex"
|
|
380
|
+
```
|
|
381
|
+
|
|
365
382
|
## `.gitignore` Integration
|
|
366
383
|
|
|
367
384
|
Ruler automatically manages your `.gitignore` file to keep generated agent configuration files out of version control.
|
|
@@ -35,9 +35,13 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.CodexCliAgent = void 0;
|
|
37
37
|
const path = __importStar(require("path"));
|
|
38
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
39
|
+
const fs_1 = require("fs");
|
|
40
|
+
const toml = __importStar(require("toml"));
|
|
41
|
+
const toml_1 = require("@iarna/toml");
|
|
38
42
|
const FileSystemUtils_1 = require("../core/FileSystemUtils");
|
|
39
43
|
/**
|
|
40
|
-
* OpenAI Codex CLI agent adapter
|
|
44
|
+
* OpenAI Codex CLI agent adapter.
|
|
41
45
|
*/
|
|
42
46
|
class CodexCliAgent {
|
|
43
47
|
getIdentifier() {
|
|
@@ -46,14 +50,107 @@ class CodexCliAgent {
|
|
|
46
50
|
getName() {
|
|
47
51
|
return 'OpenAI Codex CLI';
|
|
48
52
|
}
|
|
49
|
-
async applyRulerConfig(concatenatedRules, projectRoot, rulerMcpJson,
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
53
|
+
async applyRulerConfig(concatenatedRules, projectRoot, rulerMcpJson, agentConfig) {
|
|
54
|
+
// Get default paths
|
|
55
|
+
const defaults = this.getDefaultOutputPath(projectRoot);
|
|
56
|
+
// Determine the instructions file path
|
|
57
|
+
const instructionsPath = agentConfig?.outputPath ??
|
|
58
|
+
agentConfig?.outputPathInstructions ??
|
|
59
|
+
defaults.instructions;
|
|
60
|
+
// Write the instructions file
|
|
61
|
+
await (0, FileSystemUtils_1.backupFile)(instructionsPath);
|
|
62
|
+
await (0, FileSystemUtils_1.writeGeneratedFile)(instructionsPath, concatenatedRules);
|
|
63
|
+
// Handle MCP configuration if enabled
|
|
64
|
+
const mcpEnabled = agentConfig?.mcp?.enabled ?? true;
|
|
65
|
+
if (mcpEnabled && rulerMcpJson) {
|
|
66
|
+
// Determine the config file path
|
|
67
|
+
const configPath = agentConfig?.outputPathConfig ?? defaults.config;
|
|
68
|
+
// Ensure the parent directory exists
|
|
69
|
+
await fs_1.promises.mkdir(path.dirname(configPath), { recursive: true });
|
|
70
|
+
// Get the merge strategy
|
|
71
|
+
const strategy = agentConfig?.mcp?.strategy ?? 'merge';
|
|
72
|
+
// Extract MCP servers from ruler config
|
|
73
|
+
const rulerServers = rulerMcpJson.mcpServers || {};
|
|
74
|
+
// Read existing TOML config if it exists
|
|
75
|
+
let existingConfig = {};
|
|
76
|
+
try {
|
|
77
|
+
const existingContent = await fs_1.promises.readFile(configPath, 'utf8');
|
|
78
|
+
existingConfig = toml.parse(existingContent);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// File doesn't exist or can't be parsed, use empty config
|
|
82
|
+
}
|
|
83
|
+
// Create the updated config
|
|
84
|
+
const updatedConfig = { ...existingConfig };
|
|
85
|
+
// Initialize mcp_servers if it doesn't exist
|
|
86
|
+
if (!updatedConfig.mcp_servers) {
|
|
87
|
+
updatedConfig.mcp_servers = {};
|
|
88
|
+
}
|
|
89
|
+
if (strategy === 'overwrite') {
|
|
90
|
+
// For overwrite strategy, replace the entire mcp_servers section
|
|
91
|
+
updatedConfig.mcp_servers = {};
|
|
92
|
+
}
|
|
93
|
+
// Add the ruler servers
|
|
94
|
+
for (const [serverName, serverConfig] of Object.entries(rulerServers)) {
|
|
95
|
+
// Create a properly formatted MCP server entry
|
|
96
|
+
const mcpServer = {
|
|
97
|
+
command: serverConfig.command,
|
|
98
|
+
args: serverConfig.args,
|
|
99
|
+
};
|
|
100
|
+
// Format env as an inline table
|
|
101
|
+
if (serverConfig.env) {
|
|
102
|
+
mcpServer.env = serverConfig.env;
|
|
103
|
+
}
|
|
104
|
+
updatedConfig.mcp_servers[serverName] = mcpServer;
|
|
105
|
+
}
|
|
106
|
+
// Convert to TOML with special handling for env to ensure it's an inline table
|
|
107
|
+
let tomlContent = '';
|
|
108
|
+
// Handle non-mcp_servers sections first
|
|
109
|
+
const configWithoutMcpServers = { ...updatedConfig };
|
|
110
|
+
delete configWithoutMcpServers.mcp_servers;
|
|
111
|
+
if (Object.keys(configWithoutMcpServers).length > 0) {
|
|
112
|
+
tomlContent += (0, toml_1.stringify)(configWithoutMcpServers);
|
|
113
|
+
}
|
|
114
|
+
// Now handle mcp_servers with special formatting for env
|
|
115
|
+
if (updatedConfig.mcp_servers &&
|
|
116
|
+
Object.keys(updatedConfig.mcp_servers).length > 0) {
|
|
117
|
+
for (const [serverName, serverConfigRaw] of Object.entries(updatedConfig.mcp_servers)) {
|
|
118
|
+
const serverConfig = serverConfigRaw;
|
|
119
|
+
tomlContent += `\n[mcp_servers.${serverName}]\n`;
|
|
120
|
+
// Add command
|
|
121
|
+
if (serverConfig.command) {
|
|
122
|
+
tomlContent += `command = "${serverConfig.command}"\n`;
|
|
123
|
+
}
|
|
124
|
+
// Add args if present
|
|
125
|
+
if (serverConfig.args && Array.isArray(serverConfig.args)) {
|
|
126
|
+
const argsStr = JSON.stringify(serverConfig.args)
|
|
127
|
+
.replace(/"/g, '"')
|
|
128
|
+
.replace(/,/g, ', ');
|
|
129
|
+
tomlContent += `args = ${argsStr}\n`;
|
|
130
|
+
}
|
|
131
|
+
// Add env as inline table if present
|
|
132
|
+
if (serverConfig.env && Object.keys(serverConfig.env).length > 0) {
|
|
133
|
+
tomlContent += `env = { `;
|
|
134
|
+
const entries = Object.entries(serverConfig.env);
|
|
135
|
+
for (let i = 0; i < entries.length; i++) {
|
|
136
|
+
const [key, value] = entries[i];
|
|
137
|
+
tomlContent += `${key} = "${value}"`;
|
|
138
|
+
if (i < entries.length - 1) {
|
|
139
|
+
tomlContent += ', ';
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
tomlContent += ` }\n`;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
await (0, FileSystemUtils_1.writeGeneratedFile)(configPath, tomlContent);
|
|
147
|
+
}
|
|
54
148
|
}
|
|
55
149
|
getDefaultOutputPath(projectRoot) {
|
|
56
|
-
return
|
|
150
|
+
return {
|
|
151
|
+
instructions: path.join(projectRoot, 'AGENTS.md'),
|
|
152
|
+
config: path.join(projectRoot, '.codex', 'config.toml'),
|
|
153
|
+
};
|
|
57
154
|
}
|
|
58
155
|
}
|
|
59
156
|
exports.CodexCliAgent = CodexCliAgent;
|
|
@@ -32,15 +32,12 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
32
32
|
return result;
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
-
};
|
|
38
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
36
|
exports.loadConfig = loadConfig;
|
|
40
37
|
const fs_1 = require("fs");
|
|
41
38
|
const path = __importStar(require("path"));
|
|
42
39
|
const os = __importStar(require("os"));
|
|
43
|
-
const
|
|
40
|
+
const TOML = __importStar(require("toml"));
|
|
44
41
|
const zod_1 = require("zod");
|
|
45
42
|
const constants_1 = require("../constants");
|
|
46
43
|
const mcpConfigSchema = zod_1.z
|
|
@@ -99,7 +96,7 @@ async function loadConfig(options) {
|
|
|
99
96
|
let raw = {};
|
|
100
97
|
try {
|
|
101
98
|
const text = await fs_1.promises.readFile(configFile, 'utf8');
|
|
102
|
-
raw = text.trim() ?
|
|
99
|
+
raw = text.trim() ? TOML.parse(text) : {};
|
|
103
100
|
// Validate the configuration with zod
|
|
104
101
|
const validationResult = rulerConfigSchema.safeParse(raw);
|
|
105
102
|
if (!validationResult.success) {
|
package/dist/lib.js
CHANGED
|
@@ -59,6 +59,7 @@ const merge_1 = require("./mcp/merge");
|
|
|
59
59
|
const validate_1 = require("./mcp/validate");
|
|
60
60
|
const mcp_1 = require("./paths/mcp");
|
|
61
61
|
const propagateOpenHandsMcp_1 = require("./mcp/propagateOpenHandsMcp");
|
|
62
|
+
const propagateOpenCodeMcp_1 = require("./mcp/propagateOpenCodeMcp");
|
|
62
63
|
const constants_1 = require("./constants");
|
|
63
64
|
/**
|
|
64
65
|
* Gets all output paths for an agent, taking into account any config overrides.
|
|
@@ -267,6 +268,15 @@ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cli
|
|
|
267
268
|
(0, constants_1.logVerbose)(`DRY RUN: AugmentCode MCP config handled internally via VSCode settings`, verbose);
|
|
268
269
|
}
|
|
269
270
|
}
|
|
271
|
+
else if (agent.getIdentifier() === 'opencode') {
|
|
272
|
+
// *** Special handling for OpenCode ***
|
|
273
|
+
if (dryRun) {
|
|
274
|
+
(0, constants_1.logVerbose)(`DRY RUN: Would apply MCP config by updating OpenCode config file: ${dest}`, verbose);
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
await (0, propagateOpenCodeMcp_1.propagateMcpToOpenCode)(rulerMcpFile, dest);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
270
280
|
else {
|
|
271
281
|
if (rulerMcpJson) {
|
|
272
282
|
const strategy = cliMcpStrategy ??
|
|
@@ -0,0 +1,113 @@
|
|
|
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.propagateMcpToOpenCode = propagateMcpToOpenCode;
|
|
37
|
+
const fs = __importStar(require("fs/promises"));
|
|
38
|
+
const FileSystemUtils_1 = require("../core/FileSystemUtils");
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
/**
|
|
41
|
+
* Transform ruler MCP configuration to OpenCode's specific format
|
|
42
|
+
*/
|
|
43
|
+
function transformToOpenCodeFormat(rulerMcp) {
|
|
44
|
+
const rulerServers = rulerMcp.mcpServers || {};
|
|
45
|
+
const openCodeServers = {};
|
|
46
|
+
for (const [name, serverDef] of Object.entries(rulerServers)) {
|
|
47
|
+
const server = serverDef;
|
|
48
|
+
// Determine if this is a local or remote server
|
|
49
|
+
const isRemote = !!server.url;
|
|
50
|
+
const openCodeServer = {
|
|
51
|
+
type: isRemote ? 'remote' : 'local',
|
|
52
|
+
enabled: true, // Always true as per the issue requirements
|
|
53
|
+
};
|
|
54
|
+
if (isRemote) {
|
|
55
|
+
// Remote server configuration
|
|
56
|
+
openCodeServer.url = server.url;
|
|
57
|
+
if (server.headers) {
|
|
58
|
+
openCodeServer.headers = server.headers;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
// Local server configuration
|
|
63
|
+
if (server.command) {
|
|
64
|
+
// Combine command and args into a single array
|
|
65
|
+
const command = Array.isArray(server.command)
|
|
66
|
+
? server.command
|
|
67
|
+
: [server.command];
|
|
68
|
+
const args = server.args || [];
|
|
69
|
+
openCodeServer.command = [...command, ...args];
|
|
70
|
+
}
|
|
71
|
+
if (server.env) {
|
|
72
|
+
openCodeServer.environment = server.env;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
openCodeServers[name] = openCodeServer;
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
$schema: 'https://opencode.ai/config.json',
|
|
79
|
+
mcp: openCodeServers,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
async function propagateMcpToOpenCode(rulerMcpPath, openCodeConfigPath) {
|
|
83
|
+
let rulerMcp;
|
|
84
|
+
try {
|
|
85
|
+
const rulerJsonContent = await fs.readFile(rulerMcpPath, 'utf8');
|
|
86
|
+
rulerMcp = JSON.parse(rulerJsonContent);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
// Read existing OpenCode config if it exists
|
|
92
|
+
let existingConfig = {};
|
|
93
|
+
try {
|
|
94
|
+
const existingContent = await fs.readFile(openCodeConfigPath, 'utf8');
|
|
95
|
+
existingConfig = JSON.parse(existingContent);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// File doesn't exist, we'll create it
|
|
99
|
+
}
|
|
100
|
+
// Transform ruler MCP to OpenCode format
|
|
101
|
+
const transformedConfig = transformToOpenCodeFormat(rulerMcp);
|
|
102
|
+
// Merge with existing config, preserving non-MCP settings
|
|
103
|
+
const finalConfig = {
|
|
104
|
+
...existingConfig,
|
|
105
|
+
$schema: transformedConfig.$schema,
|
|
106
|
+
mcp: {
|
|
107
|
+
...existingConfig.mcp,
|
|
108
|
+
...transformedConfig.mcp,
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
await (0, FileSystemUtils_1.ensureDirExists)(path.dirname(openCodeConfigPath));
|
|
112
|
+
await fs.writeFile(openCodeConfigPath, JSON.stringify(finalConfig, null, 2) + '\n');
|
|
113
|
+
}
|
|
@@ -32,13 +32,11 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
32
32
|
return result;
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
-
};
|
|
38
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
36
|
exports.propagateMcpToOpenHands = propagateMcpToOpenHands;
|
|
40
37
|
const fs = __importStar(require("fs/promises"));
|
|
41
|
-
const
|
|
38
|
+
const TOML = __importStar(require("toml"));
|
|
39
|
+
const toml_1 = require("@iarna/toml");
|
|
42
40
|
const FileSystemUtils_1 = require("../core/FileSystemUtils");
|
|
43
41
|
const path = __importStar(require("path"));
|
|
44
42
|
async function propagateMcpToOpenHands(rulerMcpPath, openHandsConfigPath) {
|
|
@@ -54,7 +52,7 @@ async function propagateMcpToOpenHands(rulerMcpPath, openHandsConfigPath) {
|
|
|
54
52
|
let config = {};
|
|
55
53
|
try {
|
|
56
54
|
const tomlContent = await fs.readFile(openHandsConfigPath, 'utf8');
|
|
57
|
-
config =
|
|
55
|
+
config = TOML.parse(tomlContent);
|
|
58
56
|
}
|
|
59
57
|
catch {
|
|
60
58
|
// File doesn't exist, we'll create it.
|
|
@@ -79,5 +77,5 @@ async function propagateMcpToOpenHands(rulerMcpPath, openHandsConfigPath) {
|
|
|
79
77
|
}
|
|
80
78
|
config.mcp.stdio_servers = Array.from(existingServers.values());
|
|
81
79
|
await (0, FileSystemUtils_1.ensureDirExists)(path.dirname(openHandsConfigPath));
|
|
82
|
-
await fs.writeFile(openHandsConfigPath, toml_1.
|
|
80
|
+
await fs.writeFile(openHandsConfigPath, (0, toml_1.stringify)(config));
|
|
83
81
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intellectronica/ruler",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.15",
|
|
4
4
|
"description": "Ruler — apply the same rules to all coding agents",
|
|
5
5
|
"main": "dist/lib.js",
|
|
6
6
|
"scripts": {
|
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
"dependencies": {
|
|
62
62
|
"@iarna/toml": "^2.2.5",
|
|
63
63
|
"js-yaml": "^4.1.0",
|
|
64
|
+
"toml": "^3.0.0",
|
|
64
65
|
"yargs": "^17.7.2",
|
|
65
66
|
"zod": "^3.25.28"
|
|
66
67
|
}
|