@intellectronica/ruler 0.1.0 → 0.1.2

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
@@ -35,7 +35,14 @@ Create a `.ruler/` directory at your project root and add Markdown files definin
35
35
  Run the apply command:
36
36
 
37
37
  ```bash
38
- ruler apply [--project-root <path>] [--agents <agent1,agent2,...>]
38
+ ruler apply [--project-root <path>] [--agents <agent1,agent2,...>] [--config <path>]
39
+ ```
40
+
41
+
42
+ Run the init command to scaffold a basic `.ruler/` setup:
43
+
44
+ ```bash
45
+ ruler init [--project-root <path>]
39
46
  ```
40
47
 
41
48
  Use `--agents` to specify a comma-separated list of agent names (case-insensitive substrings) to limit which agents the rules are applied to.
@@ -52,6 +59,82 @@ The command will read all `.md` files under `.ruler/`, concatenate their content
52
59
  | Cline | `.clinerules` |
53
60
  | Aider | `ruler_aider_instructions.md` <br>and updates `.aider.conf.yml` |
54
61
 
62
+ ## Configuration
63
+
64
+ Ruler uses a TOML configuration file located at `.ruler/ruler.toml` by default. You can override its location with the `--config <path>` option in the `apply` command.
65
+
66
+ ### Configuration structure
67
+
68
+ ```toml
69
+ # Run only these agents by default (omit to use all agents)
70
+ # default_agents = ["GitHub Copilot", "Claude Code", "Aider"]
71
+
72
+ [agents.Copilot]
73
+ enabled = true
74
+ output_path = ".github/copilot-instructions.md"
75
+
76
+ [agents.Claude]
77
+ enabled = true
78
+ # output_path = "CLAUDE.md"
79
+
80
+ [agents.Aider]
81
+ enabled = false
82
+ # output_path_instructions = "ruler_aider_instructions.md"
83
+ # output_path_config = ".aider.conf.yml"
84
+ ```
85
+
86
+ - `default_agents`: array of agent names (case-insensitive substrings) to run by default.
87
+ - `[agents.<AgentName>]`: per-agent settings:
88
+ - `enabled` (boolean): enable or disable this agent.
89
+ - `output_path` (string): custom path for agents that produce a single file.
90
+ - `output_path_instructions`/`output_path_config`: custom paths for Aider's instruction and config files.
91
+
92
+ ### Precedence
93
+
94
+ 1. CLI `--agents` option (substring filters)
95
+ 2. Config file `default_agents` and `[agents]` overrides
96
+ 3. Built-in defaults (all agents enabled, standard output paths)
97
+
98
+ ## MCP servers
99
+
100
+ Ruler can propagate a project-level `.ruler/mcp.json` file to native MCP configurations of supported agents, merging (or overwriting) each agent’s existing MCP server settings.
101
+
102
+ ### `.ruler/mcp.json`
103
+
104
+ Place your MCP servers config in a file at `.ruler/mcp.json`:
105
+
106
+ ```json
107
+ {
108
+ "mcpServers": {
109
+ "example": {
110
+ "url": "https://mcp.example.com"
111
+ }
112
+ }
113
+ }
114
+ ```
115
+
116
+ ### CLI flags
117
+
118
+ | Flag | Effect |
119
+ |-------------------|--------------------------------------------------------------|
120
+ | `--with-mcp` | Enable writing MCP configs for all agents (default) |
121
+ | `--no-mcp` | Disable writing MCP configs |
122
+ | `--mcp-overwrite` | Overwrite native MCP configs instead of merging |
123
+
124
+ ### Configuration (`ruler.toml`)
125
+
126
+ Configure default behavior in your `ruler.toml`:
127
+
128
+ ```toml
129
+ [mcp]
130
+ enabled = true
131
+ merge_strategy = "merge" # or "overwrite"
132
+
133
+ [agents.Cursor.mcp]
134
+ enabled = false
135
+ merge_strategy = "overwrite"
136
+ ```
137
+
55
138
  ## Development
56
139
 
57
140
  Clone the repository and install dependencies:
@@ -45,11 +45,13 @@ class AiderAgent {
45
45
  getName() {
46
46
  return 'Aider';
47
47
  }
48
- async applyRulerConfig(concatenatedRules, projectRoot) {
49
- const mdFile = path.join(projectRoot, 'ruler_aider_instructions.md');
50
- await (0, FileSystemUtils_1.backupFile)(mdFile);
51
- await (0, FileSystemUtils_1.writeGeneratedFile)(mdFile, concatenatedRules);
52
- const cfgPath = path.join(projectRoot, '.aider.conf.yml');
48
+ async applyRulerConfig(concatenatedRules, projectRoot, agentConfig) {
49
+ const mdPath = agentConfig?.outputPathInstructions ??
50
+ this.getDefaultOutputPath(projectRoot).instructions;
51
+ await (0, FileSystemUtils_1.backupFile)(mdPath);
52
+ await (0, FileSystemUtils_1.writeGeneratedFile)(mdPath, concatenatedRules);
53
+ const cfgPath = agentConfig?.outputPathConfig ??
54
+ this.getDefaultOutputPath(projectRoot).config;
53
55
  let doc = {};
54
56
  try {
55
57
  await fs.access(cfgPath);
@@ -63,11 +65,18 @@ class AiderAgent {
63
65
  if (!Array.isArray(doc.read)) {
64
66
  doc.read = [];
65
67
  }
66
- if (!doc.read.includes('ruler_aider_instructions.md')) {
67
- doc.read.push('ruler_aider_instructions.md');
68
+ const name = path.basename(mdPath);
69
+ if (!doc.read.includes(name)) {
70
+ doc.read.push(name);
68
71
  }
69
72
  const yamlStr = yaml.dump(doc);
70
73
  await (0, FileSystemUtils_1.writeGeneratedFile)(cfgPath, yamlStr);
71
74
  }
75
+ getDefaultOutputPath(projectRoot) {
76
+ return {
77
+ instructions: path.join(projectRoot, 'ruler_aider_instructions.md'),
78
+ config: path.join(projectRoot, '.aider.conf.yml'),
79
+ };
80
+ }
72
81
  }
73
82
  exports.AiderAgent = AiderAgent;
@@ -43,10 +43,13 @@ class ClaudeAgent {
43
43
  getName() {
44
44
  return 'Claude Code';
45
45
  }
46
- async applyRulerConfig(concatenatedRules, projectRoot) {
47
- const target = path.join(projectRoot, 'CLAUDE.md');
48
- await (0, FileSystemUtils_1.backupFile)(target);
49
- await (0, FileSystemUtils_1.writeGeneratedFile)(target, concatenatedRules);
46
+ async applyRulerConfig(concatenatedRules, projectRoot, agentConfig) {
47
+ const output = agentConfig?.outputPath ?? this.getDefaultOutputPath(projectRoot);
48
+ await (0, FileSystemUtils_1.backupFile)(output);
49
+ await (0, FileSystemUtils_1.writeGeneratedFile)(output, concatenatedRules);
50
+ }
51
+ getDefaultOutputPath(projectRoot) {
52
+ return path.join(projectRoot, 'CLAUDE.md');
50
53
  }
51
54
  }
52
55
  exports.ClaudeAgent = ClaudeAgent;
@@ -43,10 +43,13 @@ class ClineAgent {
43
43
  getName() {
44
44
  return 'Cline';
45
45
  }
46
- async applyRulerConfig(concatenatedRules, projectRoot) {
47
- const target = path.join(projectRoot, '.clinerules');
48
- await (0, FileSystemUtils_1.backupFile)(target);
49
- await (0, FileSystemUtils_1.writeGeneratedFile)(target, concatenatedRules);
46
+ async applyRulerConfig(concatenatedRules, projectRoot, agentConfig) {
47
+ const output = agentConfig?.outputPath ?? this.getDefaultOutputPath(projectRoot);
48
+ await (0, FileSystemUtils_1.backupFile)(output);
49
+ await (0, FileSystemUtils_1.writeGeneratedFile)(output, concatenatedRules);
50
+ }
51
+ getDefaultOutputPath(projectRoot) {
52
+ return path.join(projectRoot, '.clinerules');
50
53
  }
51
54
  }
52
55
  exports.ClineAgent = ClineAgent;
@@ -43,10 +43,13 @@ class CodexCliAgent {
43
43
  getName() {
44
44
  return 'OpenAI Codex CLI';
45
45
  }
46
- async applyRulerConfig(concatenatedRules, projectRoot) {
47
- const target = path.join(projectRoot, 'AGENTS.md');
48
- await (0, FileSystemUtils_1.backupFile)(target);
49
- await (0, FileSystemUtils_1.writeGeneratedFile)(target, concatenatedRules);
46
+ async applyRulerConfig(concatenatedRules, projectRoot, agentConfig) {
47
+ const output = agentConfig?.outputPath ?? this.getDefaultOutputPath(projectRoot);
48
+ await (0, FileSystemUtils_1.backupFile)(output);
49
+ await (0, FileSystemUtils_1.writeGeneratedFile)(output, concatenatedRules);
50
+ }
51
+ getDefaultOutputPath(projectRoot) {
52
+ return path.join(projectRoot, 'AGENTS.md');
50
53
  }
51
54
  }
52
55
  exports.CodexCliAgent = CodexCliAgent;
@@ -43,12 +43,14 @@ class CopilotAgent {
43
43
  getName() {
44
44
  return 'GitHub Copilot';
45
45
  }
46
- async applyRulerConfig(concatenatedRules, projectRoot) {
47
- const targetDir = path.join(projectRoot, '.github');
48
- await (0, FileSystemUtils_1.ensureDirExists)(targetDir);
49
- const target = path.join(targetDir, 'copilot-instructions.md');
50
- await (0, FileSystemUtils_1.backupFile)(target);
51
- await (0, FileSystemUtils_1.writeGeneratedFile)(target, concatenatedRules);
46
+ async applyRulerConfig(concatenatedRules, projectRoot, agentConfig) {
47
+ const output = agentConfig?.outputPath ?? this.getDefaultOutputPath(projectRoot);
48
+ await (0, FileSystemUtils_1.ensureDirExists)(path.dirname(output));
49
+ await (0, FileSystemUtils_1.backupFile)(output);
50
+ await (0, FileSystemUtils_1.writeGeneratedFile)(output, concatenatedRules);
51
+ }
52
+ getDefaultOutputPath(projectRoot) {
53
+ return path.join(projectRoot, '.github', 'copilot-instructions.md');
52
54
  }
53
55
  }
54
56
  exports.CopilotAgent = CopilotAgent;
@@ -43,12 +43,14 @@ class CursorAgent {
43
43
  getName() {
44
44
  return 'Cursor';
45
45
  }
46
- async applyRulerConfig(concatenatedRules, projectRoot) {
47
- const targetDir = path.join(projectRoot, '.cursor', 'rules');
48
- await (0, FileSystemUtils_1.ensureDirExists)(targetDir);
49
- const target = path.join(targetDir, 'ruler_cursor_instructions.md');
50
- await (0, FileSystemUtils_1.backupFile)(target);
51
- await (0, FileSystemUtils_1.writeGeneratedFile)(target, concatenatedRules);
46
+ async applyRulerConfig(concatenatedRules, projectRoot, agentConfig) {
47
+ const output = agentConfig?.outputPath ?? this.getDefaultOutputPath(projectRoot);
48
+ await (0, FileSystemUtils_1.ensureDirExists)(path.dirname(output));
49
+ await (0, FileSystemUtils_1.backupFile)(output);
50
+ await (0, FileSystemUtils_1.writeGeneratedFile)(output, concatenatedRules);
51
+ }
52
+ getDefaultOutputPath(projectRoot) {
53
+ return path.join(projectRoot, '.cursor', 'rules', 'ruler_cursor_instructions.md');
52
54
  }
53
55
  }
54
56
  exports.CursorAgent = CursorAgent;
@@ -43,12 +43,14 @@ class WindsurfAgent {
43
43
  getName() {
44
44
  return 'Windsurf';
45
45
  }
46
- async applyRulerConfig(concatenatedRules, projectRoot) {
47
- const targetDir = path.join(projectRoot, '.windsurf', 'rules');
48
- await (0, FileSystemUtils_1.ensureDirExists)(targetDir);
49
- const target = path.join(targetDir, 'ruler_windsurf_instructions.md');
50
- await (0, FileSystemUtils_1.backupFile)(target);
51
- await (0, FileSystemUtils_1.writeGeneratedFile)(target, concatenatedRules);
46
+ async applyRulerConfig(concatenatedRules, projectRoot, agentConfig) {
47
+ const output = agentConfig?.outputPath ?? this.getDefaultOutputPath(projectRoot);
48
+ await (0, FileSystemUtils_1.ensureDirExists)(path.dirname(output));
49
+ await (0, FileSystemUtils_1.backupFile)(output);
50
+ await (0, FileSystemUtils_1.writeGeneratedFile)(output, concatenatedRules);
51
+ }
52
+ getDefaultOutputPath(projectRoot) {
53
+ return path.join(projectRoot, '.windsurf', 'rules', 'ruler_windsurf_instructions.md');
52
54
  }
53
55
  }
54
56
  exports.WindsurfAgent = WindsurfAgent;
@@ -1,4 +1,37 @@
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
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
37
  };
@@ -7,6 +40,8 @@ exports.run = run;
7
40
  const yargs_1 = __importDefault(require("yargs"));
8
41
  const helpers_1 = require("yargs/helpers");
9
42
  const lib_1 = require("../lib");
43
+ const path = __importStar(require("path"));
44
+ const fs_1 = require("fs");
10
45
  /**
11
46
  * Sets up and parses CLI commands.
12
47
  */
@@ -24,13 +59,33 @@ function run() {
24
59
  type: 'string',
25
60
  description: 'Comma-separated list of agent names to include (e.g. "copilot,claude")',
26
61
  });
62
+ y.option('config', {
63
+ type: 'string',
64
+ description: 'Path to TOML configuration file',
65
+ });
66
+ y.option('mcp', {
67
+ type: 'boolean',
68
+ description: 'Enable or disable applying MCP server config',
69
+ default: true,
70
+ });
71
+ y.alias('mcp', 'with-mcp');
72
+ y.option('mcp-overwrite', {
73
+ type: 'boolean',
74
+ description: 'Replace (not merge) the native MCP config(s)',
75
+ default: false,
76
+ });
27
77
  }, async (argv) => {
28
78
  const projectRoot = argv['project-root'];
29
79
  const agents = argv.agents
30
80
  ? argv.agents.split(',').map((a) => a.trim())
31
81
  : undefined;
82
+ const configPath = argv.config;
83
+ const mcpEnabled = argv.mcp;
84
+ const mcpStrategy = argv['mcp-overwrite']
85
+ ? 'overwrite'
86
+ : undefined;
32
87
  try {
33
- await (0, lib_1.applyAllAgentConfigs)(projectRoot, agents);
88
+ await (0, lib_1.applyAllAgentConfigs)(projectRoot, agents, configPath, mcpEnabled, mcpStrategy);
34
89
  console.log('Ruler apply completed successfully.');
35
90
  }
36
91
  catch (err) {
@@ -38,6 +93,104 @@ function run() {
38
93
  console.error('Error applying ruler configurations:', message);
39
94
  process.exit(1);
40
95
  }
96
+ })
97
+ .command('init', 'Scaffold a .ruler directory with default files', (y) => {
98
+ y.option('project-root', {
99
+ type: 'string',
100
+ description: 'Project root directory',
101
+ default: process.cwd(),
102
+ });
103
+ }, async (argv) => {
104
+ const projectRoot = argv['project-root'];
105
+ const rulerDir = path.join(projectRoot, '.ruler');
106
+ await fs_1.promises.mkdir(rulerDir, { recursive: true });
107
+ const instructionsPath = path.join(rulerDir, 'instructions.md');
108
+ const tomlPath = path.join(rulerDir, 'ruler.toml');
109
+ const exists = async (p) => {
110
+ try {
111
+ await fs_1.promises.access(p);
112
+ return true;
113
+ }
114
+ catch {
115
+ return false;
116
+ }
117
+ };
118
+ const DEFAULT_INSTRUCTIONS = `# Ruler Instructions
119
+
120
+ These are your centralised AI agent instructions.
121
+ Add your coding guidelines, style guides, and other project-specific context here.
122
+
123
+ Ruler will concatenate all .md files in this directory (and its subdirectories)
124
+ and apply them to your configured AI coding agents.
125
+ `;
126
+ const DEFAULT_TOML = `# Ruler Configuration File
127
+ # See https://ai.intellectronica.net/ruler for documentation.
128
+
129
+ # To specify which agents are active by default when --agents is not used,
130
+ # uncomment and populate the following line. If omitted, all agents are active.
131
+ # default_agents = ["Copilot", "Claude"]
132
+
133
+ # --- Agent Specific Configurations ---
134
+ # You can enable/disable agents and override their default output paths here.
135
+
136
+ # [agents.GitHubCopilot]
137
+ # enabled = true
138
+ # output_path = ".github/copilot-instructions.md"
139
+
140
+ # [agents.ClaudeCode]
141
+ # enabled = true
142
+ # output_path = "CLAUDE.md"
143
+
144
+ # [agents.OpenAICodexCLI]
145
+ # enabled = true
146
+ # output_path = "AGENTS.md"
147
+
148
+ # [agents.Cursor]
149
+ # enabled = true
150
+ # output_path = ".cursor/rules/ruler_cursor_instructions.md"
151
+
152
+ # [agents.Windsurf]
153
+ # enabled = true
154
+ # output_path = ".windsurf/rules/ruler_windsurf_instructions.md"
155
+
156
+ # [agents.Cline]
157
+ # enabled = true
158
+ # output_path = ".clinerules"
159
+
160
+ # [agents.Aider]
161
+ # enabled = true
162
+ # output_path_instructions = "ruler_aider_instructions.md"
163
+ # output_path_config = ".aider.conf.yml"
164
+ `;
165
+ if (!(await exists(instructionsPath))) {
166
+ await fs_1.promises.writeFile(instructionsPath, DEFAULT_INSTRUCTIONS);
167
+ console.log(`[ruler] Created ${instructionsPath}`);
168
+ }
169
+ else {
170
+ console.log(`[ruler] instructions.md already exists, skipping`);
171
+ }
172
+ if (!(await exists(tomlPath))) {
173
+ await fs_1.promises.writeFile(tomlPath, DEFAULT_TOML);
174
+ console.log(`[ruler] Created ${tomlPath}`);
175
+ }
176
+ else {
177
+ console.log(`[ruler] ruler.toml already exists, skipping`);
178
+ }
179
+ const mcpPath = path.join(rulerDir, 'mcp.json');
180
+ const DEFAULT_MCP_JSON = `{
181
+ "mcpServers": {
182
+ "example": {
183
+ "url": "https://mcp.example.com"
184
+ }
185
+ }
186
+ }`;
187
+ if (!(await exists(mcpPath))) {
188
+ await fs_1.promises.writeFile(mcpPath, DEFAULT_MCP_JSON);
189
+ console.log(`[ruler] Created ${mcpPath}`);
190
+ }
191
+ else {
192
+ console.log(`[ruler] mcp.json already exists, skipping`);
193
+ }
41
194
  })
42
195
  .demandCommand(1, 'You need to specify a command')
43
196
  .help()
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.loadConfig = loadConfig;
40
+ const fs_1 = require("fs");
41
+ const path = __importStar(require("path"));
42
+ const toml_1 = __importDefault(require("toml"));
43
+ /**
44
+ * Loads and parses the ruler TOML configuration file, applying defaults.
45
+ * If the file is missing or invalid, returns empty/default config.
46
+ */
47
+ async function loadConfig(options) {
48
+ const { projectRoot, configPath, cliAgents } = options;
49
+ const configFile = configPath
50
+ ? path.resolve(configPath)
51
+ : path.join(projectRoot, '.ruler', 'ruler.toml');
52
+ let raw = {};
53
+ try {
54
+ const text = await fs_1.promises.readFile(configFile, 'utf8');
55
+ raw = text.trim() ? toml_1.default.parse(text) : {};
56
+ }
57
+ catch (err) {
58
+ if (err instanceof Error && err.code !== 'ENOENT') {
59
+ console.warn(`[ruler] Warning: could not read config file at ${configFile}: ${err.message}`);
60
+ }
61
+ raw = {};
62
+ }
63
+ const defaultAgents = Array.isArray(raw.default_agents)
64
+ ? raw.default_agents.map((a) => String(a))
65
+ : undefined;
66
+ const agentsSection = raw.agents && typeof raw.agents === 'object' && !Array.isArray(raw.agents)
67
+ ? raw.agents
68
+ : {};
69
+ const agentConfigs = {};
70
+ for (const [name, section] of Object.entries(agentsSection)) {
71
+ if (section && typeof section === 'object') {
72
+ const sectionObj = section;
73
+ const cfg = {};
74
+ if (typeof sectionObj.enabled === 'boolean') {
75
+ cfg.enabled = sectionObj.enabled;
76
+ }
77
+ if (typeof sectionObj.output_path === 'string') {
78
+ cfg.outputPath = path.resolve(projectRoot, sectionObj.output_path);
79
+ }
80
+ if (typeof sectionObj.output_path_instructions === 'string') {
81
+ cfg.outputPathInstructions = path.resolve(projectRoot, sectionObj.output_path_instructions);
82
+ }
83
+ if (typeof sectionObj.output_path_config === 'string') {
84
+ cfg.outputPathConfig = path.resolve(projectRoot, sectionObj.output_path_config);
85
+ }
86
+ if (sectionObj.mcp && typeof sectionObj.mcp === 'object') {
87
+ const m = sectionObj.mcp;
88
+ const mcpCfg = {};
89
+ if (typeof m.enabled === 'boolean') {
90
+ mcpCfg.enabled = m.enabled;
91
+ }
92
+ if (typeof m.merge_strategy === 'string') {
93
+ const ms = m.merge_strategy;
94
+ if (ms === 'merge' || ms === 'overwrite') {
95
+ mcpCfg.strategy = ms;
96
+ }
97
+ }
98
+ cfg.mcp = mcpCfg;
99
+ }
100
+ agentConfigs[name] = cfg;
101
+ }
102
+ }
103
+ const rawMcpSection = raw.mcp && typeof raw.mcp === 'object' && !Array.isArray(raw.mcp)
104
+ ? raw.mcp
105
+ : {};
106
+ const globalMcpConfig = {};
107
+ if (typeof rawMcpSection.enabled === 'boolean') {
108
+ globalMcpConfig.enabled = rawMcpSection.enabled;
109
+ }
110
+ if (typeof rawMcpSection.merge_strategy === 'string') {
111
+ const strat = rawMcpSection.merge_strategy;
112
+ if (strat === 'merge' || strat === 'overwrite') {
113
+ globalMcpConfig.strategy = strat;
114
+ }
115
+ }
116
+ return { defaultAgents, agentConfigs, cliAgents, mcp: globalMcpConfig };
117
+ }
package/dist/lib.js CHANGED
@@ -35,8 +35,10 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.applyAllAgentConfigs = applyAllAgentConfigs;
37
37
  const path = __importStar(require("path"));
38
- const FileSystemUtils_1 = require("./core/FileSystemUtils");
38
+ const fs_1 = require("fs");
39
+ const FileSystemUtils = __importStar(require("./core/FileSystemUtils"));
39
40
  const RuleProcessor_1 = require("./core/RuleProcessor");
41
+ const ConfigLoader_1 = require("./core/ConfigLoader");
40
42
  const CopilotAgent_1 = require("./agents/CopilotAgent");
41
43
  const ClaudeAgent_1 = require("./agents/ClaudeAgent");
42
44
  const CodexCliAgent_1 = require("./agents/CodexCliAgent");
@@ -44,6 +46,9 @@ const CursorAgent_1 = require("./agents/CursorAgent");
44
46
  const WindsurfAgent_1 = require("./agents/WindsurfAgent");
45
47
  const ClineAgent_1 = require("./agents/ClineAgent");
46
48
  const AiderAgent_1 = require("./agents/AiderAgent");
49
+ const merge_1 = require("./mcp/merge");
50
+ const validate_1 = require("./mcp/validate");
51
+ const mcp_1 = require("./paths/mcp");
47
52
  const agents = [
48
53
  new CopilotAgent_1.CopilotAgent(),
49
54
  new ClaudeAgent_1.ClaudeAgent(),
@@ -62,21 +67,80 @@ const agents = [
62
67
  * @param projectRoot Root directory of the project
63
68
  * @param includedAgents Optional list of agent name filters (case-insensitive substrings)
64
69
  */
65
- async function applyAllAgentConfigs(projectRoot, includedAgents) {
66
- const rulerDir = await (0, FileSystemUtils_1.findRulerDir)(projectRoot);
70
+ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cliMcpEnabled = true, cliMcpStrategy) {
71
+ // Load configuration (default_agents, per-agent overrides, CLI filters)
72
+ const config = await (0, ConfigLoader_1.loadConfig)({
73
+ projectRoot,
74
+ cliAgents: includedAgents,
75
+ configPath,
76
+ });
77
+ // Normalize per-agent config keys to actual agent names (substring match)
78
+ const rawConfigs = config.agentConfigs;
79
+ const mappedConfigs = {};
80
+ for (const [key, cfg] of Object.entries(rawConfigs)) {
81
+ const lowerKey = key.toLowerCase();
82
+ for (const agent of agents) {
83
+ if (agent.getName().toLowerCase().includes(lowerKey)) {
84
+ mappedConfigs[agent.getName()] = cfg;
85
+ }
86
+ }
87
+ }
88
+ config.agentConfigs = mappedConfigs;
89
+ const rulerDir = await FileSystemUtils.findRulerDir(projectRoot);
67
90
  if (!rulerDir) {
68
91
  throw new Error(`.ruler directory not found from ${projectRoot}`);
69
92
  }
70
- await (0, FileSystemUtils_1.ensureDirExists)(path.join(rulerDir, 'generated'));
71
- const files = await (0, FileSystemUtils_1.readMarkdownFiles)(rulerDir);
93
+ await FileSystemUtils.ensureDirExists(path.join(rulerDir, 'generated'));
94
+ const files = await FileSystemUtils.readMarkdownFiles(rulerDir);
72
95
  const concatenated = (0, RuleProcessor_1.concatenateRules)(files);
96
+ const mcpFile = path.join(rulerDir, 'mcp.json');
97
+ let rulerMcpJson = null;
98
+ try {
99
+ const raw = await fs_1.promises.readFile(mcpFile, 'utf8');
100
+ rulerMcpJson = JSON.parse(raw);
101
+ (0, validate_1.validateMcp)(rulerMcpJson);
102
+ }
103
+ catch (err) {
104
+ if (err.code !== 'ENOENT') {
105
+ throw err;
106
+ }
107
+ }
108
+ // Determine which agents to run:
109
+ // CLI --agents > config.default_agents > per-agent.enabled flags > default all
73
110
  let selected = agents;
74
- if (includedAgents && includedAgents.length > 0) {
75
- const filters = includedAgents.map((n) => n.toLowerCase());
111
+ if (config.cliAgents && config.cliAgents.length > 0) {
112
+ const filters = config.cliAgents.map((n) => n.toLowerCase());
76
113
  selected = agents.filter((agent) => filters.some((f) => agent.getName().toLowerCase().includes(f)));
77
114
  }
115
+ else if (config.defaultAgents && config.defaultAgents.length > 0) {
116
+ const defaults = config.defaultAgents.map((n) => n.toLowerCase());
117
+ selected = agents.filter((agent) => {
118
+ const key = agent.getName();
119
+ const override = config.agentConfigs[key]?.enabled;
120
+ if (override !== undefined) {
121
+ return override;
122
+ }
123
+ return defaults.includes(key.toLowerCase());
124
+ });
125
+ }
126
+ else {
127
+ selected = agents.filter((agent) => config.agentConfigs[agent.getName()]?.enabled !== false);
128
+ }
78
129
  for (const agent of selected) {
79
130
  console.log(`[ruler] Applying rules for ${agent.getName()}...`);
80
- await agent.applyRulerConfig(concatenated, projectRoot);
131
+ const agentConfig = config.agentConfigs[agent.getName()];
132
+ await agent.applyRulerConfig(concatenated, projectRoot, agentConfig);
133
+ const dest = await (0, mcp_1.getNativeMcpPath)(agent.getName(), projectRoot);
134
+ const enabled = cliMcpEnabled &&
135
+ (agentConfig?.mcp?.enabled ?? config.mcp?.enabled ?? true);
136
+ if (dest && rulerMcpJson != null && enabled) {
137
+ const strategy = cliMcpStrategy ??
138
+ agentConfig?.mcp?.strategy ??
139
+ config.mcp?.strategy ??
140
+ 'merge';
141
+ const existing = await (0, mcp_1.readNativeMcp)(dest);
142
+ const merged = (0, merge_1.mergeMcp)(existing, rulerMcpJson, strategy);
143
+ await (0, mcp_1.writeNativeMcp)(dest, merged);
144
+ }
81
145
  }
82
146
  }
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.mergeMcp = mergeMcp;
4
+ /**
5
+ * Merge native and incoming MCP server configurations according to strategy.
6
+ * @param base Existing native MCP config object.
7
+ * @param incoming Ruler MCP config object.
8
+ * @param strategy Merge strategy: 'merge' to union servers, 'overwrite' to replace.
9
+ * @returns Merged MCP config object.
10
+ */
11
+ function mergeMcp(base, incoming, strategy) {
12
+ if (strategy === 'overwrite') {
13
+ return incoming;
14
+ }
15
+ const baseServers = base.mcpServers || {};
16
+ const incomingServers = incoming.mcpServers || {};
17
+ return {
18
+ ...base,
19
+ mcpServers: { ...baseServers, ...incomingServers },
20
+ };
21
+ }
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validateMcp = validateMcp;
4
+ /**
5
+ * Validate the structure of the Ruler MCP JSON config.
6
+ * Minimal validation: ensure 'mcpServers' property exists and is an object.
7
+ * @param data Parsed JSON object from .ruler/mcp.json.
8
+ * @throws Error if validation fails.
9
+ */
10
+ function validateMcp(data) {
11
+ if (!data ||
12
+ typeof data !== 'object' ||
13
+ !('mcpServers' in data) ||
14
+ typeof data.mcpServers !== 'object') {
15
+ throw new Error('[ruler] Invalid .ruler/mcp.json: must contain an object property "mcpServers"');
16
+ }
17
+ }
@@ -0,0 +1,100 @@
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.getNativeMcpPath = getNativeMcpPath;
37
+ exports.readNativeMcp = readNativeMcp;
38
+ exports.writeNativeMcp = writeNativeMcp;
39
+ const os = __importStar(require("os"));
40
+ const path = __importStar(require("path"));
41
+ const fs_1 = require("fs");
42
+ /** Determine the native MCP config path for a given agent. */
43
+ async function getNativeMcpPath(adapterName, projectRoot) {
44
+ const home = os.homedir();
45
+ const candidates = [];
46
+ switch (adapterName) {
47
+ case 'GitHub Copilot':
48
+ candidates.push(path.join(projectRoot, '.vscode', 'mcp.json'));
49
+ break;
50
+ case 'Visual Studio':
51
+ candidates.push(path.join(projectRoot, '.mcp.json'));
52
+ candidates.push(path.join(projectRoot, '.vs', 'mcp.json'));
53
+ break;
54
+ case 'Cursor':
55
+ candidates.push(path.join(projectRoot, '.cursor', 'mcp.json'));
56
+ candidates.push(path.join(home, '.cursor', 'mcp.json'));
57
+ break;
58
+ case 'Windsurf':
59
+ candidates.push(path.join(home, '.codeium', 'windsurf', 'mcp_config.json'));
60
+ break;
61
+ case 'Claude Code':
62
+ candidates.push(path.join(projectRoot, 'claude_desktop_config.json'));
63
+ break;
64
+ case 'OpenAI Codex CLI':
65
+ candidates.push(path.join(home, '.codex', 'config.json'));
66
+ break;
67
+ case 'Aider':
68
+ candidates.push(path.join(projectRoot, '.mcp.json'));
69
+ break;
70
+ default:
71
+ return null;
72
+ }
73
+ for (const p of candidates) {
74
+ try {
75
+ await fs_1.promises.access(p);
76
+ return p;
77
+ }
78
+ catch {
79
+ // continue
80
+ }
81
+ }
82
+ // default to first candidate if none exist
83
+ return candidates.length > 0 ? candidates[0] : null;
84
+ }
85
+ /** Read native MCP config from disk, or return empty object if missing/invalid. */
86
+ async function readNativeMcp(filePath) {
87
+ try {
88
+ const text = await fs_1.promises.readFile(filePath, 'utf8');
89
+ return JSON.parse(text);
90
+ }
91
+ catch {
92
+ return {};
93
+ }
94
+ }
95
+ /** Write native MCP config to disk, creating parent directories as needed. */
96
+ async function writeNativeMcp(filePath, data) {
97
+ await fs_1.promises.mkdir(path.dirname(filePath), { recursive: true });
98
+ const text = JSON.stringify(data, null, 2) + '\n';
99
+ await fs_1.promises.writeFile(filePath, text, 'utf8');
100
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@intellectronica/ruler",
3
- "version": "0.1.0",
4
- "description": "",
3
+ "version": "0.1.2",
4
+ "description": "Ruler — apply the same rules to all coding agents",
5
5
  "main": "dist/lib.js",
6
6
  "scripts": {
7
7
  "lint": "eslint \"src/**/*.{ts,tsx}\"",
@@ -32,7 +32,7 @@
32
32
  "bugs": {
33
33
  "url": "https://github.com/intellectronica/ruler/issues"
34
34
  },
35
- "homepage": "https://github.com/intellectronica/ruler#readme",
35
+ "homepage": "https://ai.intellectronica.net/ruler",
36
36
  "files": [
37
37
  "dist",
38
38
  "README.md",
@@ -53,10 +53,11 @@
53
53
  "jest": "^29.7.0",
54
54
  "prettier": "^3.5.3",
55
55
  "ts-jest": "^29.3.4",
56
- "typescript": "^5.8.3",
57
- "yargs": "^17.7.2"
56
+ "typescript": "^5.8.3"
58
57
  },
59
58
  "dependencies": {
60
- "js-yaml": "^4.1.0"
59
+ "js-yaml": "^4.1.0",
60
+ "toml": "^3.0.0",
61
+ "yargs": "^17.7.2"
61
62
  }
62
63
  }