@intellectronica/ruler 0.3.5 → 0.3.7

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
@@ -57,18 +57,19 @@ Ruler solves this by providing a **single source of truth** for all your AI agen
57
57
  | Agent | Rules File(s) | MCP Configuration / Notes |
58
58
  | ---------------- | ------------------------------------------------ | ------------------------------------------------ |
59
59
  | AGENTS.md | `AGENTS.md` | (pseudo-agent ensuring root `AGENTS.md` exists) |
60
- | GitHub Copilot | `.github/copilot-instructions.md` | `.vscode/mcp.json` |
60
+ | GitHub Copilot | `AGENTS.md`, `.github/copilot-instructions.md` | `.vscode/mcp.json` |
61
61
  | Claude Code | `CLAUDE.md` | `.mcp.json` |
62
62
  | OpenAI Codex CLI | `AGENTS.md` | `.codex/config.toml` |
63
63
  | Jules | `AGENTS.md` | - |
64
64
  | Cursor | `.cursor/rules/ruler_cursor_instructions.mdc` | `.cursor/mcp.json` |
65
65
  | Windsurf | `.windsurf/rules/ruler_windsurf_instructions.md` | - |
66
66
  | Cline | `.clinerules` | - |
67
+ | Crush | `CRUSH.md` | `.crush.json` |
67
68
  | Amp | `AGENTS.md` | - |
68
69
  | Amazon Q CLI | `.amazonq/rules/ruler_q_rules.md` | `.amazonq/mcp.json` |
69
70
  | Aider | `AGENTS.md`, `.aider.conf.yml` | `.mcp.json` |
70
71
  | Firebase Studio | `.idx/airules.md` | - |
71
- | Open Hands | `.openhands/microagents/repo.md` | `.openhands/config.toml` |
72
+ | Open Hands | `.openhands/microagents/repo.md` | `config.toml` |
72
73
  | Gemini CLI | `AGENTS.md` | `.gemini/settings.json` |
73
74
  | Junie | `.junie/guidelines.md` | - |
74
75
  | AugmentCode | `.augment/rules/ruler_augment_instructions.md` | `.vscode/settings.json` |
@@ -78,15 +79,12 @@ Ruler solves this by providing a **single source of truth** for all your AI agen
78
79
  | Qwen Code | `AGENTS.md` | `.qwen/settings.json` |
79
80
  | RooCode | `AGENTS.md` | `.roo/mcp.json` |
80
81
  | Zed | `AGENTS.md` | `.zed/settings.json` (project root, never $HOME) |
82
+ | Trae AI | `.trae/rules/project_rules.md` | - |
81
83
  | Warp | `WARP.md` | - |
82
84
  | Kiro | `.kiro/steering/ruler_kiro_instructions.md` | - |
83
85
 
84
86
  ## Getting Started
85
87
 
86
- ### Prerequisites
87
-
88
- Node.js 18.x or higher is required.
89
-
90
88
  ### Installation
91
89
 
92
90
  **Global Installation (Recommended for CLI use):**
@@ -216,13 +214,16 @@ The `apply` command looks for `.ruler/` in the current directory tree, reading t
216
214
  | Option | Description |
217
215
  | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
218
216
  | `--project-root <path>` | Path to your project's root (default: current directory) |
219
- | `--agents <agent1,agent2,...>` | Comma-separated list of agent names to target (agentsmd, amp, copilot, claude, codex, cursor, windsurf, cline, aider, firebase, gemini-cli, junie, augmentcode, kilocode, warp, roo) |
217
+ | `--agents <agent1,agent2,...>` | Comma-separated list of agent names to target (agentsmd, amazonqcli, amp, copilot, claude, codex, cursor, windsurf, cline, aider, firebase, openhands, gemini-cli, jules, junie, augmentcode, kilocode, opencode, goose, crush, zed, qwen, kiro, warp, roo, trae) |
220
218
  | `--config <path>` | Path to a custom `ruler.toml` configuration file |
221
219
  | `--mcp` / `--with-mcp` | Enable applying MCP server configurations (default: true) |
222
220
  | `--no-mcp` | Disable applying MCP server configurations |
223
221
  | `--mcp-overwrite` | Overwrite native MCP config entirely instead of merging |
