@intellectronica/ruler 0.2.19 → 0.3.0

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.
Files changed (47) hide show
  1. package/README.md +190 -31
  2. package/dist/agents/AbstractAgent.js +82 -0
  3. package/dist/agents/AgentsMdAgent.js +82 -0
  4. package/dist/agents/AiderAgent.js +29 -9
  5. package/dist/agents/AmpAgent.js +2 -44
  6. package/dist/agents/AugmentCodeAgent.js +10 -12
  7. package/dist/agents/ClaudeAgent.js +9 -9
  8. package/dist/agents/ClineAgent.js +3 -9
  9. package/dist/agents/CodexCliAgent.js +27 -21
  10. package/dist/agents/CopilotAgent.js +9 -10
  11. package/dist/agents/CrushAgent.js +6 -0
  12. package/dist/agents/CursorAgent.js +13 -5
  13. package/dist/agents/FirebaseAgent.js +2 -8
  14. package/dist/agents/GeminiCliAgent.js +31 -22
  15. package/dist/agents/GooseAgent.js +2 -10
  16. package/dist/agents/JulesAgent.js +3 -44
  17. package/dist/agents/JunieAgent.js +2 -9
  18. package/dist/agents/KiloCodeAgent.js +8 -9
  19. package/dist/agents/KiroAgent.js +50 -0
  20. package/dist/agents/OpenCodeAgent.js +50 -11
  21. package/dist/agents/OpenHandsAgent.js +8 -9
  22. package/dist/agents/QwenCodeAgent.js +83 -0
  23. package/dist/agents/WarpAgent.js +61 -0
  24. package/dist/agents/WindsurfAgent.js +9 -10
  25. package/dist/agents/ZedAgent.js +132 -0
  26. package/dist/agents/agent-utils.js +37 -0
  27. package/dist/agents/index.js +53 -0
  28. package/dist/cli/commands.js +48 -242
  29. package/dist/cli/handlers.js +176 -0
  30. package/dist/constants.js +9 -2
  31. package/dist/core/ConfigLoader.js +1 -1
  32. package/dist/core/FileSystemUtils.js +51 -4
  33. package/dist/core/RuleProcessor.js +15 -3
  34. package/dist/core/UnifiedConfigLoader.js +357 -0
  35. package/dist/core/UnifiedConfigTypes.js +2 -0
  36. package/dist/core/agent-selection.js +52 -0
  37. package/dist/core/apply-engine.js +302 -0
  38. package/dist/core/config-utils.js +30 -0
  39. package/dist/core/hash.js +24 -0
  40. package/dist/core/revert-engine.js +413 -0
  41. package/dist/lib.js +20 -330
  42. package/dist/mcp/capabilities.js +51 -0
  43. package/dist/mcp/propagateOpenCodeMcp.js +30 -31
  44. package/dist/mcp/propagateOpenHandsMcp.js +101 -17
  45. package/dist/paths/mcp.js +7 -3
  46. package/dist/revert.js +96 -481
  47. package/package.json +2 -1
