@intellectronica/ruler 0.3.41 → 0.3.42

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 (94) hide show
  1. package/README.md +40 -28
  2. package/dist/agents/AbstractAgent.d.ts +53 -0
  3. package/dist/agents/AgentsMdAgent.d.ts +14 -0
  4. package/dist/agents/AiderAgent.d.ts +14 -0
  5. package/dist/agents/AiderAgent.js +3 -1
  6. package/dist/agents/AmazonQCliAgent.d.ts +13 -0
  7. package/dist/agents/AmpAgent.d.ts +6 -0
  8. package/dist/agents/AntigravityAgent.d.ts +10 -0
  9. package/dist/agents/AugmentCodeAgent.d.ts +13 -0
  10. package/dist/agents/ClaudeAgent.d.ts +13 -0
  11. package/dist/agents/ClineAgent.d.ts +9 -0
  12. package/dist/agents/CodexCliAgent.d.ts +31 -0
  13. package/dist/agents/CopilotAgent.d.ts +20 -0
  14. package/dist/agents/CrushAgent.d.ts +14 -0
  15. package/dist/agents/CrushAgent.js +5 -2
  16. package/dist/agents/CursorAgent.d.ts +17 -0
  17. package/dist/agents/FactoryDroidAgent.d.ts +13 -0
  18. package/dist/agents/FirebaseAgent.d.ts +11 -0
  19. package/dist/agents/FirebenderAgent.d.ts +36 -0
  20. package/dist/agents/GeminiCliAgent.d.ts +11 -0
  21. package/dist/agents/GeminiCliAgent.js +2 -2
  22. package/dist/agents/GooseAgent.d.ts +12 -0
  23. package/dist/agents/IAgent.d.ts +72 -0
  24. package/dist/agents/JetBrainsAiAssistantAgent.d.ts +10 -0
  25. package/dist/agents/JulesAgent.d.ts +5 -0
  26. package/dist/agents/JunieAgent.d.ts +12 -0
  27. package/dist/agents/KiloCodeAgent.d.ts +14 -0
  28. package/dist/agents/KiroAgent.d.ts +8 -0
  29. package/dist/agents/MistralVibeAgent.d.ts +31 -0
  30. package/dist/agents/OpenCodeAgent.d.ts +11 -0
  31. package/dist/agents/OpenCodeAgent.js +14 -9
  32. package/dist/agents/OpenHandsAgent.d.ts +8 -0
  33. package/dist/agents/PiAgent.d.ts +9 -0
  34. package/dist/agents/QwenCodeAgent.d.ts +10 -0
  35. package/dist/agents/QwenCodeAgent.js +2 -2
  36. package/dist/agents/RooCodeAgent.d.ts +16 -0
  37. package/dist/agents/TraeAgent.d.ts +10 -0
  38. package/dist/agents/WarpAgent.d.ts +12 -0
  39. package/dist/agents/WindsurfAgent.d.ts +13 -0
  40. package/dist/agents/ZedAgent.d.ts +21 -0
  41. package/dist/agents/ZedAgent.js +5 -2
  42. package/dist/agents/agent-utils.d.ts +5 -0
  43. package/dist/agents/agent-utils.js +8 -5
  44. package/dist/agents/index.d.ts +9 -0
  45. package/dist/cli/commands.d.ts +4 -0
  46. package/dist/cli/commands.js +1 -2
  47. package/dist/cli/handlers.d.ts +41 -0
  48. package/dist/cli/handlers.js +75 -59
  49. package/dist/cli/index.d.ts +2 -0
  50. package/dist/constants.d.ts +35 -0
  51. package/dist/core/ConfigLoader.d.ts +57 -0
  52. package/dist/core/ConfigLoader.js +106 -39
  53. package/dist/core/FileSystemUtils.d.ts +51 -0
  54. package/dist/core/FileSystemUtils.js +37 -17
  55. package/dist/core/GitignoreUtils.d.ts +15 -0
  56. package/dist/core/GitignoreUtils.js +32 -1
  57. package/dist/core/RuleProcessor.d.ts +8 -0
  58. package/dist/core/SkillsProcessor.d.ts +127 -0
  59. package/dist/core/SkillsProcessor.js +104 -218
  60. package/dist/core/SkillsUtils.d.ts +26 -0
  61. package/dist/core/SubagentsProcessor.d.ts +38 -0
  62. package/dist/core/SubagentsUtils.d.ts +34 -0
  63. package/dist/core/UnifiedConfigLoader.d.ts +10 -0
  64. package/dist/core/UnifiedConfigLoader.js +61 -31
  65. package/dist/core/UnifiedConfigTypes.d.ts +95 -0
  66. package/dist/core/agent-selection.d.ts +12 -0
  67. package/dist/core/agent-selection.js +11 -3
  68. package/dist/core/apply-engine.d.ts +69 -0
  69. package/dist/core/apply-engine.js +57 -50
  70. package/dist/core/config-utils.d.ts +14 -0
  71. package/dist/core/config-utils.js +9 -3
  72. package/dist/core/hash.d.ts +2 -0
  73. package/dist/core/path-utils.d.ts +1 -0
  74. package/dist/core/path-utils.js +42 -0
  75. package/dist/core/revert-engine.d.ts +36 -0
  76. package/dist/core/revert-engine.js +70 -9
  77. package/dist/lib.d.ts +13 -0
  78. package/dist/lib.js +16 -3
  79. package/dist/mcp/capabilities.d.ts +20 -0
  80. package/dist/mcp/merge.d.ts +10 -0
  81. package/dist/mcp/merge.js +19 -1
  82. package/dist/mcp/propagateOpenCodeMcp.d.ts +2 -0
  83. package/dist/mcp/propagateOpenCodeMcp.js +21 -9
  84. package/dist/mcp/propagateOpenHandsMcp.d.ts +2 -0
  85. package/dist/mcp/propagateOpenHandsMcp.js +31 -15
  86. package/dist/mcp/validate.d.ts +7 -0
  87. package/dist/mcp/validate.js +6 -1
  88. package/dist/paths/mcp.d.ts +8 -0
  89. package/dist/paths/mcp.js +33 -4
  90. package/dist/revert.d.ts +6 -0
  91. package/dist/revert.js +39 -27
  92. package/dist/types.d.ts +87 -0
  93. package/dist/vscode/settings.d.ts +40 -0
  94. package/package.json +6 -4