224
222
  | `--gitignore` | Enable automatic .gitignore updates (default: true) |
225
223
  | `--no-gitignore` | Disable automatic .gitignore updates |
224
+ | `--nested` | Enable nested rule loading from nested .ruler directories (default: disabled) |
225
+ | `--backup` | Enable/disable creation of .bak backup files (default: enabled) |
226
+ | `--dry-run` | Preview changes without writing files |
226
227
  | `--local-only` | Do not look for configuration in `$XDG_CONFIG_HOME` |
227
228
  | `--verbose` / `-v` | Display detailed output during execution |
228
229
 
@@ -252,6 +253,12 @@ ruler apply --agents firebase
252
253
  ruler apply --agents warp
253
254
  ```
254
255
 
256
+ **Apply rules only to Trae AI:**
257
+
258
+ ```bash
259
+ ruler apply --agents trae
260
+ ```
261
+
255
262
  **Apply rules only to RooCode:**
256
263
 
257
264
  ```bash
@@ -300,7 +307,7 @@ ruler revert [options]
300
307
  | Option | Description |
301
308
  | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
302
309
  | `--project-root <path>` | Path to your project's root (default: current directory) |
303
- | `--agents <agent1,agent2,...>` | Comma-separated list of agent names to revert (agentsmd, amp, copilot, claude, codex, cursor, windsurf, cline, aider, firebase, gemini-cli, junie, kilocode, opencode, warp) |
310
+ | `--agents <agent1,agent2,...>` | Comma-separated list of agent names to revert (agentsmd, amazonqcli, amp, copilot, claude, codex, cursor, windsurf, cline, aider, firebase, openhands, gemini-cli, jules, junie, augmentcode, kilocode, opencode, goose, crush, zed, qwen, kiro, warp, roo, trae) |
304
311
  | `--config <path>` | Path to a custom `ruler.toml` configuration file |
305
312
  | `--keep-backups` | Keep backup files (.bak) after restoration (default: false) |
306
313
  | `--dry-run` | Preview changes without actually reverting files |
@@ -616,7 +623,7 @@ This creates context-specific instructions for different parts of your project w
616
623
  2. Commit the `.ruler` directory to your repository
617
624
  3. Team members pull changes and run `ruler apply` to update their local AI agent configurations
618
625
 
619
- ### Scenario 3: Project-Specific Context for AI
626
+ ### Scenario 4: Project-Specific Context for AI
620
627
 
621
628
  1. Detail your project's architecture in `.ruler/project_overview.md`
622
629
  2. Describe primary data structures in `.ruler/data_models.md`
@@ -792,43 +799,5 @@ MIT
792
799
 
793
800
  ---
794
801
 
795
- ## Development and Testing
796
-
797
- ### Running Tests
798
-
799
- The project includes comprehensive test coverage with unit, integration, and end-to-end tests:
800
-
801
- ```bash
802
- # Run all tests
803
- npm test
804
-
805
- # Run with coverage
806
- npm run test:coverage
807
-
808
- # Run integration tests specifically
809
- npm run test:integration
810
-
811
- # Run tests in watch mode
812
- npm run test:watch
813
- ```
814
-
815
- ### Integration Testing
816
-
817
- The project includes comprehensive integration tests that validate the complete CLI workflow:
818
-
819
- - **`npm run test:integration`**: Runs a full end-to-end integration test that:
820
- 1. Creates a temporary test directory
821
- 2. Runs `ruler init` to set up the initial structure
822
- 3. Creates custom `ruler.toml` with MCP servers (both stdio and remote)
823
- 4. Creates sample `AGENTS.md` and additional markdown files for concatenation
824
- 5. Runs `ruler apply` to generate all agent configuration files
825
- 6. Inspects and validates all generated files contain expected content
826
- 7. Outputs the content of all generated files for manual verification
827
- 8. Validates MCP server configurations were properly applied
828
-
829
- This integration test ensures the entire CLI workflow functions correctly and can be used for manual testing or CI validation.
830
-
831
- ---
832
-
833
802
  © Eleanor Berger
