@intellectronica/ruler 0.3.40 → 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 (95) hide show
  1. package/README.md +59 -46
  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 +2 -3
  47. package/dist/cli/handlers.d.ts +41 -0
  48. package/dist/cli/handlers.js +76 -60
  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 +123 -41
  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/SubagentsProcessor.js +68 -22
  63. package/dist/core/SubagentsUtils.d.ts +34 -0
  64. package/dist/core/UnifiedConfigLoader.d.ts +10 -0
  65. package/dist/core/UnifiedConfigLoader.js +61 -31
  66. package/dist/core/UnifiedConfigTypes.d.ts +95 -0
  67. package/dist/core/agent-selection.d.ts +12 -0
  68. package/dist/core/agent-selection.js +11 -3
  69. package/dist/core/apply-engine.d.ts +69 -0
  70. package/dist/core/apply-engine.js +57 -50
  71. package/dist/core/config-utils.d.ts +14 -0
  72. package/dist/core/config-utils.js +9 -3
  73. package/dist/core/hash.d.ts +2 -0
  74. package/dist/core/path-utils.d.ts +1 -0
  75. package/dist/core/path-utils.js +42 -0
  76. package/dist/core/revert-engine.d.ts +36 -0
  77. package/dist/core/revert-engine.js +70 -9
  78. package/dist/lib.d.ts +13 -0
  79. package/dist/lib.js +23 -5
  80. package/dist/mcp/capabilities.d.ts +20 -0
  81. package/dist/mcp/merge.d.ts +10 -0
  82. package/dist/mcp/merge.js +19 -1
  83. package/dist/mcp/propagateOpenCodeMcp.d.ts +2 -0
  84. package/dist/mcp/propagateOpenCodeMcp.js +21 -9
  85. package/dist/mcp/propagateOpenHandsMcp.d.ts +2 -0
  86. package/dist/mcp/propagateOpenHandsMcp.js +31 -15
  87. package/dist/mcp/validate.d.ts +7 -0
  88. package/dist/mcp/validate.js +6 -1
  89. package/dist/paths/mcp.d.ts +8 -0
  90. package/dist/paths/mcp.js +33 -4
  91. package/dist/revert.d.ts +6 -0
  92. package/dist/revert.js +39 -27
  93. package/dist/types.d.ts +87 -0
  94. package/dist/vscode/settings.d.ts +40 -0
  95. package/package.json +7 -4
@@ -52,6 +52,7 @@ const propagateOpenHandsMcp_1 = require("../mcp/propagateOpenHandsMcp");
52
52
  const propagateOpenCodeMcp_1 = require("../mcp/propagateOpenCodeMcp");
53
53
  const agent_utils_1 = require("../agents/agent-utils");
54
54
  const capabilities_1 = require("../mcp/capabilities");
55
+ const path_utils_1 = require("./path-utils");
55
56
  const constants_1 = require("../constants");