@@ -0,0 +1,42 @@
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.isPathInsideOrEqual = isPathInsideOrEqual;
37
+ const path = __importStar(require("path"));
38
+ function isPathInsideOrEqual(parentPath, targetPath) {
39
+ const relative = path.relative(path.resolve(parentPath), path.resolve(targetPath));
40
+ return (relative === '' ||
41
+ (!relative.startsWith('..') && !path.isAbsolute(relative)));
42
+ }
@@ -0,0 +1,36 @@
1
+ import { IAgent } from '../agents/IAgent';
2
+ import { IAgentConfig } from './ConfigLoader';
3
+ /**
4
+ * Result of reverting an agent configuration
5
+ */
6
+ export interface RevertAgentResult {
7
+ restored: number;
8
+ removed: number;
9
+ backupsRemoved: number;
10
+ }
11
+ /**
12
+ * Result of cleaning up auxiliary files
13
+ */
14
+ export interface CleanUpResult {
15
+ additionalFilesRemoved: number;
16
+ directoriesRemoved: number;
17
+ }
18
+ /**
19
+ * Reverts configuration for a single agent.
20
+ * @param agent The agent to revert
21
+ * @param projectRoot Root directory of the project
22
+ * @param agentConfig Agent-specific configuration
23
+ * @param keepBackups Whether to keep backup files
24
+ * @param verbose Whether to enable verbose logging
25
+ * @param dryRun Whether to perform a dry run
26
+ * @returns Promise resolving to revert statistics
27
+ */
28
+ export declare function revertAgentConfiguration(agent: IAgent, projectRoot: string, agentConfig: IAgentConfig | undefined, keepBackups: boolean, verbose: boolean, dryRun: boolean): Promise<RevertAgentResult>;
29
+ /**
30
+ * Cleans up auxiliary files and directories.
31
+ * @param projectRoot Root directory of the project
32
+ * @param verbose Whether to enable verbose logging
33
+ * @param dryRun Whether to perform a dry run
34
+ * @returns Promise resolving to cleanup statistics
35
+ */
36
+ export declare function cleanUpAuxiliaryFiles(projectRoot: string, verbose: boolean, dryRun: boolean): Promise<CleanUpResult>;
@@ -40,7 +40,12 @@ const fs_1 = require("fs");
40
40
  const agent_utils_1 = require("../agents/agent-utils");
41
41
  const mcp_1 = require("../paths/mcp");
42
42
  const constants_1 = require("../constants");
43
+ const GitignoreUtils_1 = require("./GitignoreUtils");
43
44
  const settings_1 = require("../vscode/settings");
45
+ const path_utils_1 = require("./path-utils");
46
+ const RULER_START_MARKER = '# START Ruler Generated Files';
47
+ const RULER_END_MARKER = '# END Ruler Generated Files';
48
+ const RULER_GENERATED_MARKER = '<!-- Generated by Ruler -->';
44
49
  /**
45
50
  * Checks if a file exists.
46
51
  */
@@ -53,6 +58,54 @@ async function fileExists(filePath) {
53
58
  return false;
54
59
  }
