@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 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 (stub implementation).
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, // eslint-disable-line @typescript-eslint/no-unused-vars
50
- agentConfig) {
51
- const output = agentConfig?.outputPath ?? this.getDefaultOutputPath(projectRoot);
52
- await (0, FileSystemUtils_1.backupFile)(output);
53
- await (0, FileSystemUtils_1.writeGeneratedFile)(output, concatenatedRules);
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 path.join(projectRoot, 'AGENTS.md');
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 toml_1 = __importDefault(require("@iarna/toml"));
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() ? toml_1.default.parse(text) : {};
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 toml_1 = __importDefault(require("@iarna/toml"));
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 = toml_1.default.parse(tomlContent);
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.default.stringify(config));
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.13",
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
  }