@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
@@ -42,8 +42,7 @@ const RuleProcessor_1 = require("./RuleProcessor");
42
42
  const FileSystemUtils = __importStar(require("./FileSystemUtils"));
43
43
  async function loadUnifiedConfig(options) {
44
44
  // Resolve the effective .ruler directory (local or global), mirroring the main loader behavior
45
- const resolvedRulerDir = (await FileSystemUtils.findRulerDir(options.projectRoot, true)) ||
46
- path.join(options.projectRoot, '.ruler');
45
+ const resolvedRulerDir = (await FileSystemUtils.findRulerDir(options.projectRoot, options.checkGlobal ?? true)) || path.join(options.projectRoot, '.ruler');
47
46
  const meta = {
48
47
  projectRoot: options.projectRoot,
49
48
  rulerDir: resolvedRulerDir,
@@ -62,11 +61,14 @@ async function loadUnifiedConfig(options) {
62
61
  meta.configFile = tomlFile;
63
62
  }
64
63
  catch (err) {
65
- if (err.code !== 'ENOENT') {
64
+ if (options.configPath ||
65
+ err.code !== 'ENOENT') {
66
66
  diagnostics.push({
67
- severity: 'warning',
67
+ severity: options.configPath ? 'error' : 'warning',
68
68
  code: 'TOML_READ_ERROR',
69
- message: 'Failed to read ruler.toml',
69
+ message: options.configPath
70
+ ? 'Failed to read explicit config file'
71
+ : 'Failed to read ruler.toml',
70
72
  file: tomlFile,
71
73
  detail: err.message,
72
74
  });
@@ -104,37 +106,41 @@ async function loadUnifiedConfig(options) {
104
106
  nested,
105
107
  skills: skillsConfig,
106
108
  };
109
+ const includeAgentsInRules = (() => {
110
+ if (!tomlRaw || typeof tomlRaw !== 'object')
111
+ return false;
112
+ const raw = tomlRaw;
113
+ const agents = raw.agents;
114
+ const subagents = raw.subagents;
115
+ return ((agents &&
116
+ typeof agents === 'object' &&
117
+ agents.include_in_rules === true) ||
118
+ (subagents &&
119
+ typeof subagents === 'object' &&
120
+ subagents.include_in_rules === true));
121
+ })();
107
122
  // Collect rule markdown files
108
123
  let ruleFiles = [];
109
124
  try {
110
- const dirEntries = await fs_1.promises.readdir(meta.rulerDir, { withFileTypes: true });
111
- const mdFiles = dirEntries
112
- .filter((e) => e.isFile() && e.name.toLowerCase().endsWith('.md'))
113
- .map((e) => path.join(meta.rulerDir, e.name));
114
- // Sort lexicographically then ensure AGENTS.md first
115
- mdFiles.sort((a, b) => a.localeCompare(b));
116
- mdFiles.sort((a, b) => {
117
- const aIs = /agents\.md$/i.test(a);
118
- const bIs = /agents\.md$/i.test(b);
119
- if (aIs && !bIs)
120
- return -1;
121
- if (bIs && !aIs)
122
- return 1;
123
- return 0;
125
+ const mdFiles = await FileSystemUtils.readMarkdownFiles(meta.rulerDir, {
126
+ includeAgents: includeAgentsInRules,
124
127
  });
125
128
  let order = 0;
126
129
  ruleFiles = await Promise.all(mdFiles.map(async (file) => {
127
- const content = await fs_1.promises.readFile(file, 'utf8');
128
- const stat = await fs_1.promises.stat(file);
130
+ const stat = await fs_1.promises.stat(file.path);
131
+ const relativeFromRuler = path.relative(meta.rulerDir, file.path);
132
+ const relativePath = relativeFromRuler.startsWith('..')
133
+ ? path.relative(path.dirname(meta.rulerDir), file.path)
134
+ : relativeFromRuler;
129
135
  return {
130
- path: file,
131
- relativePath: path.basename(file),
132
- content,
133
- contentHash: (0, hash_1.sha256)(content),
136
+ path: file.path,
137
+ relativePath: relativePath.replace(/\\/g, '/'),
138
+ content: file.content,
139
+ contentHash: (0, hash_1.sha256)(file.content),
134
140
  mtimeMs: stat.mtimeMs,
135
141
  size: stat.size,
136
142
  order: order++,
137
- primary: /agents\.md$/i.test(file),
143
+ primary: /(^|[/\\])agents\.md$/i.test(relativePath),
138
144
  };
139
145
  }));
140
146
  }
@@ -221,6 +227,11 @@ async function loadUnifiedConfig(options) {
221
227
  file: tomlFile,
222
228
  });
223
229
  }
230
+ if (hasCommand && hasUrl) {
231
+ delete server.command;
232
+ delete server.args;
233
+ delete server.env;
234
+ }
224
235
  // Derive type - remote takes precedence if both are present
225
236
  if (server.url) {
226
237
  server.type = 'remote';
@@ -282,13 +293,32 @@ async function loadUnifiedConfig(options) {
282
293
  }
283
294
  }
284
295
  const parsedObj = parsed;
285
- const serversRaw = parsedObj.mcpServers ||
286
- parsedObj.servers ||
287
- {};
288
- if (serversRaw && typeof serversRaw === 'object') {
296
+ const serversRaw = 'mcpServers' in parsedObj
297
+ ? parsedObj.mcpServers
298
+ : 'servers' in parsedObj
299
+ ? parsedObj.servers
300
+ : undefined;
301
+ if (!serversRaw ||
302
+ typeof serversRaw !== 'object' ||
303
+ Array.isArray(serversRaw)) {
304
+ diagnostics.push({
305
+ severity: 'warning',
306
+ code: 'MCP_INVALID_SHAPE',
307
+ message: 'mcp.json must contain a non-array object in "mcpServers" or "servers"',
308
+ file: mcpFile,
309
+ });
310
+ }
311
+ else {
289
312
  for (const [name, def] of Object.entries(serversRaw)) {
290
- if (!def || typeof def !== 'object')
313
+ if (!def || typeof def !== 'object' || Array.isArray(def)) {
314
+ diagnostics.push({
315
+ severity: 'warning',
316
+ code: 'MCP_INVALID_SERVER',
317
+ message: `MCP server '${name}' must be an object`,
318
+ file: mcpFile,
319
+ });
291
320
  continue;
321
+ }
292
322
  const server = {};
293
323
  if (typeof def.command === 'string')
294
324
  server.command = def.command;
@@ -0,0 +1,95 @@
1
+ import { McpConfig, GitignoreConfig, SkillsConfig, SubagentsConfig, McpStrategy } from '../types';
2
+ export interface RulerUnifiedConfig {
3
+ meta: ConfigMeta;
4
+ toml: TomlConfig;
5
+ rules: RulesBundle;
6
+ mcp: McpBundle | null;
7
+ agents: Record<string, EffectiveAgentConfig>;
8
+ diagnostics: ConfigDiagnostic[];
9
+ hash: string;
10
+ }
11
+ export interface ConfigMeta {
12
+ projectRoot: string;
13
+ rulerDir: string;
14
+ configFile?: string;
15
+ mcpFile?: string;
16
+ loadedAt: Date;
17
+ version: string;
18
+ }
19
+ export interface TomlConfig {
20
+ raw: unknown;
21
+ schemaVersion: number;
22
+ defaultAgents?: string[];
23
+ agents: Record<string, AgentTomlConfig>;
24
+ mcp?: McpToggleConfig;
25
+ mcpServers?: Record<string, McpServerDef>;
26
+ gitignore?: GitignoreConfig;
27
+ skills?: SkillsConfig;
28
+ subagents?: SubagentsConfig;
29
+ nested?: boolean;
30
+ }
31
+ export type McpToggleConfig = McpConfig;
32
+ export interface AgentTomlConfig {
33
+ enabled?: boolean;
34
+ outputPath?: string;
35
+ outputPathInstructions?: string;
36
+ outputPathConfig?: string;
37
+ mcp?: McpConfig;
38
+ source: AgentConfigSourceMeta;
39
+ }
40
+ export interface AgentConfigSourceMeta {
41
+ sectionPath: string;
42
+ }
43
+ export interface RulesBundle {
44
+ files: RuleFile[];
45
+ concatenated: string;
46
+ concatenatedHash: string;
47
+ }
48
+ export interface RuleFile {
49
+ path: string;
50
+ relativePath: string;
51
+ content: string;
52
+ contentHash: string;
53
+ mtimeMs: number;
54
+ size: number;
55
+ order: number;
56
+ primary: boolean;
57
+ }
58
+ export interface McpBundle {
59
+ servers: Record<string, McpServerDef>;
60
+ raw: Record<string, unknown>;
61
+ hash: string;
62
+ }
63
+ export interface McpServerDef {
64
+ type?: 'stdio' | 'local' | 'remote';
65
+ command?: string;
66
+ args?: string[];
67
+ env?: Record<string, string>;
68
+ url?: string;
69
+ headers?: Record<string, string>;
70
+ timeout?: number;
71
+ }
72
+ export interface EffectiveAgentConfig {
73
+ identifier: string;
74
+ enabled: boolean;
75
+ output: AgentOutputPaths;
76
+ mcp: EffectiveMcpConfig;
77
+ toml?: AgentTomlConfig;
78
+ }
79
+ export interface AgentOutputPaths {
80
+ instructions?: string;
81
+ config?: string;
82
+ generic?: string;
83
+ }
84
+ export interface EffectiveMcpConfig {
85
+ enabled: boolean;
86
+ strategy: McpStrategy;
87
+ }
88
+ export type DiagnosticSeverity = 'info' | 'warning' | 'error';
89
+ export interface ConfigDiagnostic {
90
+ severity: DiagnosticSeverity;
91
+ code: string;
92
+ message: string;
93
+ file?: string;
94
+ detail?: string;
95
+ }
@@ -0,0 +1,12 @@
1
+ import { IAgent } from '../agents/IAgent';
2
+ import { LoadedConfig } from './ConfigLoader';
3
+ export declare function agentMatchesFilter(agent: IAgent, filter: string, validAgentIdentifiers: Set<string>): boolean;
4
+ /**
5
+ * Resolves which agents should be selected based on configuration.
6
+ * Handles precedence: CLI agents > default_agents > per-agent enabled flags > all agents
7
+ *
8
+ * @param config Loaded configuration containing CLI agents, default agents, and per-agent configs
9
+ * @param allAgents Array of all available agents
10
+ * @returns Array of agents that should be processed
11
+ */
12
+ export declare function resolveSelectedAgents(config: LoadedConfig, allAgents: IAgent[]): IAgent[];
@@ -1,7 +1,16 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.agentMatchesFilter = agentMatchesFilter;
3
4
  exports.resolveSelectedAgents = resolveSelectedAgents;
4
5
  const constants_1 = require("../constants");
6
+ function agentMatchesFilter(agent, filter, validAgentIdentifiers) {
7
+ const identifier = agent.getIdentifier().toLowerCase();
8
+ // Exact identifier matches take precedence over fuzzy display-name matching.
9
+ if (validAgentIdentifiers.has(filter)) {
10
+ return identifier === filter;
11
+ }
12
+ return agent.getName().toLowerCase().includes(filter);
13
+ }
5
14
  /**
6
15
  * Resolves which agents should be selected based on configuration.
7
16
  * Handles precedence: CLI agents > default_agents > per-agent enabled flags > all agents
@@ -23,8 +32,7 @@ function resolveSelectedAgents(config, allAgents) {
23
32
  if (invalidAgents.length > 0) {
24
33
  throw (0, constants_1.createRulerError)(`Invalid agent specified: ${invalidAgents.join(', ')}`, `Valid agents are: ${[...validAgentIdentifiers].join(', ')}`);
25
34
  }
26
- selected = allAgents.filter((agent) => filters.some((f) => agent.getIdentifier() === f ||
27
- agent.getName().toLowerCase().includes(f)));
35
+ selected = allAgents.filter((agent) => filters.some((f) => agentMatchesFilter(agent, f, validAgentIdentifiers)));
28
36
  }
29
37
  else if (config.defaultAgents && config.defaultAgents.length > 0) {
30
38
  const defaults = config.defaultAgents.map((n) => n.toLowerCase());
@@ -42,7 +50,7 @@ function resolveSelectedAgents(config, allAgents) {
42
50
  if (override !== undefined) {
43
51
  return override;
44
52
  }
45
- return defaults.some((d) => identifier === d || agent.getName().toLowerCase().includes(d));
53
+ return defaults.some((d) => agentMatchesFilter(agent, d, validAgentIdentifiers));
46
54
  });
47
55
  }
48
56
  else {
@@ -0,0 +1,69 @@
1
+ import { LoadedConfig } from './ConfigLoader';
2
+ import { IAgent } from '../agents/IAgent';
3
+ import { McpStrategy } from '../types';
4
+ /**
5
+ * Configuration data loaded from the ruler setup
6
+ */
7
+ export interface RulerConfiguration {
8
+ config: LoadedConfig;
9
+ concatenatedRules: string;
10
+ rulerMcpJson: Record<string, unknown> | null;
11
+ }
12
+ /**
13
+ * Configuration data for a specific .ruler directory in hierarchical mode
14
+ */
15
+ export interface HierarchicalRulerConfiguration extends RulerConfiguration {
16
+ rulerDir: string;
17
+ }
18
+ export declare function loadNestedConfigurations(projectRoot: string, configPath: string | undefined, localOnly: boolean, resolvedNested: boolean): Promise<HierarchicalRulerConfiguration[]>;
19
+ /**
20
+ * Loads configuration for single-directory mode (existing behavior).
21
+ */
22
+ export declare function loadSingleConfiguration(projectRoot: string, configPath: string | undefined, localOnly: boolean): Promise<RulerConfiguration>;
23
+ /**
24
+ * Processes hierarchical configurations by applying rules to each .ruler directory independently.
25
+ * Each directory gets its own set of rules and generates its own agent files.
26
+ * @param agents Array of agents to process
27
+ * @param configurations Array of hierarchical configurations for each .ruler directory
28
+ * @param verbose Whether to enable verbose logging
29
+ * @param dryRun Whether to perform a dry run
30
+ * @param cliMcpEnabled Whether MCP is enabled via CLI
31
+ * @param cliMcpStrategy MCP strategy from CLI
32
+ * @returns Promise resolving to array of generated file paths
33
+ */
34
+ export declare function processHierarchicalConfigurations(agents: IAgent[], configurations: HierarchicalRulerConfiguration[], verbose: boolean, dryRun: boolean, cliMcpEnabled: boolean, cliMcpStrategy?: McpStrategy, backup?: boolean): Promise<string[]>;
35
+ /**
36
+ * Processes a single configuration by applying rules to all selected agents.
37
+ * All rules are concatenated and applied to generate agent files in the project root.
38
+ * @param agents Array of agents to process
39
+ * @param configuration Single ruler configuration with concatenated rules
40
+ * @param projectRoot Root directory of the project
41
+ * @param verbose Whether to enable verbose logging
42
+ * @param dryRun Whether to perform a dry run
43
+ * @param cliMcpEnabled Whether MCP is enabled via CLI
44
+ * @param cliMcpStrategy MCP strategy from CLI
45
+ * @returns Promise resolving to array of generated file paths
46
+ */
47
+ export declare function processSingleConfiguration(agents: IAgent[], configuration: RulerConfiguration, projectRoot: string, verbose: boolean, dryRun: boolean, cliMcpEnabled: boolean, cliMcpStrategy?: McpStrategy, backup?: boolean): Promise<string[]>;
48
+ /**
49
+ * Applies configurations to the selected agents (internal function).
50
+ * @param agents Array of agents to process
51
+ * @param concatenatedRules Concatenated rule content
52
+ * @param rulerMcpJson MCP configuration JSON
53
+ * @param config Loaded configuration
54
+ * @param projectRoot Root directory of the project
55
+ * @param verbose Whether to enable verbose logging
56
+ * @param dryRun Whether to perform a dry run
57
+ * @returns Promise resolving to array of generated file paths
58
+ */
59
+ export declare function applyConfigurationsToAgents(agents: IAgent[], concatenatedRules: string, rulerMcpJson: Record<string, unknown> | null, config: LoadedConfig, projectRoot: string, verbose: boolean, dryRun: boolean, cliMcpEnabled?: boolean, cliMcpStrategy?: McpStrategy, backup?: boolean): Promise<string[]>;
60
+ /**
61
+ * Updates the .gitignore file with generated paths.
62
+ * @param projectRoot Root directory of the project
63
+ * @param generatedPaths Array of generated file paths
64
+ * @param config Loaded configuration
65
+ * @param cliGitignoreEnabled CLI gitignore setting
66
+ * @param dryRun Whether to perform a dry run
67
+ * @param cliGitignoreLocal CLI toggle for .git/info/exclude usage
68
+ */
69
+ export declare function updateGitignore(projectRoot: string, generatedPaths: string[], config: LoadedConfig, cliGitignoreEnabled: boolean | undefined, dryRun: boolean, cliGitignoreLocal?: boolean): Promise<void>;
@@ -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;