55
60
  }
61
+ async function ignoreFileHasRulerGeneratedPath(ignoreFilePath, generatedPath) {
62
+ try {
63
+ const content = await fs_1.promises.readFile(ignoreFilePath, 'utf8');
64
+ const lines = content.split('\n');
65
+ let inRulerBlock = false;
66
+ for (const line of lines) {
67
+ const trimmed = line.trim();
68
+ if (trimmed === RULER_START_MARKER) {
69
+ inRulerBlock = true;
70
+ continue;
71
+ }
72
+ if (trimmed === RULER_END_MARKER) {
73
+ inRulerBlock = false;
74
+ continue;
75
+ }
76
+ if (inRulerBlock &&
77
+ (trimmed === generatedPath || trimmed === generatedPath.slice(1))) {
78
+ return true;
79
+ }
80
+ }
81
+ }
82
+ catch {
83
+ return false;
84
+ }
85
+ return false;
86
+ }
87
+ async function hasRulerGeneratedProvenance(filePath, projectRoot) {
88
+ try {
89
+ const content = await fs_1.promises.readFile(filePath, 'utf8');
90
+ if (content.startsWith(RULER_GENERATED_MARKER)) {
91
+ return true;
92
+ }
93
+ }
94
+ catch {
95
+ return false;
96
+ }
97
+ const relativePath = `/${path.relative(projectRoot, filePath).replace(/\\/g, '/')}`;
98
+ const ignoreFiles = [
99
+ await (0, GitignoreUtils_1.resolveIgnoreFilePath)(projectRoot, '.gitignore'),
100
+ await (0, GitignoreUtils_1.resolveIgnoreFilePath)(projectRoot, '.git/info/exclude'),
101
+ ];
102
+ for (const ignoreFile of ignoreFiles) {
103
+ if (await ignoreFileHasRulerGeneratedPath(ignoreFile, relativePath)) {
104
+ return true;
105
+ }
106
+ }
107
+ return false;
108
+ }
56
109
  /**
57
110
  * Restores a file from its backup if the backup exists.
58
111
  */