56
57
  async function loadNestedConfigurations(projectRoot, configPath, localOnly, resolvedNested) {
57
58
  const { dirs: rulerDirs } = await findRulerDirectories(projectRoot, localOnly, true);
@@ -59,11 +60,11 @@ async function loadNestedConfigurations(projectRoot, configPath, localOnly, reso
59
60
  // Load config first so we know whether `.ruler/agents/` should be included
60
61
  // in the rule concatenation for each directory.
61
62
  for (const rulerDir of rulerDirs) {
62
- const config = await loadConfigForRulerDir(rulerDir, configPath, resolvedNested);
63
+ const config = await loadConfigForRulerDir(rulerDir, configPath, resolvedNested, localOnly);
63
64
  const files = await FileSystemUtils.readMarkdownFiles(rulerDir, {
64
65
  includeAgents: shouldIncludeAgentsInRules(config),
65
66
  });
66
- results.push(await createHierarchicalConfiguration(rulerDir, files, config, configPath));
67
+ results.push(await createHierarchicalConfiguration(rulerDir, files, config, configPath, localOnly));
67
68
  }
68
69
  return results;
69
70
  }
@@ -74,7 +75,7 @@ async function loadNestedConfigurations(projectRoot, configPath, localOnly, reso
74
75
  function shouldIncludeAgentsInRules(config) {
75
76
  return config.subagents?.include_in_rules === true;
76
77
  }
77
- async function createHierarchicalConfiguration(rulerDir, files, config, cliConfigPath) {
78
+ async function createHierarchicalConfiguration(rulerDir, files, config, cliConfigPath, localOnly) {
78
79
  await warnAboutLegacyMcpJson(rulerDir);
79
80
  const concatenatedRules = (0, RuleProcessor_1.concatenateRules)(files, path.dirname(rulerDir));
80
81
  const directoryRoot = path.dirname(rulerDir);
@@ -91,6 +92,7 @@ async function createHierarchicalConfiguration(rulerDir, files, config, cliConfi
91
92
  const unifiedConfig = await loadUnifiedConfig({
92
93
  projectRoot: directoryRoot,
93
94
  configPath: configPathToUse,
95
+ checkGlobal: !localOnly,
94
96
  });
95
97
  let rulerMcpJson = null;
96
98
  if (unifiedConfig.mcp && Object.keys(unifiedConfig.mcp.servers).length > 0) {
@@ -105,7 +107,7 @@ async function createHierarchicalConfiguration(rulerDir, files, config, cliConfi
105
107
  rulerMcpJson,
106
108
  };
107
109
  }
108
- async function loadConfigForRulerDir(rulerDir, cliConfigPath, resolvedNested) {
110
+ async function loadConfigForRulerDir(rulerDir, cliConfigPath, resolvedNested, localOnly) {
109
111
  const directoryRoot = path.dirname(rulerDir);
110
112
  const localConfigPath = path.join(rulerDir, 'ruler.toml');
111
113
  let hasLocalConfig = false;
@@ -119,6 +121,7 @@ async function loadConfigForRulerDir(rulerDir, cliConfigPath, resolvedNested) {
119
121
  const loaded = await (0, ConfigLoader_1.loadConfig)({
120
122
  projectRoot: directoryRoot,
121
123
  configPath: hasLocalConfig ? localConfigPath : cliConfigPath,
124
+ checkGlobal: !localOnly,
122
125
  });
123
126
  const cloned = cloneLoadedConfig(loaded);
124
127
  if (resolvedNested) {
@@ -144,6 +147,7 @@ function cloneLoadedConfig(config) {
144
147
  cliAgents: config.cliAgents ? [...config.cliAgents] : undefined,
145
148
  mcp: config.mcp ? { ...config.mcp } : undefined,
146
149
  gitignore: config.gitignore ? { ...config.gitignore } : undefined,
150
+ backup: config.backup ? { ...config.backup } : undefined,
147
151
  skills: config.skills ? { ...config.skills } : undefined,
148
152
  subagents: config.subagents ? { ...config.subagents } : undefined,
149
153
  nested: config.nested,
@@ -156,18 +160,10 @@ function cloneLoadedConfig(config) {
156
160
  async function findRulerDirectories(projectRoot, localOnly, hierarchical) {
157
161
  if (hierarchical) {
158
162
  const dirs = await FileSystemUtils.findAllRulerDirs(projectRoot);
159
- const allDirs = [...dirs];
160
- // Add global config if not local-only
161
- if (!localOnly) {
162
- const globalDir = await FileSystemUtils.findGlobalRulerDir();
163
- if (globalDir) {
164
- allDirs.push(globalDir);
165
- }
166
- }
167
- if (allDirs.length === 0) {
163
+ if (dirs.length === 0) {
168
164
  throw (0, constants_1.createRulerError)(`.ruler directory not found`, `Searched from: ${projectRoot}`);
169
165
  }
170
- return { dirs: allDirs, primaryDir: allDirs[0] };
166
+ return { dirs, primaryDir: dirs[0] };
171
167
  }
172
168
  else {
173
169
  const dir = await FileSystemUtils.findRulerDir(projectRoot, !localOnly);
@@ -202,6 +198,7 @@ async function loadSingleConfiguration(projectRoot, configPath, localOnly) {
202
198
  const config = await (0, ConfigLoader_1.loadConfig)({
203
199
  projectRoot,
204
200
  configPath,
201
+ checkGlobal: !localOnly,
205
202
  });
206
203
  // Read rule files. `.ruler/agents/` is only included when
207
204
  // `[agents] include_in_rules = true`.
@@ -212,7 +209,11 @@ async function loadSingleConfiguration(projectRoot, configPath, localOnly) {
212
209
  const concatenatedRules = (0, RuleProcessor_1.concatenateRules)(files, path.dirname(primaryDir));
213
210
  // Load unified config to get merged MCP configuration
214
211
  const { loadUnifiedConfig } = await Promise.resolve().then(() => __importStar(require('./UnifiedConfigLoader')));
215
- const unifiedConfig = await loadUnifiedConfig({ projectRoot, configPath });
212
+ const unifiedConfig = await loadUnifiedConfig({
213
+ projectRoot,
214
+ configPath,
215
+ checkGlobal: !localOnly,
216
+ });
216
217
  // Synthesize rulerMcpJson from unified MCP bundle for backward compatibility
217
218
  let rulerMcpJson = null;
218
219
  if (unifiedConfig.mcp && Object.keys(unifiedConfig.mcp.servers).length > 0) {
@@ -306,22 +307,27 @@ async function applyConfigurationsToAgents(agents, concatenatedRules, rulerMcpJs
306
307
  agentsMdWritten = true;
307
308
  }
308
309
  }
309
- let finalAgentConfig = agentConfig;
310
- if (agent.getIdentifier() === 'augmentcode' && agentRulerMcpJson) {
311
- const resolvedStrategy = cliMcpStrategy ??
312
- agentConfig?.mcp?.strategy ??
313
- config.mcp?.strategy ??
314
- 'merge';
315
- finalAgentConfig = {
316
- ...agentConfig,
317
- mcp: {
318
- ...agentConfig?.mcp,
319
- strategy: resolvedStrategy,
320
- },
321
- };
322
- }
310
+ const effectiveMcpEnabled = cliMcpEnabled &&
311
+ (agentConfig?.mcp?.enabled ?? config.mcp?.enabled ?? true);
312
+ const effectiveMcpStrategy = cliMcpStrategy ??
313
+ agentConfig?.mcp?.strategy ??
314
+ config.mcp?.strategy ??
315
+ 'merge';
316
+ const finalAgentConfig = {
317
+ ...agentConfig,
318
+ mcp: {
319
+ ...agentConfig?.mcp,
320
+ enabled: effectiveMcpEnabled,
321
+ strategy: effectiveMcpStrategy,
322
+ },
323
+ };
324
+ const mcpForAgentApply = shouldUseEngineManagedMcp(agent)
325
+ ? null
326
+ : effectiveMcpEnabled
327
+ ? agentRulerMcpJson
328
+ : null;
323
329
  if (!skipApplyForThisAgent) {
324
- await agent.applyRulerConfig(concatenatedRules, projectRoot, agentRulerMcpJson, finalAgentConfig, backup);
330
+ await agent.applyRulerConfig(concatenatedRules, projectRoot, mcpForAgentApply, finalAgentConfig, backup);
325
331
  }
326
332
  }
327
333
  // Handle MCP configuration
@@ -334,7 +340,7 @@ async function handleMcpConfiguration(agent, agentConfig, config, rulerMcpJson,
334
340
  (0, constants_1.logVerbose)(`Agent ${agent.getName()} does not support MCP - skipping MCP configuration`, verbose);
335
341
  return;
336
342
  }
337
- const dest = await (0, mcp_1.getNativeMcpPath)(agent.getName(), projectRoot);
343
+ const dest = await resolveMcpDestination(agent, agentConfig, projectRoot);
338
344
  const mcpEnabledForAgent = cliMcpEnabled && (agentConfig?.mcp?.enabled ?? config.mcp?.enabled ?? true);
339
345
  if (!dest || !mcpEnabledForAgent) {
340
346
  return;
@@ -349,8 +355,17 @@ async function handleMcpConfiguration(agent, agentConfig, config, rulerMcpJson,
349
355
  await updateGitignoreForMcpFile(dest, projectRoot, generatedPaths, backup);
350
356
  await applyMcpConfiguration(agent, filteredMcpJson, dest, agentConfig, config, projectRoot, cliMcpStrategy, dryRun, verbose, backup);
351
357
  }
358
+ function shouldUseEngineManagedMcp(agent) {
359
+ return (agent.getIdentifier() === 'codex' || agent.getIdentifier() === 'opencode');
360
+ }
361
+ async function resolveMcpDestination(agent, agentConfig, projectRoot) {
362
+ if (agentConfig?.outputPathConfig) {
363
+ return path.resolve(projectRoot, agentConfig.outputPathConfig);
364
+ }
365
+ return await (0, mcp_1.getNativeMcpPath)(agent.getName(), projectRoot);
366
+ }
352
367
  async function updateGitignoreForMcpFile(dest, projectRoot, generatedPaths, backup = true) {
353
- if (dest.startsWith(projectRoot)) {
368
+ if ((0, path_utils_1.isPathInsideOrEqual)(projectRoot, dest)) {
354
369
  const relativeDest = path.relative(projectRoot, dest);
355
370
  generatedPaths.push(relativeDest);
356
371
  if (backup) {
@@ -391,16 +406,16 @@ function sanitizeMcpTimeoutsForAgent(agent, mcpJson, dryRun) {
391
406
  }
392
407
  async function applyMcpConfiguration(agent, filteredMcpJson, dest, agentConfig, config, projectRoot, cliMcpStrategy, dryRun, verbose, backup = true) {
393
408
  // Prevent writing MCP configs outside the project root (e.g., legacy home-directory targets)
394
- if (!dest.startsWith(projectRoot)) {
409
+ if (!(0, path_utils_1.isPathInsideOrEqual)(projectRoot, dest)) {
395
410
  (0, constants_1.logVerbose)(`Skipping MCP config for ${agent.getName()} because target path is outside project: ${dest}`, verbose);
396
411
  return;
397
412
  }
398
413
  const agentMcpJson = sanitizeMcpTimeoutsForAgent(agent, filteredMcpJson, dryRun);
399
414
  if (agent.getIdentifier() === 'openhands') {
400
- return await applyOpenHandsMcpConfiguration(agentMcpJson, dest, dryRun, verbose, backup);
415
+ return await applyOpenHandsMcpConfiguration(agentMcpJson, dest, cliMcpStrategy ?? agentConfig?.mcp?.strategy ?? config.mcp?.strategy, dryRun, verbose, backup);
401
416
  }
402
417
  if (agent.getIdentifier() === 'opencode') {
403
- return await applyOpenCodeMcpConfiguration(agentMcpJson, dest, dryRun, verbose, backup);
418
+ return await applyOpenCodeMcpConfiguration(agentMcpJson, dest, cliMcpStrategy ?? agentConfig?.mcp?.strategy ?? config.mcp?.strategy, dryRun, verbose, backup);
404
419
  }
405
420
  // Agents that handle MCP configuration internally should not have external MCP handling
406
421
  if (agent.getIdentifier() === 'zed' ||
@@ -412,20 +427,20 @@ async function applyMcpConfiguration(agent, filteredMcpJson, dest, agentConfig,
412
427
  }
413
428
  return await applyStandardMcpConfiguration(agent, agentMcpJson, dest, agentConfig, config, cliMcpStrategy, dryRun, verbose, backup);
414
429
  }
415
- async function applyOpenHandsMcpConfiguration(filteredMcpJson, dest, dryRun, verbose, backup = true) {
430
+ async function applyOpenHandsMcpConfiguration(filteredMcpJson, dest, strategy, dryRun, verbose, backup = true) {
416
431
  if (dryRun) {
417
432
  (0, constants_1.logVerbose)(`DRY RUN: Would apply MCP config by updating TOML file: ${dest}`, verbose);
418
433
  }
419
434
  else {
420
- await (0, propagateOpenHandsMcp_1.propagateMcpToOpenHands)(filteredMcpJson, dest, backup);
435
+ await (0, propagateOpenHandsMcp_1.propagateMcpToOpenHands)(filteredMcpJson, dest, backup, strategy);
421
436
  }
422
437
  }
423
- async function applyOpenCodeMcpConfiguration(filteredMcpJson, dest, dryRun, verbose, backup = true) {
438
+ async function applyOpenCodeMcpConfiguration(filteredMcpJson, dest, strategy, dryRun, verbose, backup = true) {
424
439
  if (dryRun) {
425
440
  (0, constants_1.logVerbose)(`DRY RUN: Would apply MCP config by updating OpenCode config file: ${dest}`, verbose);
426
441
  }
427
442
  else {
428
- await (0, propagateOpenCodeMcp_1.propagateMcpToOpenCode)(filteredMcpJson, dest, backup);
443
+ await (0, propagateOpenCodeMcp_1.propagateMcpToOpenCode)(filteredMcpJson, dest, backup, strategy);
429
444
  }
430
445
  }
431
446
  /**
@@ -550,17 +565,9 @@ async function applyStandardMcpConfiguration(agent, filteredMcpJson, dest, agent
550
565
  }
551
566
  const CODEX_AGENT_ID = 'codex';
552
567
  const isCodexToml = agent.getIdentifier() === CODEX_AGENT_ID && dest.endsWith('.toml');
553
- let existing = await (0, mcp_1.readNativeMcp)(dest);
554
- if (isCodexToml) {
555
- try {
556
- const tomlContent = await fs_1.promises.readFile(dest, 'utf8');
557
- existing = (0, toml_1.parse)(tomlContent);
558
- }
559
- catch (error) {
560
- (0, constants_1.logVerbose)(`Failed to read Codex MCP TOML at ${dest}: ${error.message}`, verbose);
561
- // ignore missing or invalid TOML, fall back to previously read value
562
- }
563
- }
568
+ const existing = isCodexToml
569
+ ? await (0, mcp_1.readNativeMcpToml)(dest, (text) => (0, toml_1.parse)(text))
570
+ : await (0, mcp_1.readNativeMcp)(dest);
564
571
  let merged = (0, merge_1.mergeMcp)(existing, mcpToMerge, strategy, serverKey);
565
572
  if (isCodexToml) {
566
573
  const { [serverKey]: servers, ...rest } = merged;
@@ -0,0 +1,14 @@
1
+ import { IAgent, IAgentConfig } from '../agents/IAgent';
2
+ /**
3
+ * Maps raw agent configuration keys to their corresponding agent identifiers.
4
+ *
5
+ * This function normalizes configuration keys by matching them against agent identifiers
6
+ * and display names. It performs both exact matching (case-insensitive) with agent
7
+ * identifiers and substring matching (case-insensitive) with agent display names
8
+ * for backwards compatibility.
9
+ *
10
+ * @param raw Raw agent configurations with user-provided keys
11
+ * @param agents Array of all available agents
12
+ * @returns Record with agent identifiers as keys and their configurations as values
13
+ */
14
+ export declare function mapRawAgentConfigs(raw: Record<string, IAgentConfig>, agents: IAgent[]): Record<string, IAgentConfig>;
@@ -17,11 +17,17 @@ function mapRawAgentConfigs(raw, agents) {
17
17
  const mappedConfigs = {};
18
18
  for (const [key, cfg] of Object.entries(raw)) {
19
19
  const lowerKey = key.toLowerCase();
20
+ const exactMatches = agents.filter((agent) => agent.getIdentifier().toLowerCase() === lowerKey);
21
+ // Exact identifier matches take precedence over fuzzy display-name matching.
22
+ if (exactMatches.length > 0) {
23
+ for (const agent of exactMatches) {
24
+ mappedConfigs[agent.getIdentifier()] = cfg;
25
+ }
26
+ continue;
27
+ }
20
28
  for (const agent of agents) {
21
29
  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)) {
30
+ if (agent.getName().toLowerCase().includes(lowerKey)) {
25
31
  mappedConfigs[identifier] = cfg;
26
32
  }
27
33
  }
@@ -0,0 +1,2 @@
1
+ export declare function sha256(data: string): string;
2
+ export declare function stableJson(value: unknown): string;
@@ -0,0 +1 @@
1
+ export declare function isPathInsideOrEqual(parentPath: string, targetPath: string): boolean;
@@ -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).
@@ -70,6 +81,9 @@ function resolveSubagentsEnabled(cliFlag, configSetting) {
70
81
  ? configSetting
71
82
  : false; // default to disabled — see spec: subagents must opt in
72
83
  }
84
+ function resolveSubagentsCleanupOrphaned(configSetting) {
85
+ return configSetting === true;
86
+ }
73
87
  /**
74
88
  * Applies ruler configurations for all supported AI agents.
75
89
  * @param projectRoot Root directory of the project
@@ -79,7 +93,7 @@ function resolveSubagentsEnabled(cliFlag, configSetting) {
79
93
  * @param projectRoot Root directory of the project
80
94
  * @param includedAgents Optional list of agent name filters (case-insensitive substrings)
81
95
  */
82
- 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) {
83
97
  // Load configuration and rules
84
98
  (0, constants_1.logVerbose)(`Loading configuration from project root: ${projectRoot}`, verbose);
85
99
  if (configPath) {
@@ -119,15 +133,17 @@ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cli
119
133
  }
120
134
  // Propagate subagents (mirrors skills handling for nested mode).
121
135
  const subagentsEnabledResolved = resolveSubagentsEnabled(subagentsEnabled, rootConfig.subagents?.enabled);
136
+ const subagentsCleanupOrphaned = resolveSubagentsCleanupOrphaned(rootConfig.subagents?.cleanup_orphaned);
137
+ const backupEnabledResolved = resolveBackupEnabled(backup, rootConfig.backup?.enabled);
122
138
  {
123
139
  const { propagateSubagents } = await Promise.resolve().then(() => __importStar(require('./core/SubagentsProcessor')));
124
140
  for (const configEntry of hierarchicalConfigs) {
125
141
  const nestedRoot = path.dirname(configEntry.rulerDir);
126
142
  (0, constants_1.logVerbose)(`Propagating subagents for nested directory: ${nestedRoot}`, verbose);
127
- await propagateSubagents(nestedRoot, selectedAgents, subagentsEnabledResolved, verbose, dryRun);
143
+ await propagateSubagents(nestedRoot, selectedAgents, subagentsEnabledResolved, subagentsCleanupOrphaned, verbose, dryRun);
128
144
  }
129
145
  }
130
- 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);
131
147
  }
132
148
  else {
133
149
  const singleConfig = await (0, apply_engine_1.loadSingleConfiguration)(projectRoot, configPath, localOnly);
@@ -146,11 +162,13 @@ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cli
146
162
  }
147
163
  // Propagate subagents (mirrors skills handling).
148
164
  const subagentsEnabledResolvedSingle = resolveSubagentsEnabled(subagentsEnabled, singleConfig.config.subagents?.enabled);
165
+ const subagentsCleanupOrphanedSingle = resolveSubagentsCleanupOrphaned(singleConfig.config.subagents?.cleanup_orphaned);
166
+ const backupEnabledResolvedSingle = resolveBackupEnabled(backup, singleConfig.config.backup?.enabled);
149
167
  {
150
168
  const { propagateSubagents } = await Promise.resolve().then(() => __importStar(require('./core/SubagentsProcessor')));
151
- await propagateSubagents(projectRoot, selectedAgents, subagentsEnabledResolvedSingle, verbose, dryRun);
169
+ await propagateSubagents(projectRoot, selectedAgents, subagentsEnabledResolvedSingle, subagentsCleanupOrphanedSingle, verbose, dryRun);
152
170
  }
153
- 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);
154
172
  }
155
173
  // Add skills-generated paths to gitignore if skills are enabled
156
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>;