@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
@@ -62,16 +62,13 @@ async function discoverSubagents(projectRoot) {
62
62
  catch {
63
63
  return { subagents: [], warnings: [] };
64
64
  }
65
- const entries = await fs.readdir(dir, { withFileTypes: true });
66
- const mdFiles = entries
67
- .filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
68
- .map((entry) => path.join(dir, entry.name))
69
- .sort();
65
+ const mdFiles = await listMarkdownFilesRecursive(dir);
70
66
  const subagents = [];
71
67
  const warnings = [];
72
68
  for (const filePath of mdFiles) {
73
69
  const info = await (0, SubagentsUtils_1.loadSubagentFile)(filePath);
74
70
  if (info.valid) {
71
+ info.sourceRelativePath = path.relative(dir, filePath);
75
72
  subagents.push(info);
76
73
  }
77
74
  else if (info.error) {
@@ -80,6 +77,22 @@ async function discoverSubagents(projectRoot) {
80
77
  }
81
78
  return { subagents, warnings };
82
79
  }
80
+ async function listMarkdownFilesRecursive(dir) {
81
+ const results = [];
82
+ const entries = await fs.readdir(dir, { withFileTypes: true });
83
+ for (const entry of entries) {
84
+ const fullPath = path.join(dir, entry.name);
85
+ if (entry.isDirectory()) {
86
+ const nested = await listMarkdownFilesRecursive(fullPath);
87
+ results.push(...nested);
88
+ continue;
89
+ }
90
+ if (entry.isFile() && entry.name.endsWith('.md')) {
91
+ results.push(fullPath);
92
+ }
93
+ }
94
+ return results.sort();
95
+ }
83
96
  const SUBAGENT_TARGET_TO_IDENTIFIERS = new Map([
84
97
  ['claude', ['claude']],
85
98
  ['cursor', ['cursor']],
@@ -167,7 +180,9 @@ async function writeAgentsDirectoryAtomic(targetDir, files) {
167
180
  await fs.mkdir(tempDir, { recursive: true });
168
181
  try {
169
182
  for (const { name, content } of files) {
170
- await fs.writeFile(path.join(tempDir, name), content, 'utf8');
183
+ const outputPath = path.join(tempDir, name);
184
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
185
+ await fs.writeFile(outputPath, content, 'utf8');
171
186
  }
172
187
  try {
173
188
  await fs.rm(targetDir, { recursive: true, force: true });
@@ -187,6 +202,19 @@ async function writeAgentsDirectoryAtomic(targetDir, files) {
187
202
  throw error;
188
203
  }
189
204
  }
205
+ function getSourceRelativeMdPath(sub) {
206
+ const fromSource = sub.sourceRelativePath;
207
+ if (typeof fromSource === 'string' &&
208
+ fromSource.length > 0 &&
209
+ !path.isAbsolute(fromSource) &&
210
+ !fromSource.startsWith('..')) {
211
+ return fromSource;
212
+ }
213
+ return `${sub.name}.md`;
214
+ }
215
+ function withExtension(filePath, ext) {
216
+ return filePath.replace(/\.md$/i, ext);
217
+ }
190
218
  function buildClaudeFile(sub) {
191
219
  const fm = sub.frontmatter;
192
220
  const meta = {
@@ -276,10 +304,10 @@ async function propagateSubagentsForClaude(projectRoot, subagents, options) {
276
304
  return [];
277
305
  const targetDir = path.join(projectRoot, constants_1.CLAUDE_SUBAGENTS_PATH);
278
306
  if (options.dryRun) {
279
- return subagents.map((s) => `Write ${path.join(constants_1.CLAUDE_SUBAGENTS_PATH, `${s.name}.md`)}`);
307
+ return subagents.map((s) => `Write ${path.join(constants_1.CLAUDE_SUBAGENTS_PATH, getSourceRelativeMdPath(s))}`);
280
308
  }
281
309
  const files = subagents.map((s) => ({
282
- name: `${s.name}.md`,
310
+ name: getSourceRelativeMdPath(s),
283
311
  content: buildClaudeFile(s),
284
312
  }));
285
313
  await writeAgentsDirectoryAtomic(targetDir, files);
@@ -290,10 +318,10 @@ async function propagateSubagentsForCursor(projectRoot, subagents, options) {
290
318
  return [];
291
319
  const targetDir = path.join(projectRoot, constants_1.CURSOR_SUBAGENTS_PATH);
292
320
  if (options.dryRun) {
293
- return subagents.map((s) => `Write ${path.join(constants_1.CURSOR_SUBAGENTS_PATH, `${s.name}.md`)}`);
321
+ return subagents.map((s) => `Write ${path.join(constants_1.CURSOR_SUBAGENTS_PATH, getSourceRelativeMdPath(s))}`);
294
322
  }
295
323
  const files = subagents.map((s) => ({
296
- name: `${s.name}.md`,
324
+ name: getSourceRelativeMdPath(s),
297
325
  content: buildCursorFile(s),
298
326
  }));
299
327
  await writeAgentsDirectoryAtomic(targetDir, files);
@@ -304,10 +332,10 @@ async function propagateSubagentsForCodex(projectRoot, subagents, options) {
304
332
  return [];
305
333
  const targetDir = path.join(projectRoot, constants_1.CODEX_SUBAGENTS_PATH);
306
334
  if (options.dryRun) {
307
- return subagents.map((s) => `Write ${path.join(constants_1.CODEX_SUBAGENTS_PATH, `${s.name}.toml`)}`);
335
+ return subagents.map((s) => `Write ${path.join(constants_1.CODEX_SUBAGENTS_PATH, withExtension(getSourceRelativeMdPath(s), '.toml'))}`);
308
336
  }
309
337
  const files = subagents.map((s) => ({
310
- name: `${s.name}.toml`,
338
+ name: withExtension(getSourceRelativeMdPath(s), '.toml'),
311
339
  content: buildCodexFile(s),
312
340
  }));
313
341
  await writeAgentsDirectoryAtomic(targetDir, files);
@@ -325,12 +353,12 @@ async function propagateSubagentsForCopilot(projectRoot, subagents, options) {
325
353
  // emits when dryRun is true so users previewing a change can see
326
354
  // which tools would be dropped before it actually happens.
327
355
  buildCopilotFile(s, true, verbose);
328
- planLines.push(`Write ${path.join(constants_1.COPILOT_SUBAGENTS_PATH, `${s.name}.md`)}`);
356
+ planLines.push(`Write ${path.join(constants_1.COPILOT_SUBAGENTS_PATH, getSourceRelativeMdPath(s))}`);
329
357
  }
330
358
  return planLines;
331
359
  }
332
360
  const files = subagents.map((s) => ({
333
- name: `${s.name}.md`,
361
+ name: getSourceRelativeMdPath(s),
334
362
  content: buildCopilotFile(s, false, verbose).content,
335
363
  }));
336
364
  await writeAgentsDirectoryAtomic(targetDir, files);
@@ -363,10 +391,24 @@ async function cleanupAllSubagentsDirectories(projectRoot, dryRun, verbose) {
363
391
  /* ------------------------------------------------------------------ */
364
392
  /* Orchestrator */
365
393
  /* ------------------------------------------------------------------ */
366
- async function propagateSubagents(projectRoot, agents, subagentsEnabled, verbose, dryRun) {
367
- if (!subagentsEnabled) {
368
- (0, constants_1.logVerboseInfo)('Subagents support disabled, cleaning up subagent directories', verbose, dryRun);
394
+ async function propagateSubagents(projectRoot, agents, subagentsEnabled, cleanupOrphaned, verbose, dryRun) {
395
+ const maybeCleanupAllSubagentsDirectories = async () => {
396
+ if (!cleanupOrphaned) {
397
+ (0, constants_1.logVerboseInfo)('Subagent cleanup skipped (set [agents] cleanup_orphaned = true to enable directory cleanup)', verbose, dryRun);
398
+ return;
399
+ }
369
400
  await cleanupAllSubagentsDirectories(projectRoot, dryRun, verbose);
401
+ };
402
+ const maybeCleanupSubagentsDir = async (relPath) => {
403
+ if (!cleanupOrphaned)
404
+ return;
405
+ await cleanupSubagentsDir(projectRoot, relPath, dryRun, verbose);
406
+ };
407
+ if (!subagentsEnabled) {
408
+ (0, constants_1.logVerboseInfo)(cleanupOrphaned
409
+ ? 'Subagents support disabled, cleaning up subagent directories'
410
+ : 'Subagents support disabled, leaving existing subagent directories unchanged', verbose, dryRun);
411
+ await maybeCleanupAllSubagentsDirectories();
370
412
  return;
371
413
  }
372
414
  const sourceDir = path.join(projectRoot, constants_1.RULER_SUBAGENTS_PATH);
@@ -374,16 +416,20 @@ async function propagateSubagents(projectRoot, agents, subagentsEnabled, verbose
374
416
  await fs.access(sourceDir);
375
417
  }
376
418
  catch {
377
- (0, constants_1.logVerboseInfo)('No .ruler/agents directory found, cleaning up any stale managed subagent directories', verbose, dryRun);
378
- await cleanupAllSubagentsDirectories(projectRoot, dryRun, verbose);
419
+ (0, constants_1.logVerboseInfo)(cleanupOrphaned
420
+ ? 'No .ruler/agents directory found, cleaning up any stale managed subagent directories'
421
+ : 'No .ruler/agents directory found; leaving existing subagent directories unchanged', verbose, dryRun);
422
+ await maybeCleanupAllSubagentsDirectories();
379
423
  return;
380
424
  }
381
425
  const { subagents, warnings } = await discoverSubagents(projectRoot);
382
426
  for (const w of warnings)
383
427
  (0, constants_1.logWarn)(w, dryRun);
384
428
  if (subagents.length === 0) {
385
- (0, constants_1.logVerboseInfo)('No valid subagents found in .ruler/agents; cleaning up any stale managed subagent directories', verbose, dryRun);
386
- await cleanupAllSubagentsDirectories(projectRoot, dryRun, verbose);
429
+ (0, constants_1.logVerboseInfo)(cleanupOrphaned
430
+ ? 'No valid subagents found in .ruler/agents; cleaning up any stale managed subagent directories'
431
+ : 'No valid subagents found in .ruler/agents; leaving existing subagent directories unchanged', verbose, dryRun);
432
+ await maybeCleanupAllSubagentsDirectories();
387
433
  return;
388
434
  }
389
435
  (0, constants_1.logVerboseInfo)(`Discovered ${subagents.length} subagent(s)`, verbose, dryRun);
@@ -401,7 +447,7 @@ async function propagateSubagents(projectRoot, agents, subagentsEnabled, verbose
401
447
  const allTargets = ['claude', 'cursor', 'codex', 'copilot'];
402
448
  for (const target of allTargets) {
403
449
  if (!targets.has(target)) {
404
- await cleanupSubagentsDir(projectRoot, SUBAGENT_TARGET_PATHS[target], dryRun, verbose);
450
+ await maybeCleanupSubagentsDir(SUBAGENT_TARGET_PATHS[target]);
405
451
  }
406
452
  }
407
453
  if (supporting.length === 0) {
@@ -0,0 +1,34 @@
1
+ import type { SubagentFrontmatter, SubagentInfo } from '../types';
2
+ export interface ParsedFrontmatter {
3
+ meta: Record<string, unknown>;
4
+ body: string;
5
+ }
6
+ /**
7
+ * Extracts YAML frontmatter and body from a Markdown file's contents.
8
+ * Returns null if no frontmatter delimiter pair is present at the head of the file.
9
+ */
10
+ export declare function parseFrontmatter(content: string): ParsedFrontmatter | null;
11
+ /**
12
+ * Validates a parsed frontmatter object and reads the required and optional
13
+ * fields into a typed SubagentFrontmatter. Returns the typed value on success
14
+ * or an error message on failure.
15
+ */
16
+ export declare function validateFrontmatter(meta: Record<string, unknown>, expectedName: string): {
17
+ value: SubagentFrontmatter;
18
+ } | {
19
+ error: string;
20
+ };
21
+ /**
22
+ * Loads a single subagent file and produces a SubagentInfo.
23
+ * Invalid files produce a SubagentInfo with valid=false and an error string.
24
+ */
25
+ export declare function loadSubagentFile(filePath: string): Promise<SubagentInfo>;
26
+ export interface CopilotToolMapping {
27
+ tools: string[];
28
+ unknown: string[];
29
+ }
30
+ /**
31
+ * Translates Claude tool names to Copilot aliases. Deduplicates results.
32
+ * Unknown source tools are reported separately so callers can surface a warning.
33
+ */
34
+ export declare function mapToolsForCopilot(sourceTools: string[]): CopilotToolMapping;
@@ -0,0 +1,10 @@
1
+ import { RulerUnifiedConfig } from './UnifiedConfigTypes';
2
+ export interface UnifiedLoadOptions {
3
+ projectRoot: string;
4
+ configPath?: string;
5
+ cliAgents?: string[];
6
+ cliMcpEnabled?: boolean;
7
+ cliMcpStrategy?: string;
8
+ checkGlobal?: boolean;
9
+ }
10
+ export declare function loadUnifiedConfig(options: UnifiedLoadOptions): Promise<RulerUnifiedConfig>;
@@ -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>;