@@ -0,0 +1,302 @@
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.loadRulerConfiguration = loadRulerConfiguration;
37
+ exports.selectAgentsToRun = selectAgentsToRun;
38
+ exports.applyConfigurationsToAgents = applyConfigurationsToAgents;
39
+ exports.updateGitignore = updateGitignore;
40
+ const path = __importStar(require("path"));
41
+ const FileSystemUtils = __importStar(require("./FileSystemUtils"));
42
+ const RuleProcessor_1 = require("./RuleProcessor");
43
+ const ConfigLoader_1 = require("./ConfigLoader");
44
+ const GitignoreUtils_1 = require("./GitignoreUtils");
45
+ const merge_1 = require("../mcp/merge");
46
+ const mcp_1 = require("../paths/mcp");
47
+ const propagateOpenHandsMcp_1 = require("../mcp/propagateOpenHandsMcp");
48
+ const propagateOpenCodeMcp_1 = require("../mcp/propagateOpenCodeMcp");
49
+ const agent_utils_1 = require("../agents/agent-utils");
50
+ const capabilities_1 = require("../mcp/capabilities");
51
+ const constants_1 = require("../constants");
52
+ /**
53
+ * Loads all necessary configurations for ruler operation.
54
+ * @param projectRoot Root directory of the project
55
+ * @param configPath Optional custom config path
56
+ * @param localOnly Whether to search only locally for .ruler directory
57
+ * @returns Promise resolving to the loaded configuration
58
+ */
59
+ async function loadRulerConfiguration(projectRoot, configPath, localOnly) {
60
+ // Find the .ruler directory
61
+ const rulerDir = await FileSystemUtils.findRulerDir(projectRoot, !localOnly);
62
+ if (!rulerDir) {
63
+ throw (0, constants_1.createRulerError)(`.ruler directory not found`, `Searched from: ${projectRoot}`);
64
+ }
65
+ // Load the ruler.toml configuration
66
+ const config = await (0, ConfigLoader_1.loadConfig)({
67
+ projectRoot,
68
+ configPath,
69
+ });
70
+ // Read and concatenate the markdown rule files
71
+ const files = await FileSystemUtils.readMarkdownFiles(rulerDir);
72
+ const concatenatedRules = (0, RuleProcessor_1.concatenateRules)(files, path.dirname(rulerDir));
73
+ // Load unified config to get merged MCP configuration
74
+ const { loadUnifiedConfig } = await Promise.resolve().then(() => __importStar(require('./UnifiedConfigLoader')));
75
+ const unifiedConfig = await loadUnifiedConfig({ projectRoot, configPath });
76
+ // Synthesize rulerMcpJson from unified MCP bundle for backward compatibility
77
+ let rulerMcpJson = null;
78
+ if (unifiedConfig.mcp && Object.keys(unifiedConfig.mcp.servers).length > 0) {
79
+ rulerMcpJson = {
80
+ mcpServers: unifiedConfig.mcp.servers,
81
+ };
82
+ }
83
+ return {
84
+ config,
85
+ concatenatedRules,
86
+ rulerMcpJson,
87
+ };
88
+ }
89
+ /**
90
+ * Selects the agents to process based on configuration.
91
+ * @param allAgents Array of all available agents
92
+ * @param config Loaded configuration
93
+ * @returns Array of agents to be processed
94
+ */
95
+ function selectAgentsToRun(allAgents, config) {
96
+ // CLI --agents > config.default_agents > per-agent.enabled flags > default all
97
+ let selected = allAgents;
98
+ if (config.cliAgents && config.cliAgents.length > 0) {
99
+ const filters = config.cliAgents.map((n) => n.toLowerCase());
100
+ // Check if any of the specified agents don't exist
101
+ const validAgentIdentifiers = new Set(allAgents.map((agent) => agent.getIdentifier()));
102
+ const validAgentNames = new Set(allAgents.map((agent) => agent.getName().toLowerCase()));
103
+ const invalidAgents = filters.filter((filter) => !validAgentIdentifiers.has(filter) &&
104
+ ![...validAgentNames].some((name) => name.includes(filter)));
105
+ if (invalidAgents.length > 0) {
106
+ throw (0, constants_1.createRulerError)(`Invalid agent specified: ${invalidAgents.join(', ')}`, `Valid agents are: ${[...validAgentIdentifiers].join(', ')}`);
107
+ }
108
+ selected = allAgents.filter((agent) => filters.some((f) => agent.getIdentifier() === f ||
109
+ agent.getName().toLowerCase().includes(f)));
110
+ }
111
+ else if (config.defaultAgents && config.defaultAgents.length > 0) {
112
+ const defaults = config.defaultAgents.map((n) => n.toLowerCase());
113
+ // Check if any of the default agents don't exist
114
+ const validAgentIdentifiers = new Set(allAgents.map((agent) => agent.getIdentifier()));
115
+ const validAgentNames = new Set(allAgents.map((agent) => agent.getName().toLowerCase()));
116
+ const invalidAgents = defaults.filter((filter) => !validAgentIdentifiers.has(filter) &&
117
+ ![...validAgentNames].some((name) => name.includes(filter)));
118
+ if (invalidAgents.length > 0) {
119
+ throw (0, constants_1.createRulerError)(`Invalid agent specified in default_agents: ${invalidAgents.join(', ')}`, `Valid agents are: ${[...validAgentIdentifiers].join(', ')}`);
120
+ }
121
+ selected = allAgents.filter((agent) => {
122
+ const identifier = agent.getIdentifier();
123
+ const override = config.agentConfigs[identifier]?.enabled;
124
+ if (override !== undefined) {
125
+ return override;
126
+ }
127
+ return defaults.some((d) => identifier === d || agent.getName().toLowerCase().includes(d));
128
+ });
129
+ }
130
+ else {
131
+ selected = allAgents.filter((agent) => config.agentConfigs[agent.getIdentifier()]?.enabled !== false);
132
+ }
133
+ return selected;
134
+ }
135
+ /**
136
+ * Applies configurations to the selected agents.
137
+ * @param agents Array of agents to process
138
+ * @param concatenatedRules Concatenated rule content
139
+ * @param rulerMcpJson MCP configuration JSON
140
+ * @param config Loaded configuration
141
+ * @param projectRoot Root directory of the project
142
+ * @param verbose Whether to enable verbose logging
143
+ * @param dryRun Whether to perform a dry run
144
+ * @returns Promise resolving to array of generated file paths
145
+ */
146
+ async function applyConfigurationsToAgents(agents, concatenatedRules, rulerMcpJson, config, projectRoot, verbose, dryRun, cliMcpEnabled = true, cliMcpStrategy) {
147
+ const generatedPaths = [];
148
+ let agentsMdWritten = false;
149
+ for (const agent of agents) {
150
+ const prefix = (0, constants_1.actionPrefix)(dryRun);
151
+ console.log(`${prefix} Applying rules for ${agent.getName()}...`);
152
+ (0, constants_1.logVerbose)(`Processing agent: ${agent.getName()}`, verbose);
153
+ const agentConfig = config.agentConfigs[agent.getIdentifier()];
154
+ // Collect output paths for .gitignore
155
+ const outputPaths = (0, agent_utils_1.getAgentOutputPaths)(agent, projectRoot, agentConfig);
156
+ (0, constants_1.logVerbose)(`Agent ${agent.getName()} output paths: ${outputPaths.join(', ')}`, verbose);
157
+ generatedPaths.push(...outputPaths);
158
+ // Also add the backup file paths to the gitignore list
159
+ const backupPaths = outputPaths.map((p) => `${p}.bak`);
160
+ generatedPaths.push(...backupPaths);
161
+ if (dryRun) {
162
+ (0, constants_1.logVerbose)(`DRY RUN: Would write rules to: ${outputPaths.join(', ')}`, verbose);
163
+ }
164
+ else {
165
+ let skipApplyForThisAgent = false;
166
+ if (agent.getIdentifier() === 'jules' ||
167
+ agent.getIdentifier() === 'agentsmd') {
168
+ if (agentsMdWritten) {
169
+ // Skip rewriting AGENTS.md, but still allow MCP handling below
170
+ skipApplyForThisAgent = true;
171
+ }
172
+ else {
173
+ agentsMdWritten = true;
174
+ }
175
+ }
176
+ let finalAgentConfig = agentConfig;
177
+ if (agent.getIdentifier() === 'augmentcode' && rulerMcpJson) {
178
+ const resolvedStrategy = cliMcpStrategy ??
179
+ agentConfig?.mcp?.strategy ??
180
+ config.mcp?.strategy ??
181
+ 'merge';
182
+ finalAgentConfig = {
183
+ ...agentConfig,
184
+ mcp: {
185
+ ...agentConfig?.mcp,
186
+ strategy: resolvedStrategy,
187
+ },
188
+ };
189
+ }
190
+ if (!skipApplyForThisAgent) {
191
+ await agent.applyRulerConfig(concatenatedRules, projectRoot, rulerMcpJson, finalAgentConfig);
192
+ }
193
+ }
194
+ // Handle MCP configuration
195
+ await handleMcpConfiguration(agent, agentConfig, config, rulerMcpJson, projectRoot, generatedPaths, verbose, dryRun, cliMcpEnabled, cliMcpStrategy);
196
+ }
197
+ return generatedPaths;
198
+ }
199
+ /**
200
+ * Handles MCP configuration for a specific agent.
201
+ */
202
+ async function handleMcpConfiguration(agent, agentConfig, config, rulerMcpJson, projectRoot, generatedPaths, verbose, dryRun, cliMcpEnabled = true, cliMcpStrategy) {
203
+ // Check if agent supports MCP at all
204
+ if (!(0, capabilities_1.agentSupportsMcp)(agent)) {
205
+ (0, constants_1.logVerbose)(`Agent ${agent.getName()} does not support MCP - skipping MCP configuration`, verbose);
206
+ return;
207
+ }
208
+ const dest = await (0, mcp_1.getNativeMcpPath)(agent.getName(), projectRoot);
209
+ const mcpEnabledForAgent = cliMcpEnabled && (agentConfig?.mcp?.enabled ?? config.mcp?.enabled ?? true);
210
+ if (dest && mcpEnabledForAgent && rulerMcpJson) {
211
+ // Filter MCP configuration based on agent capabilities
212
+ const filteredMcpJson = (0, capabilities_1.filterMcpConfigForAgent)(rulerMcpJson, agent);
213
+ if (!filteredMcpJson) {
214
+ (0, constants_1.logVerbose)(`No compatible MCP servers found for ${agent.getName()} - skipping MCP configuration`, verbose);
215
+ return;
216
+ }
217
+ // Include MCP config file in .gitignore only if it's within the project directory
218
+ if (dest.startsWith(projectRoot)) {
219
+ const relativeDest = path.relative(projectRoot, dest);
220
+ generatedPaths.push(relativeDest);
221
+ // Also add the backup for the MCP file
222
+ generatedPaths.push(`${relativeDest}.bak`);
223
+ }
224
+ // Prevent writing MCP configs outside the project root (e.g., legacy home-directory targets)
225
+ if (!dest.startsWith(projectRoot)) {
226
+ (0, constants_1.logVerbose)(`Skipping MCP config for ${agent.getName()} because target path is outside project: ${dest}`, verbose);
227
+ return;
228
+ }
229
+ if (agent.getIdentifier() === 'openhands') {
230
+ // *** Special handling for Open Hands ***
231
+ if (dryRun) {
232
+ (0, constants_1.logVerbose)(`DRY RUN: Would apply MCP config by updating TOML file: ${dest}`, verbose);
233
+ }
234
+ else {
235
+ await (0, propagateOpenHandsMcp_1.propagateMcpToOpenHands)(filteredMcpJson, dest);
236
+ }
237
+ }
238
+ else if (agent.getIdentifier() === 'opencode') {
239
+ // *** Special handling for OpenCode ***
240
+ if (dryRun) {
241
+ (0, constants_1.logVerbose)(`DRY RUN: Would apply MCP config by updating OpenCode config file: ${dest}`, verbose);
242
+ }
243
+ else {
244
+ await (0, propagateOpenCodeMcp_1.propagateMcpToOpenCode)(filteredMcpJson, dest);
245
+ }
246
+ }
247
+ else {
248
+ // Standard MCP handling using capabilities
249
+ const strategy = cliMcpStrategy ??
250
+ agentConfig?.mcp?.strategy ??
251
+ config.mcp?.strategy ??
252
+ 'merge';
253
+ // Determine the correct server key for the agent
254
+ const serverKey = agent.getMcpServerKey?.() || 'mcpServers';
255
+ (0, constants_1.logVerbose)(`Applying filtered MCP config for ${agent.getName()} with strategy: ${strategy} and key: ${serverKey}`, verbose);
256
+ if (dryRun) {
257
+ (0, constants_1.logVerbose)(`DRY RUN: Would apply MCP config to: ${dest}`, verbose);
258
+ }
259
+ else {
260
+ const existing = await (0, mcp_1.readNativeMcp)(dest);
261
+ const merged = (0, merge_1.mergeMcp)(existing, filteredMcpJson, strategy, serverKey);
262
+ await (0, mcp_1.writeNativeMcp)(dest, merged);
263
+ }
264
+ }
265
+ }
266
+ }
267
+ /**
268
+ * Updates the .gitignore file with generated paths.
269
+ * @param projectRoot Root directory of the project
270
+ * @param generatedPaths Array of generated file paths
271
+ * @param config Loaded configuration
272
+ * @param cliGitignoreEnabled CLI gitignore setting
273
+ * @param dryRun Whether to perform a dry run
274
+ */
275
+ async function updateGitignore(projectRoot, generatedPaths, config, cliGitignoreEnabled, dryRun) {
276
+ // Configuration precedence: CLI > TOML > Default (enabled)
277
+ let gitignoreEnabled;
278
+ if (cliGitignoreEnabled !== undefined) {
279
+ gitignoreEnabled = cliGitignoreEnabled;
280
+ }
281
+ else if (config.gitignore?.enabled !== undefined) {
282
+ gitignoreEnabled = config.gitignore.enabled;
283
+ }
284
+ else {
285
+ gitignoreEnabled = true; // Default enabled
286
+ }
287
+ if (gitignoreEnabled && generatedPaths.length > 0) {
288
+ const uniquePaths = [...new Set(generatedPaths)];
289
+ // Add wildcard pattern for backup files
290
+ uniquePaths.push('*.bak');
291
+ if (uniquePaths.length > 0) {
292
+ const prefix = (0, constants_1.actionPrefix)(dryRun);
293
+ if (dryRun) {
294
+ console.log(`${prefix} Would update .gitignore with ${uniquePaths.length} unique path(s): ${uniquePaths.join(', ')}`);
295
+ }
296
+ else {
297
+ await (0, GitignoreUtils_1.updateGitignore)(projectRoot, uniquePaths);
298
+ console.log(`${prefix} Updated .gitignore with ${uniquePaths.length} unique path(s) in the Ruler block.`);
299
+ }
300
+ }
301
+ }
302
+ }
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.mapRawAgentConfigs = mapRawAgentConfigs;
4
+ /**
5
+ * Maps raw agent configuration keys to their corresponding agent identifiers.
6
+ *
7
+ * This function normalizes configuration keys by matching them against agent identifiers
8
+ * and display names. It performs both exact matching (case-insensitive) with agent
9
+ * identifiers and substring matching (case-insensitive) with agent display names
10
+ * for backwards compatibility.
11
+ *
12
+ * @param raw Raw agent configurations with user-provided keys
13
+ * @param agents Array of all available agents
14
+ * @returns Record with agent identifiers as keys and their configurations as values
15
+ */
16
+ function mapRawAgentConfigs(raw, agents) {
17
+ const mappedConfigs = {};
18
+ for (const [key, cfg] of Object.entries(raw)) {
19
+ const lowerKey = key.toLowerCase();
20
+ for (const agent of agents) {
21
+ const identifier = agent.getIdentifier();
22
+ // Exact match with identifier or substring match with display name for backwards compatibility
23
+ if (identifier === lowerKey ||
24
+ agent.getName().toLowerCase().includes(lowerKey)) {
25
+ mappedConfigs[identifier] = cfg;
26
+ }
27
+ }
28
+ }
29
+ return mappedConfigs;
30
+ }
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sha256 = sha256;
4
+ exports.stableJson = stableJson;
5
+ const crypto_1 = require("crypto");
6
+ function sha256(data) {
7
+ return (0, crypto_1.createHash)('sha256').update(data, 'utf8').digest('hex');
8
+ }
9
+ // Stable JSON stringify: sorts object keys recursively.
10
+ function stableJson(value) {
11
+ return JSON.stringify(sortValue(value));
12
+ }
13
+ function sortValue(value) {
14
+ if (Array.isArray(value)) {
15
+ return value.map(sortValue);
16
+ }
17
+ if (value && typeof value === 'object') {
18
+ const entries = Object.entries(value)
19
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
20
+ .map(([k, v]) => [k, sortValue(v)]);
21
+ return Object.fromEntries(entries);
22
+ }
23
+ return value;
24
+ }