@@ -76,7 +129,7 @@ async function restoreFromBackup(filePath, verbose, dryRun) {
76
129
  /**
77
130
  * Removes a file if it exists and has no backup (meaning it was generated by ruler).
78
131
  */
79
- async function removeGeneratedFile(filePath, verbose, dryRun) {
132
+ async function removeGeneratedFile(filePath, verbose, dryRun, projectRoot) {
80
133
  const fileExistsFlag = await fileExists(filePath);
81
134
  const backupExists = await fileExists(`${filePath}.bak`);
82
135
  if (!fileExistsFlag) {
@@ -87,6 +140,11 @@ async function removeGeneratedFile(filePath, verbose, dryRun) {
87
140
  (0, constants_1.logVerbose)(`File has backup, skipping removal: ${filePath}`, verbose);
88
141
  return false;
89
142
  }
143
+ if (projectRoot &&
144
+ !(await hasRulerGeneratedProvenance(filePath, projectRoot))) {
145
+ (0, constants_1.logVerbose)(`Preserving file without backup or Ruler provenance: ${filePath}`, verbose);
146
+ return false;
147
+ }
90
148
  const prefix = (0, constants_1.actionPrefix)(dryRun);
91
149
  if (dryRun) {
92
150
  (0, constants_1.logVerbose)(`${prefix} Would remove generated file: ${filePath}`, verbose);
@@ -267,6 +325,9 @@ async function removeAdditionalAgentFiles(projectRoot, verbose, dryRun) {
267
325
  filesRemoved++;
268
326
  }
269
327
  }
328
+ else if (!(await hasRulerGeneratedProvenance(fullPath, projectRoot))) {
329
+ (0, constants_1.logVerbose)(`Preserving additional file without backup or Ruler provenance: ${fullPath}`, verbose);
330
+ }
270
331
  else {
271
332
  if (dryRun) {
272
333
  (0, constants_1.logVerbose)(`${prefix} Would remove additional file: ${fullPath}`, verbose);
@@ -288,7 +349,7 @@ async function removeAdditionalAgentFiles(projectRoot, verbose, dryRun) {
288
349
  const restored = await restoreFromBackup(settingsPath, verbose, dryRun);
289
350
  if (restored) {
290
351
  filesRemoved++;
291
- (0, constants_1.logVerbose)(`${constants_1.actionPrefix} Restored VSCode settings from backup`, verbose);
352
+ (0, constants_1.logVerbose)(`${prefix} Restored VSCode settings from backup`, verbose);
292
353
  }
293
354
  }
294
355
  else if (await fileExists(settingsPath)) {
@@ -299,10 +360,10 @@ async function removeAdditionalAgentFiles(projectRoot, verbose, dryRun) {
299
360
  delete settings['augment.advanced'];
300
361
  const remainingKeys = Object.keys(settings);
301
362
  if (remainingKeys.length === 0) {
302
- (0, constants_1.logVerbose)(`${constants_1.actionPrefix} Would remove empty VSCode settings file`, verbose);
363
+ (0, constants_1.logVerbose)(`${prefix} Would remove empty VSCode settings file`, verbose);
303
364
  }
304
365
  else {
305
- (0, constants_1.logVerbose)(`${constants_1.actionPrefix} Would remove augment.advanced section from ${settingsPath}`, verbose);
366
+ (0, constants_1.logVerbose)(`${prefix} Would remove augment.advanced section from ${settingsPath}`, verbose);
306
367
  }
307
368
  filesRemoved++;
308
369
  }
@@ -314,11 +375,11 @@ async function removeAdditionalAgentFiles(projectRoot, verbose, dryRun) {
314
375
  const remainingKeys = Object.keys(settings);
315
376
  if (remainingKeys.length === 0) {
316
377
  await fs_1.promises.unlink(settingsPath);
317
- (0, constants_1.logVerbose)(`${constants_1.actionPrefix} Removed empty VSCode settings file`, verbose);
378
+ (0, constants_1.logVerbose)(`${prefix} Removed empty VSCode settings file`, verbose);
318
379
  }
319
380
  else {
320
381
  await (0, settings_1.writeVSCodeSettings)(settingsPath, settings);
321
- (0, constants_1.logVerbose)(`${constants_1.actionPrefix} Removed augment.advanced section from VSCode settings`, verbose);
382
+ (0, constants_1.logVerbose)(`${prefix} Removed augment.advanced section from VSCode settings`, verbose);
322
383
  }
323
384
  filesRemoved++;
324
385
  }
@@ -363,7 +424,7 @@ async function revertAgentConfiguration(agent, projectRoot, agentConfig, keepBac
363
424
  }
364
425
  }
365
426
  else {
366
- const removed = await removeGeneratedFile(outputPath, verbose, dryRun);
427
+ const removed = await removeGeneratedFile(outputPath, verbose, dryRun, projectRoot);
367
428
  if (removed) {
368
429
  result.removed++;
369
430
  }
@@ -371,7 +432,7 @@ async function revertAgentConfiguration(agent, projectRoot, agentConfig, keepBac
371
432
  }
372
433
  // Handle MCP files
373
434
  const mcpPath = await (0, mcp_1.getNativeMcpPath)(agent.getName(), projectRoot);
374
- if (mcpPath && mcpPath.startsWith(projectRoot)) {
435
+ if (mcpPath && (0, path_utils_1.isPathInsideOrEqual)(projectRoot, mcpPath)) {
375
436
  if (agent.getName() === 'AugmentCode' &&
376
437
  mcpPath.endsWith('.vscode/settings.json')) {
377
438
  (0, constants_1.logVerbose)(`Skipping MCP handling for AugmentCode settings.json - handled separately`, verbose);
@@ -388,7 +449,7 @@ async function revertAgentConfiguration(agent, projectRoot, agentConfig, keepBac
388
449
  }
389
450
  }
390
451
  else {
391
- const mcpRemoved = await removeGeneratedFile(mcpPath, verbose, dryRun);
452
+ const mcpRemoved = await removeGeneratedFile(mcpPath, verbose, dryRun, projectRoot);
392
453
  if (mcpRemoved) {
393
454
  result.removed++;
394
455
  }
package/dist/lib.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ import { allAgents } from './agents';
2
+ import { McpStrategy } from './types';
3
+ export { allAgents };
4
+ /**
5
+ * Applies ruler configurations for all supported AI agents.
6
+ * @param projectRoot Root directory of the project
7
+ */
8
+ /**
9
+ * Applies ruler configurations for selected AI agents.
10
+ * @param projectRoot Root directory of the project
11
+ * @param includedAgents Optional list of agent name filters (case-insensitive substrings)
12
+ */
13
+ export declare function applyAllAgentConfigs(projectRoot: string, includedAgents?: string[], configPath?: string, cliMcpEnabled?: boolean, cliMcpStrategy?: McpStrategy, cliGitignoreEnabled?: boolean, verbose?: boolean, dryRun?: boolean, localOnly?: boolean, nested?: boolean, backup?: boolean, skillsEnabled?: boolean, cliGitignoreLocal?: boolean, subagentsEnabled?: boolean): Promise<void>;
package/dist/lib.js CHANGED
@@ -53,6 +53,17 @@ function resolveSkillsEnabled(cliFlag, configSetting) {
53
53
  ? configSetting
54
54
  : true; // default to enabled
55
55
  }
56
+ /**
57
+ * Resolves backup enabled state based on precedence:
58
+ * CLI flag > ruler.toml > default (enabled).
59
+ */
60
+ function resolveBackupEnabled(cliFlag, configSetting) {
61
+ return cliFlag !== undefined
62
+ ? cliFlag
63
+ : configSetting !== undefined
64
+ ? configSetting
65
+ : true; // default to enabled
66
+ }
56
67
  /**
57
68
  * Resolves subagents enabled state based on precedence:
58
69
  * CLI flag > ruler.toml > default (disabled).
@@ -82,7 +93,7 @@ function resolveSubagentsCleanupOrphaned(configSetting) {
82
93
  * @param projectRoot Root directory of the project
83
94
  * @param includedAgents Optional list of agent name filters (case-insensitive substrings)
84
95
  */
85
- async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cliMcpEnabled = true, cliMcpStrategy, cliGitignoreEnabled, verbose = false, dryRun = false, localOnly = false, nested = false, backup = true, skillsEnabled, cliGitignoreLocal, subagentsEnabled) {
96
+ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cliMcpEnabled = true, cliMcpStrategy, cliGitignoreEnabled, verbose = false, dryRun = false, localOnly = false, nested = false, backup, skillsEnabled, cliGitignoreLocal, subagentsEnabled) {
86
97
  // Load configuration and rules
87
98
  (0, constants_1.logVerbose)(`Loading configuration from project root: ${projectRoot}`, verbose);
88
99
  if (configPath) {
@@ -123,6 +134,7 @@ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cli
123
134
  // Propagate subagents (mirrors skills handling for nested mode).
124
135
  const subagentsEnabledResolved = resolveSubagentsEnabled(subagentsEnabled, rootConfig.subagents?.enabled);
125
136
  const subagentsCleanupOrphaned = resolveSubagentsCleanupOrphaned(rootConfig.subagents?.cleanup_orphaned);
137
+ const backupEnabledResolved = resolveBackupEnabled(backup, rootConfig.backup?.enabled);
126
138
  {
127
139
  const { propagateSubagents } = await Promise.resolve().then(() => __importStar(require('./core/SubagentsProcessor')));
128
140
  for (const configEntry of hierarchicalConfigs) {
@@ -131,7 +143,7 @@ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cli
131
143
  await propagateSubagents(nestedRoot, selectedAgents, subagentsEnabledResolved, subagentsCleanupOrphaned, verbose, dryRun);
132
144
  }
133
145
  }
134
- generatedPaths = await (0, apply_engine_1.processHierarchicalConfigurations)(selectedAgents, hierarchicalConfigs, verbose, dryRun, cliMcpEnabled, cliMcpStrategy, backup);
146
+ generatedPaths = await (0, apply_engine_1.processHierarchicalConfigurations)(selectedAgents, hierarchicalConfigs, verbose, dryRun, cliMcpEnabled, cliMcpStrategy, backupEnabledResolved);
135
147
  }
136
148
  else {
137
149
  const singleConfig = await (0, apply_engine_1.loadSingleConfiguration)(projectRoot, configPath, localOnly);
@@ -151,11 +163,12 @@ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cli
151
163
  // Propagate subagents (mirrors skills handling).
152
164
  const subagentsEnabledResolvedSingle = resolveSubagentsEnabled(subagentsEnabled, singleConfig.config.subagents?.enabled);
153
165
  const subagentsCleanupOrphanedSingle = resolveSubagentsCleanupOrphaned(singleConfig.config.subagents?.cleanup_orphaned);
166
+ const backupEnabledResolvedSingle = resolveBackupEnabled(backup, singleConfig.config.backup?.enabled);
154
167
  {
155
168
  const { propagateSubagents } = await Promise.resolve().then(() => __importStar(require('./core/SubagentsProcessor')));
156
169
  await propagateSubagents(projectRoot, selectedAgents, subagentsEnabledResolvedSingle, subagentsCleanupOrphanedSingle, verbose, dryRun);
157
170
  }
158
- generatedPaths = await (0, apply_engine_1.processSingleConfiguration)(selectedAgents, singleConfig, projectRoot, verbose, dryRun, cliMcpEnabled, cliMcpStrategy, backup);
171
+ generatedPaths = await (0, apply_engine_1.processSingleConfiguration)(selectedAgents, singleConfig, projectRoot, verbose, dryRun, cliMcpEnabled, cliMcpStrategy, backupEnabledResolvedSingle);
159
172
  }
160
173
  // Add skills-generated paths to gitignore if skills are enabled
161
174
  let allGeneratedPaths = generatedPaths;
@@ -0,0 +1,20 @@
1
+ import { IAgent } from '../agents/IAgent';
2
+ /**
3
+ * MCP capability types for agents
4
+ */
5
+ export interface McpCapabilities {
6
+ supportsStdio: boolean;
7
+ supportsRemote: boolean;
8
+ }
9
+ /**
10
+ * Derives MCP capabilities for an agent
11
+ */
12
+ export declare function getAgentMcpCapabilities(agent: IAgent): McpCapabilities;
13
+ /**
14
+ * Checks if an agent supports any MCP functionality
15
+ */
16
+ export declare function agentSupportsMcp(agent: IAgent): boolean;
17
+ /**
18
+ * Filters MCP configuration based on agent capabilities
19
+ */
20
+ export declare function filterMcpConfigForAgent(mcpConfig: Record<string, unknown>, agent: IAgent): Record<string, unknown> | null;
@@ -0,0 +1,10 @@
1
+ import { McpStrategy } from '../types';
2
+ /**
3
+ * Merge native and incoming MCP server configurations according to strategy.
4
+ * @param base Existing native MCP config object.
5
+ * @param incoming Ruler MCP config object.
6
+ * @param strategy Merge strategy: 'merge' to union servers, 'overwrite' to replace.
7
+ * @param serverKey The key to use for servers in the output (e.g., 'servers' for Copilot, 'mcpServers' for others).
8
+ * @returns Merged MCP config object.
9
+ */
10
+ export declare function mergeMcp(base: Record<string, unknown>, incoming: Record<string, unknown>, strategy: McpStrategy, serverKey: string): Record<string, unknown>;
package/dist/mcp/merge.js CHANGED
@@ -1,6 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.mergeMcp = mergeMcp;
4
+ const MCP_SERVER_KEYS = [
5
+ 'mcp',
6
+ 'mcpServers',
7
+ 'servers',
8
+ 'mcp_servers',
9
+ 'context_servers',
10
+ ];
4
11
  /**
5
12
  * Merge native and incoming MCP server configurations according to strategy.
6
13
  * @param base Existing native MCP config object.
@@ -17,7 +24,14 @@ function mergeMcp(base, incoming, strategy, serverKey) {
17
24
  incoming.mcpServers ||
18
25
  incoming.mcp ||
19
26
  {};
27
+ const preservedBase = { ...base };
28
+ for (const key of MCP_SERVER_KEYS) {
29
+ if (key !== serverKey) {
30
+ delete preservedBase[key];
31
+ }
32
+ }
20
33
  return {
34
+ ...preservedBase,
21
35
  [serverKey]: incomingServers,
22
36
  };
23
37
  }
@@ -31,7 +45,11 @@ function mergeMcp(base, incoming, strategy, serverKey) {
31
45
  {};
32
46
  const mergedServers = { ...baseServers, ...incomingServers };
33
47
  const newBase = { ...base };
34
- delete newBase.mcpServers; // Remove old key if present
48
+ for (const key of MCP_SERVER_KEYS) {
49
+ if (key !== serverKey) {
50
+ delete newBase[key];
51
+ }
52
+ }
35
53
  return {
36
54
  ...newBase,
37
55
  [serverKey]: mergedServers,
@@ -0,0 +1,2 @@
1
+ import { McpStrategy } from '../types';
2
+ export declare function propagateMcpToOpenCode(rulerMcpData: Record<string, unknown> | null, openCodeConfigPath: string, backup?: boolean, strategy?: McpStrategy): Promise<void>;
@@ -91,16 +91,26 @@ function transformToOpenCodeFormat(rulerMcp) {
91
91
  mcp: openCodeServers,
92
92
  };
93
93
  }
94
- async function propagateMcpToOpenCode(rulerMcpData, openCodeConfigPath, backup = true) {
94
+ async function propagateMcpToOpenCode(rulerMcpData, openCodeConfigPath, backup = true, strategy = 'merge') {
95
95
  const rulerMcp = rulerMcpData || {};
96
96
  // Read existing OpenCode config if it exists
97
97
  let existingConfig = {};
98
+ let existingContent;
98
99
  try {
99
- const existingContent = await fs.readFile(openCodeConfigPath, 'utf8');
100
- existingConfig = JSON.parse(existingContent);
100
+ existingContent = await fs.readFile(openCodeConfigPath, 'utf8');
101
101
  }
102
- catch {
103
- // File doesn't exist, we'll create it
102
+ catch (error) {
103
+ if (error.code !== 'ENOENT') {
104
+ throw new Error(`Could not read OpenCode config at ${openCodeConfigPath}: ${error instanceof Error ? error.message : String(error)}`);
105
+ }
106
+ }
107
+ if (existingContent !== undefined) {
108
+ try {
109
+ existingConfig = JSON.parse(existingContent);
110
+ }
111
+ catch (error) {
112
+ throw new Error(`Invalid OpenCode config at ${openCodeConfigPath}: ${error instanceof Error ? error.message : String(error)}`);
113
+ }
104
114
  }
105
115
  // Transform ruler MCP to OpenCode format
106
116
  const transformedConfig = transformToOpenCodeFormat(rulerMcp);
@@ -108,10 +118,12 @@ async function propagateMcpToOpenCode(rulerMcpData, openCodeConfigPath, backup =
108
118
  const finalConfig = {
109
119
  ...existingConfig,
110
120
  $schema: transformedConfig.$schema,
111
- mcp: {
112
- ...existingConfig.mcp,
113
- ...transformedConfig.mcp,
114
- },
121
+ mcp: strategy === 'overwrite'
122
+ ? transformedConfig.mcp
123
+ : {
124
+ ...existingConfig.mcp,
125
+ ...transformedConfig.mcp,
126
+ },
115
127
  };
116
128
  await (0, FileSystemUtils_1.ensureDirExists)(path.dirname(openCodeConfigPath));
117
129
  if (backup) {
@@ -0,0 +1,2 @@
1
+ import { McpStrategy } from '../types';
2
+ export declare function propagateMcpToOpenHands(rulerMcpData: Record<string, unknown> | null, openHandsConfigPath: string, backup?: boolean, strategy?: McpStrategy): Promise<void>;
@@ -89,7 +89,7 @@ function normalizeRemoteServerArray(entries) {
89
89
  // All entries are strings, keep as is
90
90
  return entries;
91
91
  }
92
- async function propagateMcpToOpenHands(rulerMcpData, openHandsConfigPath, backup = true) {
92
+ async function propagateMcpToOpenHands(rulerMcpData, openHandsConfigPath, backup = true, strategy = 'merge') {
93
93
  const rulerMcp = rulerMcpData || {};
94
94
  // Always use the legacy Ruler MCP config format as input (top-level "mcpServers" key)
95
95
  const rulerServers = rulerMcp.mcpServers || {};
@@ -100,12 +100,22 @@ async function propagateMcpToOpenHands(rulerMcpData, openHandsConfigPath, backup
100
100
  return;
101
101
  }
102
102
  let config = {};
103
+ let tomlContent;
103
104
  try {
104
- const tomlContent = await fs.readFile(openHandsConfigPath, 'utf8');
105
- config = (0, toml_1.parse)(tomlContent);
105
+ tomlContent = await fs.readFile(openHandsConfigPath, 'utf8');
106
106
  }
107
- catch {
108
- // File doesn't exist, we'll create it.
107
+ catch (error) {
108
+ if (error.code !== 'ENOENT') {
109
+ throw new Error(`Could not read OpenHands config at ${openHandsConfigPath}: ${error instanceof Error ? error.message : String(error)}`);
110
+ }
111
+ }
112
+ if (tomlContent !== undefined) {
113
+ try {
114
+ config = (0, toml_1.parse)(tomlContent);
115
+ }
116
+ catch (error) {
117
+ throw new Error(`Invalid OpenHands config at ${openHandsConfigPath}: ${error instanceof Error ? error.message : String(error)}`);
118
+ }
109
119
  }
110
120
  if (!config.mcp) {
111
121
  config.mcp = {};
@@ -119,18 +129,24 @@ async function propagateMcpToOpenHands(rulerMcpData, openHandsConfigPath, backup
119
129
  if (!config.mcp.shttp_servers) {
120
130
  config.mcp.shttp_servers = [];
121
131
  }
122
- // Build maps for merging existing servers
123
- const existingStdioServers = new Map(config.mcp.stdio_servers.map((s) => [s.name, s]));
132
+ // Build maps for merging existing servers, or start fresh when overwriting.
133
+ const existingStdioServers = new Map(strategy === 'overwrite'
134
+ ? []
135
+ : config.mcp.stdio_servers.map((s) => [s.name, s]));
124
136
  const existingSseServers = new Map();
125
- config.mcp.sse_servers.forEach((entry) => {
126
- const url = typeof entry === 'string' ? entry : entry.url;
127
- existingSseServers.set(url, entry);
128
- });
137
+ if (strategy !== 'overwrite') {
138
+ config.mcp.sse_servers.forEach((entry) => {
139
+ const url = typeof entry === 'string' ? entry : entry.url;
140
+ existingSseServers.set(url, entry);
141
+ });
142
+ }
129
143
  const existingShttpServers = new Map();
130
- config.mcp.shttp_servers.forEach((entry) => {
131
- const url = typeof entry === 'string' ? entry : entry.url;
132
- existingShttpServers.set(url, entry);
133
- });
144
+ if (strategy !== 'overwrite') {
145
+ config.mcp.shttp_servers.forEach((entry) => {
146
+ const url = typeof entry === 'string' ? entry : entry.url;
147
+ existingShttpServers.set(url, entry);
148
+ });
149
+ }
134
150
  for (const [name, serverDef] of Object.entries(rulerServers)) {
135
151
  if (isRulerMcpServer(serverDef)) {
136
152
  if (serverDef.command) {
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Validate the structure of the Ruler MCP JSON config.
3
+ * Minimal validation: ensure 'mcpServers' property exists and is an object.
4
+ * @param data Parsed JSON object from .ruler/mcp.json.
5
+ * @throws Error if validation fails.
6
+ */
7
+ export declare function validateMcp(data: unknown): void;
@@ -8,10 +8,15 @@ exports.validateMcp = validateMcp;
8
8
  * @throws Error if validation fails.
9
9
  */
10
10
  function validateMcp(data) {
11
+ const mcpServers = data && typeof data === 'object'
12
+ ? data.mcpServers
13
+ : undefined;
11
14
  if (!data ||
12
15
  typeof data !== 'object' ||
13
16
  !('mcpServers' in data) ||
14
- typeof data.mcpServers !== 'object') {
17
+ !mcpServers ||
18
+ typeof mcpServers !== 'object' ||
19
+ Array.isArray(mcpServers)) {
15
20
  throw new Error('[ruler] Invalid MCP config: must contain an object property "mcpServers" (Ruler style)');
16
21
  }
17
22
  }
@@ -0,0 +1,8 @@
1
+ /** Determine the native MCP config path for a given agent. */
2
+ export declare function getNativeMcpPath(adapterName: string, projectRoot: string): Promise<string | null>;
3
+ /** Read native MCP config from disk, or return empty object if missing. */
4
+ export declare function readNativeMcp(filePath: string): Promise<Record<string, unknown>>;
5
+ /** Read native Codex TOML MCP config from disk, or return empty object if missing. */
6
+ export declare function readNativeMcpToml(filePath: string, parseToml: (text: string) => Record<string, unknown>): Promise<Record<string, unknown>>;
7
+ /** Write native MCP config to disk, creating parent directories as needed. */
8
+ export declare function writeNativeMcp(filePath: string, data: unknown): Promise<void>;
package/dist/paths/mcp.js CHANGED
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.getNativeMcpPath = getNativeMcpPath;
37
37
  exports.readNativeMcp = readNativeMcp;
38
+ exports.readNativeMcpToml = readNativeMcpToml;
38
39
  exports.writeNativeMcp = writeNativeMcp;
39
40
  const path = __importStar(require("path"));
40
41
  const fs_1 = require("fs");
@@ -111,14 +112,42 @@ async function getNativeMcpPath(adapterName, projectRoot) {
111
112
  // default to first candidate if none exist
112
113
  return candidates.length > 0 ? candidates[0] : null;
113
114
  }
114
- /** Read native MCP config from disk, or return empty object if missing/invalid. */
115
+ /** Read native MCP config from disk, or return empty object if missing. */
115
116
  async function readNativeMcp(filePath) {
117
+ let text;
118
+ try {
119
+ text = await fs_1.promises.readFile(filePath, 'utf8');
120
+ }
121
+ catch (error) {
122
+ if (error.code === 'ENOENT') {
123
+ return {};
124
+ }
125
+ throw new Error(`Could not read MCP config at ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
126
+ }
116
127
  try {
117
- const text = await fs_1.promises.readFile(filePath, 'utf8');
118
128
  return JSON.parse(text);
119
129
  }
120
- catch {
121
- return {};
130
+ catch (error) {
131
+ throw new Error(`Invalid MCP config at ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
132
+ }
133
+ }
134
+ /** Read native Codex TOML MCP config from disk, or return empty object if missing. */
135
+ async function readNativeMcpToml(filePath, parseToml) {
136
+ let text;
137
+ try {
138
+ text = await fs_1.promises.readFile(filePath, 'utf8');
139
+ }
140
+ catch (error) {
141
+ if (error.code === 'ENOENT') {
142
+ return {};
143
+ }
144
+ throw new Error(`Could not read MCP config at ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
145
+ }
146
+ try {
147
+ return parseToml(text);
148
+ }
149
+ catch (error) {
150
+ throw new Error(`Invalid MCP config at ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
122
151
  }
123
152
  }
124
153
  /** Write native MCP config to disk, creating parent directories as needed. */
@@ -0,0 +1,6 @@
1
+ import { allAgents } from './agents';
2
+ export { allAgents };
3
+ /**
4
+ * Reverts ruler configurations for selected AI agents.
5
+ */
6
+ export declare function revertAllAgentConfigs(projectRoot: string, includedAgents?: string[], configPath?: string, keepBackups?: boolean, verbose?: boolean, dryRun?: boolean, localOnly?: boolean): Promise<void>;