834
803
  [ai.intellectronica.net](https://ai.intellectronica.net/)
@@ -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");
@@ -85,7 +84,7 @@ class CodexCliAgent {
85
84
  let existingConfig = {};
86
85
  try {
87
86
  const existingContent = await fs_1.promises.readFile(configPath, 'utf8');
88
- existingConfig = toml.parse(existingContent);
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,59 +120,10 @@ class CodexCliAgent {
121
120
  updatedConfig.mcp_servers[serverName] = mcpServer;
122
121
  }
123
122
  }
124
- // Convert to TOML with special handling for env to ensure it's an inline table
125
- let tomlContent = '';
126
- // Handle non-mcp_servers sections first
127
- const configWithoutMcpServers = { ...updatedConfig };
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
  }
@@ -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 extends AgentsMdAgent_1.AgentsMdAgent {
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, // eslint-disable-line @typescript-eslint/no-unused-vars
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
- // Ensure .gemini/settings.json has contextFileName set to AGENTS.md
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
  }
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.allAgents = exports.AbstractAgent = void 0;
4
+ exports.getAgentIdentifiersForCliHelp = getAgentIdentifiersForCliHelp;
4
5
  const AbstractAgent_1 = require("./AbstractAgent");
5
6
  Object.defineProperty(exports, "AbstractAgent", { enumerable: true, get: function () { return AbstractAgent_1.AbstractAgent; } });
6
7
  const CopilotAgent_1 = require("./CopilotAgent");
@@ -57,3 +58,18 @@ exports.allAgents = [
57
58
  new TraeAgent_1.TraeAgent(),
58
59
  new AmazonQCliAgent_1.AmazonQCliAgent(),
59
60
  ];
61
+ /**
62
+ * Generates a comma-separated list of agent identifiers for CLI help text.
63
+ * Returns identifiers in alphabetical order, with 'agentsmd' always first.
64
+ */
65
+ function getAgentIdentifiersForCliHelp() {
66
+ const identifiers = exports.allAgents.map((agent) => agent.getIdentifier());
67
+ const sorted = identifiers.sort();
68
+ // Ensure agentsmd is first (it should already be first alphabetically, but let's be explicit)
69
+ const agentsMdIndex = sorted.indexOf('agentsmd');
70
+ if (agentsMdIndex > 0) {
71
+ const agentsmd = sorted.splice(agentsMdIndex, 1)[0];
72
+ sorted.unshift(agentsmd);
73
+ }
74
+ return sorted.join(', ');
75
+ }
@@ -7,6 +7,7 @@ exports.run = run;
7
7
  const yargs_1 = __importDefault(require("yargs"));
8
8
  const helpers_1 = require("yargs/helpers");
9
9
  const handlers_1 = require("./handlers");
10
+ const index_1 = require("../agents/index");
10
11
  /**
11
12
  * Sets up and parses CLI commands.
12
13
  */
@@ -23,7 +24,7 @@ function run() {
23
24
  })
24
25
  .option('agents', {
25
26
  type: 'string',
26
- description: 'Comma-separated list of agent identifiers: amp, copilot, claude, codex, cursor, windsurf, cline, aider, firebase, gemini-cli, junie, kilocode, opencode, crush, zed, qwen',
27
+ description: `Comma-separated list of agent identifiers: ${(0, index_1.getAgentIdentifiersForCliHelp)()}`,
27
28
  })
28
29
  .option('config', {
29
30
  type: 'string',
@@ -93,7 +94,7 @@ function run() {
93
94
  })
94
95
  .option('agents', {
95
96
  type: 'string',
96
- description: 'Comma-separated list of agent identifiers: amp, copilot, claude, codex, cursor, windsurf, cline, aider, firebase, gemini-cli, junie, kilocode, opencode, crush, zed, qwen',
97
+ description: `Comma-separated list of agent identifiers: ${(0, index_1.getAgentIdentifiersForCliHelp)()}`,
97
98
  })
98
99
  .option('config', {
99
100
  type: 'string',
package/dist/constants.js CHANGED
@@ -4,6 +4,10 @@ exports.DEFAULT_RULES_FILENAME = exports.ERROR_PREFIX = void 0;
4
4
  exports.actionPrefix = actionPrefix;
5
5
  exports.createRulerError = createRulerError;
6
6
  exports.logVerbose = logVerbose;
7
+ exports.logInfo = logInfo;
8
+ exports.logWarn = logWarn;
9
+ exports.logError = logError;
10
+ exports.logVerboseInfo = logVerboseInfo;
7
11
  exports.ERROR_PREFIX = '[ruler]';
8
12
  // Centralized default rules filename. Now points to 'AGENTS.md'.
9
13
  // Legacy '.ruler/instructions.md' is still supported as a fallback with a warning.
@@ -22,3 +26,26 @@ function logVerbose(message, isVerbose) {
22
26
  console.error(`[ruler:verbose] ${message}`);
23
27
  }
24
28
  }
29
+ /**
30
+ * Centralized logging functions with consistent output streams and prefixing.
31
+ * - info/verbose go to stdout (user-visible progress)
32
+ * - warn/error go to stderr (problems)
33
+ */
34
+ function logInfo(message, dryRun = false) {
35
+ const prefix = actionPrefix(dryRun);
36
+ console.log(`${prefix} ${message}`);
37
+ }
38
+ function logWarn(message, dryRun = false) {
39
+ const prefix = actionPrefix(dryRun);
40
+ console.warn(`${prefix} ${message}`);
41
+ }
42
+ function logError(message, dryRun = false) {
43
+ const prefix = actionPrefix(dryRun);
44
+ console.error(`${prefix} ${message}`);
45
+ }
46
+ function logVerboseInfo(message, isVerbose, dryRun = false) {
47
+ if (isVerbose) {
48
+ const prefix = actionPrefix(dryRun);
49
+ console.log(`${prefix} ${message}`);
50
+ }
51
+ }
@@ -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 TOML = __importStar(require("toml"));
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() ? TOML.parse(text) : {};
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) {
@@ -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 TOML = __importStar(require("toml"));
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() ? TOML.parse(text) : {};
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
- console.warn('[ruler] Warning: Using legacy .ruler/mcp.json. Please migrate to ruler.toml. This fallback will be removed in a future release.');
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;
@@ -122,7 +121,7 @@ async function warnAboutLegacyMcpJson(rulerDir) {
122
121
  try {
123
122
  const legacyMcpPath = path.join(rulerDir, 'mcp.json');
124
123
  await (await Promise.resolve().then(() => __importStar(require('fs/promises')))).access(legacyMcpPath);
125
- console.warn('[ruler] Warning: Using legacy .ruler/mcp.json. Please migrate to ruler.toml. This fallback will be removed in a future release.');
124
+ (0, constants_1.logWarn)('Warning: Using legacy .ruler/mcp.json. Please migrate to ruler.toml. This fallback will be removed in a future release.');
126
125
  }
127
126
  catch {
128
127
  // ignore
@@ -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.
@@ -221,7 +174,7 @@ function selectAgentsToRun(allAgents, config) {
221
174
  async function processHierarchicalConfigurations(agents, configurations, verbose, dryRun, cliMcpEnabled, cliMcpStrategy, backup = true) {
222
175
  const allGeneratedPaths = [];
223
176
  for (const config of configurations) {
224
- console.log(`[ruler] Processing .ruler directory: ${config.rulerDir}`);
177
+ (0, constants_1.logVerboseInfo)(`Processing .ruler directory: ${config.rulerDir}`, verbose, dryRun);
225
178
  const rulerRoot = path.dirname(config.rulerDir);
226
179
  const paths = await applyConfigurationsToAgents(agents, config.concatenatedRules, config.rulerMcpJson, config.config, rulerRoot, verbose, dryRun, cliMcpEnabled, cliMcpStrategy, backup);
227
180
  allGeneratedPaths.push(...paths);
@@ -258,8 +211,7 @@ async function applyConfigurationsToAgents(agents, concatenatedRules, rulerMcpJs
258
211
  const generatedPaths = [];
259
212
  let agentsMdWritten = false;
260
213
  for (const agent of agents) {
261
- const prefix = (0, constants_1.actionPrefix)(dryRun);
262
- console.log(`${prefix} Applying rules for ${agent.getName()}...`);
214
+ (0, constants_1.logInfo)(`Applying rules for ${agent.getName()}...`, dryRun);
263
215
  (0, constants_1.logVerbose)(`Processing agent: ${agent.getName()}`, verbose);
264
216
  const agentConfig = config.agentConfigs[agent.getIdentifier()];
265
217
  // Collect output paths for .gitignore
@@ -356,6 +308,13 @@ async function applyMcpConfiguration(agent, filteredMcpJson, dest, agentConfig,
356
308
  if (agent.getIdentifier() === 'opencode') {
357
309
  return await applyOpenCodeMcpConfiguration(filteredMcpJson, dest, dryRun, verbose, backup);
358
310
  }
311
+ // Agents that handle MCP configuration internally should not have external MCP handling
312
+ if (agent.getIdentifier() === 'zed' ||
313
+ agent.getIdentifier() === 'gemini-cli' ||
314
+ agent.getIdentifier() === 'amazon-q-cli') {
315
+ (0, constants_1.logVerbose)(`Skipping external MCP config for ${agent.getName()} - handled internally by agent`, verbose);
316
+ return;
317
+ }
359
318
  return await applyStandardMcpConfiguration(agent, filteredMcpJson, dest, agentConfig, config, cliMcpStrategy, dryRun, verbose, backup);
360
319
  }
361
320
  async function applyOpenHandsMcpConfiguration(filteredMcpJson, dest, dryRun, verbose, backup = true) {
@@ -385,13 +344,21 @@ async function applyStandardMcpConfiguration(agent, filteredMcpJson, dest, agent
385
344
  (0, constants_1.logVerbose)(`DRY RUN: Would apply MCP config to: ${dest}`, verbose);
386
345
  }
387
346
  else {
388
- if (backup) {
389
- const { backupFile } = await Promise.resolve().then(() => __importStar(require('../core/FileSystemUtils')));
390
- await backupFile(dest);
391
- }
392
347
  const existing = await (0, mcp_1.readNativeMcp)(dest);
393
348
  const merged = (0, merge_1.mergeMcp)(existing, filteredMcpJson, strategy, serverKey);
394
- await (0, mcp_1.writeNativeMcp)(dest, merged);
349
+ // Only backup and write if content would actually change (idempotent)
350
+ const currentContent = JSON.stringify(existing, null, 2);
351
+ const newContent = JSON.stringify(merged, null, 2);
352
+ if (currentContent !== newContent) {
353
+ if (backup) {
354
+ const { backupFile } = await Promise.resolve().then(() => __importStar(require('../core/FileSystemUtils')));
355
+ await backupFile(dest);
356
+ }
357
+ await (0, mcp_1.writeNativeMcp)(dest, merged);
358
+ }
359
+ else {
360
+ (0, constants_1.logVerbose)(`MCP config for ${agent.getName()} is already up to date - skipping backup and write`, verbose);
361
+ }
395
362
  }
396
363
  }
397
364
  /**
@@ -419,13 +386,12 @@ async function updateGitignore(projectRoot, generatedPaths, config, cliGitignore
419
386
  // Note: Individual backup patterns are added per-file in the collection phase
420
387
  // No need to add a broad *.bak pattern here
421
388
  if (uniquePaths.length > 0) {
422
- const prefix = (0, constants_1.actionPrefix)(dryRun);
423
389
  if (dryRun) {
424
- console.log(`${prefix} Would update .gitignore with ${uniquePaths.length} unique path(s): ${uniquePaths.join(', ')}`);
390
+ (0, constants_1.logInfo)(`Would update .gitignore with ${uniquePaths.length} unique path(s): ${uniquePaths.join(', ')}`, dryRun);
425
391
  }
426
392
  else {
427
393
  await (0, GitignoreUtils_1.updateGitignore)(projectRoot, uniquePaths);
428
- console.log(`${prefix} Updated .gitignore with ${uniquePaths.length} unique path(s) in the Ruler block.`);
394
+ (0, constants_1.logInfo)(`Updated .gitignore with ${uniquePaths.length} unique path(s) in the Ruler block.`, dryRun);
429
395
  }
430
396
  }
431
397
  }
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, apply_engine_1.selectAgentsToRun)(agents, rootConfig);
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,7 +50,7 @@ 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, apply_engine_1.selectAgentsToRun)(agents, singleConfig.config);
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
  }
@@ -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 = TOML.parse(tomlContent);
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(home, '.codeium', 'windsurf', 'mcp_config.json'));
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(home, '.codex', 'config.json'));
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.5",
3
+ "version": "0.3.7",
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
  }