@intellectronica/ruler 0.3.42 → 0.3.43

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 (50) hide show
  1. package/README.md +97 -10
  2. package/dist/agents/AbstractAgent.js +3 -2
  3. package/dist/agents/AgentsMdAgent.js +3 -2
  4. package/dist/agents/AiderAgent.js +4 -3
  5. package/dist/agents/AmazonQCliAgent.js +6 -4
  6. package/dist/agents/AugmentCodeAgent.js +3 -2
  7. package/dist/agents/CodexCliAgent.js +1 -1
  8. package/dist/agents/CrushAgent.d.ts +1 -1
  9. package/dist/agents/CrushAgent.js +15 -6
  10. package/dist/agents/FirebenderAgent.js +5 -4
  11. package/dist/agents/GeminiCliAgent.d.ts +1 -0
  12. package/dist/agents/GeminiCliAgent.js +11 -5
  13. package/dist/agents/IAgent.d.ts +2 -0
  14. package/dist/agents/MistralVibeAgent.js +14 -3
  15. package/dist/agents/OpenCodeAgent.d.ts +1 -1
  16. package/dist/agents/OpenCodeAgent.js +10 -3
  17. package/dist/agents/QwenCodeAgent.d.ts +1 -0
  18. package/dist/agents/QwenCodeAgent.js +9 -3
  19. package/dist/agents/RooCodeAgent.js +3 -2
  20. package/dist/agents/ZedAgent.js +3 -3
  21. package/dist/constants.d.ts +1 -1
  22. package/dist/constants.js +1 -1
  23. package/dist/core/ConfigLoader.d.ts +2 -0
  24. package/dist/core/ConfigLoader.js +73 -6
  25. package/dist/core/FileSystemUtils.d.ts +4 -2
  26. package/dist/core/FileSystemUtils.js +120 -3
  27. package/dist/core/GitignoreUtils.d.ts +10 -0
  28. package/dist/core/GitignoreUtils.js +62 -31
  29. package/dist/core/SkillsProcessor.d.ts +2 -2
  30. package/dist/core/SkillsProcessor.js +46 -37
  31. package/dist/core/SubagentsProcessor.js +8 -5
  32. package/dist/core/UnifiedConfigLoader.js +54 -2
  33. package/dist/core/UnifiedConfigTypes.d.ts +3 -1
  34. package/dist/core/agent-selection.js +6 -4
  35. package/dist/core/apply-engine.d.ts +1 -0
  36. package/dist/core/apply-engine.js +38 -15
  37. package/dist/core/revert-engine.d.ts +2 -1
  38. package/dist/core/revert-engine.js +73 -26
  39. package/dist/lib.js +9 -6
  40. package/dist/mcp/merge.js +28 -26
  41. package/dist/mcp/propagateOpenCodeMcp.d.ts +1 -1
  42. package/dist/mcp/propagateOpenCodeMcp.js +10 -3
  43. package/dist/mcp/propagateOpenHandsMcp.d.ts +1 -1
  44. package/dist/mcp/propagateOpenHandsMcp.js +18 -7
  45. package/dist/paths/mcp.d.ts +1 -1
  46. package/dist/paths/mcp.js +11 -4
  47. package/dist/revert.js +27 -27
  48. package/dist/vscode/settings.d.ts +1 -1
  49. package/dist/vscode/settings.js +3 -3
  50. package/package.json +4 -3
package/README.md CHANGED
@@ -59,7 +59,7 @@ Ruler solves this by providing a **single source of truth** for all your AI agen
59
59
  | AGENTS.md | `AGENTS.md` | (pseudo-agent ensuring root `AGENTS.md` exists) | - | - |
60
60
  | GitHub Copilot | `AGENTS.md` | `.vscode/mcp.json` | `.claude/skills/` | `.github/agents/` |
61
61
  | Claude Code | `CLAUDE.md` | `.mcp.json` | `.claude/skills/` | `.claude/agents/` |
62
- | OpenAI Codex CLI | `AGENTS.md` | `.codex/config.toml` | `.codex/skills/` | `.codex/agents/` (`.toml`) |
62
+ | OpenAI Codex CLI | `AGENTS.md` | `.codex/config.toml` | `.agents/skills/` | `.codex/agents/` (`.toml`) |
63
63
  | Pi Coding Agent | `AGENTS.md` | - | `.pi/skills/` | - |
64
64
  | Jules | `AGENTS.md` | - | - | - |
65
65
  | Cursor | `AGENTS.md` | `.cursor/mcp.json` | `.cursor/skills/` | `.cursor/agents/` |
@@ -514,6 +514,20 @@ Authorization = "Bearer your-token"
514
514
  "X-API-Version" = "v1"
515
515
  ```
516
516
 
517
+ Agent-specific MCP servers can be defined under `[agents.<agent>.mcp_servers.<name>]`.
518
+ They are applied only to that agent and override global servers with the same name:
519
+
520
+ ```toml
521
+ [agents.cursor.mcp_servers.slack]
522
+ url = "https://mcp.slack.com/mcp"
523
+ auth = { CLIENT_ID = "CURSOR_ID" }
524
+
525
+ [agents.claude.mcp_servers.slack]
526
+ type = "http"
527
+ url = "https://mcp.slack.com/mcp"
528
+ oauth = { clientId = "CLAUDE_ID", callbackPort = 3118 }
529
+ ```
530
+
517
531
  ### Legacy `.ruler/mcp.json` (Deprecated)
518
532
 
519
533
  For backward compatibility, you can still use the JSON format; a warning is issued encouraging migration to TOML. The file is no longer created during `ruler init`.
@@ -592,7 +606,7 @@ Skills are specialized knowledge packages that extend AI agent capabilities with
592
606
  - **Claude Code**: `.claude/skills/`
593
607
  - **GitHub Copilot**: `.claude/skills/` (shared with Claude Code)
594
608
  - **Kilo Code**: `.claude/skills/` (shared with Claude Code)
595
- - **OpenAI Codex CLI**: `.codex/skills/`
609
+ - **OpenAI Codex CLI**: `.agents/skills/` (shared with Goose, Amp, and Zed)
596
610
  - **OpenCode**: `.opencode/skills/`
597
611
  - **Pi Coding Agent**: `.pi/skills/`
598
612
  - **Goose**: `.agents/skills/`
@@ -661,10 +675,9 @@ If you run Ruler for agents that do not support native skills, Ruler logs a warn
661
675
  When skills support is enabled and gitignore integration is active, Ruler automatically adds:
662
676
 
663
677
  - `.claude/skills/` (for Claude Code, GitHub Copilot, and Kilo Code)
664
- - `.codex/skills/` (for OpenAI Codex CLI)
678
+ - `.agents/skills/` (for OpenAI Codex CLI, Goose, Amp, and Zed)
665
679
  - `.opencode/skills/` (for OpenCode)
666
680
  - `.pi/skills/` (for Pi Coding Agent)
667
- - `.agents/skills/` (for Goose, Amp, and Zed)
668
681
  - `.agent/skills/` (for Antigravity)
669
682
  - `.factory/skills/` (for Factory Droid)
670
683
  - `.vibe/skills/` (for Mistral Vibe)
@@ -678,7 +691,7 @@ to your `.gitignore` file within the managed Ruler block.
678
691
 
679
692
  ### Requirements
680
693
 
681
- - **For agents with native skills support** (Claude Code, GitHub Copilot, Kilo Code, OpenAI Codex CLI, OpenCode, Pi Coding Agent, Goose, Amp, Antigravity, Factory Droid, Mistral Vibe, Roo Code, Gemini CLI, Junie, Cursor, Windsurf): No additional requirements.
694
+ - **For agents with native skills support** (Claude Code, GitHub Copilot, Kilo Code, OpenAI Codex CLI, OpenCode, Pi Coding Agent, Goose, Amp, Zed, Antigravity, Factory Droid, Mistral Vibe, Roo Code, Gemini CLI, Junie, Cursor, Windsurf): No additional requirements.
682
695
 
683
696
  ### Validation
684
697
 
@@ -722,10 +735,10 @@ ruler apply
722
735
 
723
736
  # 3. Skills are now available to compatible agents:
724
737
  # - Claude Code, GitHub Copilot & Kilo Code: .claude/skills/my-skill/
725
- # - OpenAI Codex CLI: .codex/skills/my-skill/
738
+ # - OpenAI Codex CLI: .agents/skills/my-skill/ (shared with Goose, Amp & Zed)
726
739
  # - OpenCode: .opencode/skills/my-skill/
727
740
  # - Pi Coding Agent: .pi/skills/my-skill/
728
- # - Goose, Amp & Zed: .agents/skills/my-skill/
741
+ # - Goose, Amp, Zed & OpenAI Codex CLI: .agents/skills/my-skill/
729
742
  # - Antigravity: .agent/skills/my-skill/
730
743
  # - Factory Droid: .factory/skills/my-skill/
731
744
  # - Mistral Vibe: .vibe/skills/my-skill/
@@ -938,7 +951,81 @@ ruler init
938
951
  ruler apply
939
952
  ```
940
953
 
941
- ### Scenario 2: Complex Projects with Nested Rules
954
+ ### Scenario 2: Working with worktrees
955
+
956
+ When using the default `git add worktree` command (which is also run by agents apps such as Claude code or Codex through the interface), the gitignored files are not copied over. You will need to ask your agent to run `ruler apply` at the start of every session.
957
+
958
+ As an alternative you can commit your default agents files to source control.
959
+
960
+ ```toml
961
+ # .ruler/ruler.toml
962
+ default_agents = ["claude", "codex"]
963
+
964
+ [gitignore]
965
+ enabled = false
966
+ ```
967
+
968
+ ```ignore
969
+ # Do not ignore AGENTS.md and CLAUDE.md
970
+ /.claude/*
971
+ !/.claude/skills/
972
+ /.codex/*
973
+ !/.codex/skills/
974
+ /.cursor
975
+ /AGENTS.md.bak
976
+ /CLAUDE.md.bak
977
+ ```
978
+
979
+ To avoid having other contributors commit instructions outside of .ruler you can setup a github action to check there is no diff when running `ruler apply` in CI.
980
+
981
+ ```yml
982
+ # .github/workflows/ruler-check/yml
983
+
984
+ # Verifies the committed agent files (AGENTS.md, CLAUDE.md, skills) match the .ruler/ source.
985
+ # They are committed so a fresh clone/worktree has guidance immediately; this guards against drift.
986
+ name: Ruler guidance in sync
987
+
988
+ on:
989
+ pull_request:
990
+ push:
991
+ branches:
992
+ - main
993
+ - 'build/**'
994
+
995
+ permissions:
996
+ contents: read
997
+
998
+ env:
999
+ CI_NODE_VERSION: 24.15.0
1000
+
1001
+ jobs:
1002
+ ruler-check:
1003
+ runs-on: ubuntu-latest
1004
+ steps:
1005
+ - name: Checkout
1006
+ uses: actions/checkout@v6
1007
+
1008
+ - uses: pnpm/action-setup@v5
1009
+ - name: Setup Node
1010
+ uses: actions/setup-node@v6
1011
+ with:
1012
+ node-version: ${{ env.CI_NODE_VERSION }}
1013
+ cache: 'pnpm'
1014
+
1015
+ - name: Verify committed agent files match .ruler/
1016
+ run: |
1017
+ pnpm dlx @intellectronica/ruler@0.3.42 apply --no-gitignore --no-mcp
1018
+ DRIFT="$(git status --porcelain -- AGENTS.md CLAUDE.md .claude/skills .codex/skills)"
1019
+ if [ -n "$DRIFT" ]; then
1020
+ echo "::error::Committed agent files are out of sync with .ruler/. Run 'pnpm dlx @intellectronica/ruler apply --no-gitignore --no-mcp' and commit the result."
1021
+ echo "$DRIFT"
1022
+ git --no-pager diff -- AGENTS.md CLAUDE.md .claude/skills .codex/skills
1023
+ exit 1
1024
+ fi
1025
+ echo "Agent files are in sync with .ruler/."
1026
+ ```
1027
+
1028
+ ### Scenario 3: Complex Projects with Nested Rules
942
1029
 
943
1030
  For large projects with multiple components or services, enable nested rule loading so each directory keeps its own rules and MCP bundle:
944
1031
 
@@ -970,13 +1057,13 @@ This creates context-specific instructions for different parts of your project w
970
1057
  > [!NOTE]
971
1058
  > The CLI prints "Nested mode is experimental and may change in future releases." the first time nested processing runs. Expect refinements in future versions.
972
1059
 
973
- ### Scenario 3: Team Standardization
1060
+ ### Scenario 4: Team Standardization
974
1061
 
975
1062
  1. Create `.ruler/coding_standards.md`, `.ruler/api_usage.md`
976
1063
  2. Commit the `.ruler` directory to your repository
977
1064
  3. Team members pull changes and run `ruler apply` to update their local AI agent configurations
978
1065
 
979
- ### Scenario 4: Project-Specific Context for AI
1066
+ ### Scenario 5: Project-Specific Context for AI
980
1067
 
981
1068
  1. Detail your project's architecture in `.ruler/project_overview.md`
982
1069
  2. Describe primary data structures in `.ruler/data_models.md`
@@ -52,11 +52,12 @@ class AbstractAgent {
52
52
  async applyRulerConfig(concatenatedRules, projectRoot, _rulerMcpJson, agentConfig, backup = true) {
53
53
  const output = agentConfig?.outputPath ?? this.getDefaultOutputPath(projectRoot);
54
54
  const absolutePath = path.resolve(projectRoot, output);
55
+ await (0, FileSystemUtils_1.assertManagedPathInsideRoot)(absolutePath, projectRoot, 'Refusing to write generated file outside project');
55
56
  await (0, FileSystemUtils_1.ensureDirExists)(path.dirname(absolutePath));
56
57
  if (backup) {
57
- await (0, FileSystemUtils_1.backupFile)(absolutePath);
58
+ await (0, FileSystemUtils_1.backupFile)(absolutePath, projectRoot);
58
59
  }
59
- await (0, FileSystemUtils_1.writeGeneratedFile)(absolutePath, concatenatedRules);
60
+ await (0, FileSystemUtils_1.writeGeneratedFile)(absolutePath, concatenatedRules, projectRoot);
60
61
  }
61
62
  /**
62
63
  * Returns the specific key to be used for the server object in MCP JSON.
@@ -56,6 +56,7 @@ class AgentsMdAgent extends AbstractAgent_1.AbstractAgent {
56
56
  async applyRulerConfig(concatenatedRules, projectRoot, _rulerMcpJson, agentConfig, backup = true) {
57
57
  const output = agentConfig?.outputPath ?? this.getDefaultOutputPath(projectRoot);
58
58
  const absolutePath = path.resolve(projectRoot, output);
59
+ await (0, FileSystemUtils_1.assertManagedPathInsideRoot)(absolutePath, projectRoot, 'Refusing to write generated file outside project');
59
60
  await (0, FileSystemUtils_1.ensureDirExists)(path.dirname(absolutePath));
60
61
  // Add marker comment to the content to identify it as generated
61
62
  const contentWithMarker = `<!-- Generated by Ruler -->\n${concatenatedRules}`;
@@ -73,9 +74,9 @@ class AgentsMdAgent extends AbstractAgent_1.AbstractAgent {
73
74
  }
74
75
  // Backup (only if file existed and backup is enabled) then write new content
75
76
  if (backup) {
76
- await (0, FileSystemUtils_1.backupFile)(absolutePath);
77
+ await (0, FileSystemUtils_1.backupFile)(absolutePath, projectRoot);
77
78
  }
78
- await (0, FileSystemUtils_1.writeGeneratedFile)(absolutePath, contentWithMarker);
79
+ await (0, FileSystemUtils_1.writeGeneratedFile)(absolutePath, contentWithMarker, projectRoot);
79
80
  }
80
81
  getMcpServerKey() {
81
82
  // No MCP configuration for this pseudo-agent
@@ -67,7 +67,7 @@ class AiderAgent {
67
67
  try {
68
68
  await fs.access(cfgPath);
69
69
  if (backup) {
70
- await (0, FileSystemUtils_1.backupFile)(cfgPath);
70
+ await (0, FileSystemUtils_1.backupFile)(cfgPath, projectRoot);
71
71
  }
72
72
  const raw = await fs.readFile(cfgPath, 'utf8');
73
73
  doc = (yaml.load(raw) || {});
@@ -89,16 +89,17 @@ class AiderAgent {
89
89
  doc.read.push(name);
90
90
  }
91
91
  const yamlStr = yaml.dump(doc);
92
- await (0, FileSystemUtils_1.writeGeneratedFile)(cfgPath, yamlStr);
92
+ await (0, FileSystemUtils_1.writeGeneratedFile)(cfgPath, yamlStr, projectRoot);
93
93
  }
94
94
  getDefaultOutputPath(projectRoot) {
95
95
  return {
96
96
  instructions: path.join(projectRoot, 'AGENTS.md'),
97
97
  config: path.join(projectRoot, '.aider.conf.yml'),
98
+ mcp: path.join(projectRoot, '.mcp.json'),
98
99
  };
99
100
  }
100
101
  getMcpServerKey() {
101
- return this.agentsMdAgent.getMcpServerKey();
102
+ return 'mcpServers';
102
103
  }
103
104
  supportsMcpStdio() {
104
105
  return true;
@@ -54,16 +54,18 @@ class AmazonQCliAgent {
54
54
  agentConfig?.outputPathInstructions ||
55
55
  outputPaths['instructions']);
56
56
  // Write rules file to .amazonq/rules/
57
+ await (0, FileSystemUtils_1.assertManagedPathInsideRoot)(rulesPath, projectRoot, 'Refusing to write generated file outside project');
57
58
  await (0, FileSystemUtils_1.ensureDirExists)(path.dirname(rulesPath));
58
59
  if (backup) {
59
- await (0, FileSystemUtils_1.backupFile)(rulesPath);
60
+ await (0, FileSystemUtils_1.backupFile)(rulesPath, projectRoot);
60
61
  }
61
- await (0, FileSystemUtils_1.writeGeneratedFile)(rulesPath, concatenatedRules);
62
+ await (0, FileSystemUtils_1.writeGeneratedFile)(rulesPath, concatenatedRules, projectRoot);
62
63
  // Handle MCP configuration if enabled and provided
63
64
  const mcpEnabled = agentConfig?.mcp?.enabled ?? true;
64
65
  if (mcpEnabled && rulerMcpJson) {
65
66
  const mcpPath = path.resolve(projectRoot, agentConfig?.outputPathConfig ?? outputPaths['mcp']);
66
67
  const mcpStrategy = agentConfig?.mcp?.strategy ?? 'merge';
68
+ await (0, FileSystemUtils_1.assertManagedPathInsideRoot)(mcpPath, projectRoot, 'Refusing to write generated file outside project');
67
69
  await (0, FileSystemUtils_1.ensureDirExists)(path.dirname(mcpPath));
68
70
  let existingMcpConfig = {};
69
71
  try {
@@ -79,9 +81,9 @@ class AmazonQCliAgent {
79
81
  // Merge the MCP configurations using the standard merge function
80
82
  const mergedConfig = (0, merge_1.mergeMcp)(existingMcpConfig, rulerMcpJson, mcpStrategy, 'mcpServers');
81
83
  if (backup) {
82
- await (0, FileSystemUtils_1.backupFile)(mcpPath);
84
+ await (0, FileSystemUtils_1.backupFile)(mcpPath, projectRoot);
83
85
  }
84
- await (0, FileSystemUtils_1.writeGeneratedFile)(mcpPath, JSON.stringify(mergedConfig, null, 2));
86
+ await (0, FileSystemUtils_1.writeGeneratedFile)(mcpPath, JSON.stringify(mergedConfig, null, 2), projectRoot);
85
87
  }
86
88
  }
87
89
  getDefaultOutputPath(projectRoot) {
@@ -49,10 +49,11 @@ class AugmentCodeAgent {
49
49
  }
50
50
  async applyRulerConfig(concatenatedRules, projectRoot, _rulerMcpJson, agentConfig, backup = true) {
51
51
  const output = agentConfig?.outputPath ?? this.getDefaultOutputPath(projectRoot);
52
+ const absolutePath = path.resolve(projectRoot, output);
52
53
  if (backup) {
53
- await (0, FileSystemUtils_1.backupFile)(output);
54
+ await (0, FileSystemUtils_1.backupFile)(absolutePath, projectRoot);
54
55
  }
55
- await (0, FileSystemUtils_1.writeGeneratedFile)(output, concatenatedRules);
56
+ await (0, FileSystemUtils_1.writeGeneratedFile)(absolutePath, concatenatedRules, projectRoot);
56
57
  // AugmentCode does not support MCP servers
57
58
  // MCP configuration is ignored for this agent
58
59
  }
@@ -128,7 +128,7 @@ class CodexCliAgent {
128
128
  const finalConfig = { ...updatedConfig };
129
129
  // @iarna/toml should handle the formatting properly
130
130
  const tomlContent = (0, toml_1.stringify)(finalConfig);
131
- await (0, FileSystemUtils_1.writeGeneratedFile)(configPath, tomlContent);
131
+ await (0, FileSystemUtils_1.writeGeneratedFile)(configPath, tomlContent, projectRoot);
132
132
  }
133
133
  }
134
134
  getDefaultOutputPath(projectRoot) {
@@ -8,7 +8,7 @@ export declare class CrushAgent implements IAgent {
8
8
  * Crush expects "http" for HTTP servers and "sse" for SSE servers, not "remote".
9
9
  */
10
10
  private transformMcpServersForCrush;
11
- applyRulerConfig(concatenatedRules: string, projectRoot: string, rulerMcpJson: Record<string, unknown> | null, agentConfig?: IAgentConfig): Promise<void>;
11
+ applyRulerConfig(concatenatedRules: string, projectRoot: string, rulerMcpJson: Record<string, unknown> | null, agentConfig?: IAgentConfig, backup?: boolean): Promise<void>;
12
12
  supportsMcpStdio(): boolean;
13
13
  supportsMcpRemote(): boolean;
14
14
  }
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.CrushAgent = void 0;
37
37
  const fs = __importStar(require("fs/promises"));
38
38
  const path = __importStar(require("path"));
39
+ const FileSystemUtils_1 = require("../core/FileSystemUtils");
39
40
  class CrushAgent {
40
41
  getIdentifier() {
41
42
  return 'crush';
@@ -80,13 +81,17 @@ class CrushAgent {
80
81
  }
81
82
  return transformedServers;
82
83
  }
83
- async applyRulerConfig(concatenatedRules, projectRoot, rulerMcpJson, agentConfig) {
84
+ async applyRulerConfig(concatenatedRules, projectRoot, rulerMcpJson, agentConfig, backup = true) {
84
85
  const outputPaths = this.getDefaultOutputPath(projectRoot);
85
- const instructionsPath = agentConfig?.outputPath ??
86
+ const instructionsPath = path.resolve(projectRoot, agentConfig?.outputPath ??
86
87
  agentConfig?.outputPathInstructions ??
87
- outputPaths['instructions'];
88
- const mcpPath = agentConfig?.outputPathConfig ?? outputPaths['mcp'];
89
- await fs.writeFile(instructionsPath, concatenatedRules);
88
+ outputPaths['instructions']);
89
+ const mcpPath = path.resolve(projectRoot, agentConfig?.outputPathConfig ?? outputPaths['mcp']);
90
+ await fs.mkdir(path.dirname(instructionsPath), { recursive: true });
91
+ if (backup) {
92
+ await (0, FileSystemUtils_1.backupFile)(instructionsPath, projectRoot);
93
+ }
94
+ await (0, FileSystemUtils_1.writeGeneratedFile)(instructionsPath, concatenatedRules, projectRoot);
90
95
  // Always transform from mcpServers ({ mcpServers: ... }) to { mcp: ... } for Crush
91
96
  let finalMcpConfig = { mcp: {} };
92
97
  const strategy = agentConfig?.mcp?.strategy ?? 'merge';
@@ -118,7 +123,11 @@ class CrushAgent {
118
123
  }
119
124
  }
120
125
  if (Object.keys(finalMcpConfig.mcp).length > 0) {
121
- await fs.writeFile(mcpPath, JSON.stringify(finalMcpConfig, null, 2));
126
+ await fs.mkdir(path.dirname(mcpPath), { recursive: true });
127
+ if (backup) {
128
+ await (0, FileSystemUtils_1.backupFile)(mcpPath, projectRoot);
129
+ }
130
+ await (0, FileSystemUtils_1.writeGeneratedFile)(mcpPath, JSON.stringify(finalMcpConfig, null, 2), projectRoot);
122
131
  }
123
132
  }
124
133
  supportsMcpStdio() {
@@ -60,6 +60,7 @@ class FirebenderAgent {
60
60
  }
61
61
  async applyRulerConfig(concatenatedRules, projectRoot, rulerMcpJson, agentConfig, backup = true) {
62
62
  const rulesPath = this.resolveOutputPath(projectRoot, agentConfig);
63
+ await (0, FileSystemUtils_1.assertManagedPathInsideRoot)(rulesPath, projectRoot, 'Refusing to write generated file outside project');
63
64
  await (0, FileSystemUtils_1.ensureDirExists)(path.dirname(rulesPath));
64
65
  const firebenderConfig = await this.loadExistingConfig(rulesPath);
65
66
  const newRules = this.createRulesFromConcatenatedRules(concatenatedRules, projectRoot);
@@ -69,7 +70,7 @@ class FirebenderAgent {
69
70
  if (mcpEnabled && rulerMcpJson) {
70
71
  await this.handleMcpConfiguration(firebenderConfig, rulerMcpJson, agentConfig);
71
72
  }
72
- await this.saveConfig(rulesPath, firebenderConfig, backup);
73
+ await this.saveConfig(rulesPath, firebenderConfig, backup, projectRoot);
73
74
  }
74
75
  resolveOutputPath(projectRoot, agentConfig) {
75
76
  const outputPaths = this.getDefaultOutputPath(projectRoot);
@@ -135,12 +136,12 @@ class FirebenderAgent {
135
136
  return true;
136
137
  });
137
138
  }
138
- async saveConfig(rulesPath, config, backup) {
139
+ async saveConfig(rulesPath, config, backup, projectRoot) {
139
140
  const updatedContent = JSON.stringify(config, null, 2);
140
141
  if (backup) {
141
- await (0, FileSystemUtils_1.backupFile)(rulesPath);
142
+ await (0, FileSystemUtils_1.backupFile)(rulesPath, projectRoot);
142
143
  }
143
- await (0, FileSystemUtils_1.writeGeneratedFile)(rulesPath, updatedContent);
144
+ await (0, FileSystemUtils_1.writeGeneratedFile)(rulesPath, updatedContent, projectRoot);
144
145
  }
145
146
  /**
146
147
  * Handle MCP server configuration for Firebender.
@@ -3,6 +3,7 @@ import { AgentsMdAgent } from './AgentsMdAgent';
3
3
  export declare class GeminiCliAgent extends AgentsMdAgent {
4
4
  getIdentifier(): string;
5
5
  getName(): string;
6
+ private getContextFileName;
6
7
  applyRulerConfig(concatenatedRules: string, projectRoot: string, rulerMcpJson: Record<string, unknown> | null, agentConfig?: IAgentConfig, backup?: boolean): Promise<void>;
7
8
  getMcpServerKey(): string;
8
9
  supportsMcpStdio(): boolean;
@@ -37,6 +37,7 @@ exports.GeminiCliAgent = void 0;
37
37
  const path = __importStar(require("path"));
38
38
  const fs_1 = require("fs");
39
39
  const AgentsMdAgent_1 = require("./AgentsMdAgent");
40
+ const FileSystemUtils_1 = require("../core/FileSystemUtils");
40
41
  class GeminiCliAgent extends AgentsMdAgent_1.AgentsMdAgent {
41
42
  getIdentifier() {
42
43
  return 'gemini-cli';
@@ -44,13 +45,19 @@ class GeminiCliAgent extends AgentsMdAgent_1.AgentsMdAgent {
44
45
  getName() {
45
46
  return 'Gemini CLI';
46
47
  }
48
+ getContextFileName(projectRoot, agentConfig) {
49
+ const outputPath = agentConfig?.outputPath ?? 'AGENTS.md';
50
+ return path
51
+ .relative(projectRoot, path.resolve(projectRoot, outputPath))
52
+ .replace(/\\/g, '/');
53
+ }
47
54
  async applyRulerConfig(concatenatedRules, projectRoot, rulerMcpJson, agentConfig, backup = true) {
48
55
  // First, perform idempotent write of AGENTS.md via base class
49
56
  await super.applyRulerConfig(concatenatedRules, projectRoot, null, {
50
57
  outputPath: agentConfig?.outputPath,
51
58
  }, backup);
52
- // Prepare .gemini/settings.json with contextFileName and MCP configuration
53
- const settingsPath = path.join(projectRoot, '.gemini', 'settings.json');
59
+ // Prepare settings with contextFileName and MCP configuration
60
+ const settingsPath = path.resolve(projectRoot, agentConfig?.outputPathConfig ?? path.join('.gemini', 'settings.json'));
54
61
  let existingSettings = {};
55
62
  try {
56
63
  const raw = await fs_1.promises.readFile(settingsPath, 'utf8');
@@ -63,7 +70,7 @@ class GeminiCliAgent extends AgentsMdAgent_1.AgentsMdAgent {
63
70
  }
64
71
  const updated = {
65
72
  ...existingSettings,
66
- contextFileName: 'AGENTS.md',
73
+ contextFileName: this.getContextFileName(projectRoot, agentConfig),
67
74
  };
68
75
  // Handle MCP server configuration if provided
69
76
  const mcpEnabled = agentConfig?.mcp?.enabled ?? true;
@@ -100,8 +107,7 @@ class GeminiCliAgent extends AgentsMdAgent_1.AgentsMdAgent {
100
107
  updated[this.getMcpServerKey()] = stripTypeField(mergedServers);
101
108
  }
102
109
  }
103
- await fs_1.promises.mkdir(path.dirname(settingsPath), { recursive: true });
104
- await fs_1.promises.writeFile(settingsPath, JSON.stringify(updated, null, 2));
110
+ await (0, FileSystemUtils_1.writeGeneratedFile)(settingsPath, JSON.stringify(updated, null, 2), projectRoot);
105
111
  }
106
112
  // Ensure MCP merging uses the correct key for Gemini (.gemini/settings.json)
107
113
  getMcpServerKey() {
@@ -16,6 +16,8 @@ export interface IAgentConfig {
16
16
  outputPathConfig?: string;
17
17
  /** MCP propagation config for this agent. */
18
18
  mcp?: McpConfig;
19
+ /** Agent-scoped MCP server definitions. */
20
+ mcpServers?: Record<string, Record<string, unknown>>;
19
21
  }
20
22
  export interface IAgent {
21
23
  /**
@@ -109,12 +109,20 @@ class MistralVibeAgent {
109
109
  }
110
110
  // Read existing TOML config if it exists
111
111
  let existingConfig = {};
112
+ let configExists = false;
112
113
  try {
113
114
  const existingContent = await fs_1.promises.readFile(configPath, 'utf8');
115
+ configExists = true;
114
116
  existingConfig = (0, toml_1.parse)(existingContent);
115
117
  }
116
- catch {
117
- // File doesn't exist or can't be parsed, use empty config
118
+ catch (error) {
119
+ if (error.code === 'ENOENT') {
120
+ // Missing config starts from an empty config.
121
+ existingConfig = {};
122
+ }
123
+ else {
124
+ throw new Error(`Invalid Mistral config at ${configPath}: ${error instanceof Error ? error.message : String(error)}`);
125
+ }
118
126
  }
119
127
  // Create the updated config
120
128
  const updatedConfig = { ...existingConfig };
@@ -134,7 +142,10 @@ class MistralVibeAgent {
134
142
  // Convert to TOML and write
135
143
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
136
144
  const tomlContent = (0, toml_1.stringify)(updatedConfig);
137
- await (0, FileSystemUtils_1.writeGeneratedFile)(configPath, tomlContent);
145
+ if (configExists && backup) {
146
+ await (0, FileSystemUtils_1.backupFile)(configPath, projectRoot);
147
+ }
148
+ await (0, FileSystemUtils_1.writeGeneratedFile)(configPath, tomlContent, projectRoot);
138
149
  }
139
150
  }
140
151
  /**
@@ -3,7 +3,7 @@ export declare class OpenCodeAgent implements IAgent {
3
3
  getIdentifier(): string;
4
4
  getName(): string;
5
5
  getDefaultOutputPath(projectRoot: string): Record<string, string>;
6
- applyRulerConfig(concatenatedRules: string, projectRoot: string, rulerMcpJson: Record<string, unknown> | null, agentConfig?: IAgentConfig): Promise<void>;
6
+ applyRulerConfig(concatenatedRules: string, projectRoot: string, rulerMcpJson: Record<string, unknown> | null, agentConfig?: IAgentConfig, backup?: boolean): Promise<void>;
7
7
  supportsMcpStdio(): boolean;
8
8
  supportsMcpRemote(): boolean;
9
9
  supportsMcpTimeout(): boolean;
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.OpenCodeAgent = void 0;
37
37
  const fs = __importStar(require("fs/promises"));
38
38
  const path = __importStar(require("path"));
39
+ const FileSystemUtils_1 = require("../core/FileSystemUtils");
39
40
  class OpenCodeAgent {
40
41
  getIdentifier() {
41
42
  return 'opencode';
@@ -49,14 +50,17 @@ class OpenCodeAgent {
49
50
  mcp: path.join(projectRoot, 'opencode.json'),
50
51
  };
51
52
  }
52
- async applyRulerConfig(concatenatedRules, projectRoot, rulerMcpJson, agentConfig) {
53
+ async applyRulerConfig(concatenatedRules, projectRoot, rulerMcpJson, agentConfig, backup = true) {
53
54
  const outputPaths = this.getDefaultOutputPath(projectRoot);
54
55
  const instructionsPath = path.resolve(projectRoot, agentConfig?.outputPath ??
55
56
  agentConfig?.outputPathInstructions ??
56
57
  outputPaths['instructions']);
57
58
  const mcpPath = path.resolve(projectRoot, agentConfig?.outputPathConfig ?? outputPaths['mcp']);
58
59
  await fs.mkdir(path.dirname(instructionsPath), { recursive: true });
59
- await fs.writeFile(instructionsPath, concatenatedRules);
60
+ if (backup) {
61
+ await (0, FileSystemUtils_1.backupFile)(instructionsPath, projectRoot);
62
+ }
63
+ await (0, FileSystemUtils_1.writeGeneratedFile)(instructionsPath, concatenatedRules, projectRoot);
60
64
  if (!rulerMcpJson) {
61
65
  return;
62
66
  }
@@ -92,7 +96,10 @@ class OpenCodeAgent {
92
96
  }
93
97
  // Always write the config file, even if MCP is empty
94
98
  await fs.mkdir(path.dirname(mcpPath), { recursive: true });
95
- await fs.writeFile(mcpPath, JSON.stringify(finalMcpConfig, null, 2));
99
+ if (backup) {
100
+ await (0, FileSystemUtils_1.backupFile)(mcpPath, projectRoot);
101
+ }
102
+ await (0, FileSystemUtils_1.writeGeneratedFile)(mcpPath, JSON.stringify(finalMcpConfig, null, 2), projectRoot);
96
103
  }
97
104
  supportsMcpStdio() {
98
105
  return true;
@@ -3,6 +3,7 @@ import { AgentsMdAgent } from './AgentsMdAgent';
3
3
  export declare class QwenCodeAgent extends AgentsMdAgent {
4
4
  getIdentifier(): string;
5
5
  getName(): string;
6
+ private getContextFileName;
6
7
  applyRulerConfig(concatenatedRules: string, projectRoot: string, _rulerMcpJson: Record<string, unknown> | null, agentConfig?: IAgentConfig, backup?: boolean): Promise<void>;
7
8
  getMcpServerKey(): string;
8
9
  supportsMcpStdio(): boolean;
@@ -37,6 +37,7 @@ exports.QwenCodeAgent = void 0;
37
37
  const path = __importStar(require("path"));
38
38
  const fs_1 = require("fs");
39
39
  const AgentsMdAgent_1 = require("./AgentsMdAgent");
40
+ const FileSystemUtils_1 = require("../core/FileSystemUtils");
40
41
  class QwenCodeAgent extends AgentsMdAgent_1.AgentsMdAgent {
41
42
  getIdentifier() {
42
43
  return 'qwen';
@@ -44,6 +45,12 @@ class QwenCodeAgent extends AgentsMdAgent_1.AgentsMdAgent {
44
45
  getName() {
45
46
  return 'Qwen Code';
46
47
  }
48
+ getContextFileName(projectRoot, agentConfig) {
49
+ const outputPath = agentConfig?.outputPath ?? 'AGENTS.md';
50
+ return path
51
+ .relative(projectRoot, path.resolve(projectRoot, outputPath))
52
+ .replace(/\\/g, '/');
53
+ }
47
54
  async applyRulerConfig(concatenatedRules, projectRoot, _rulerMcpJson, agentConfig, backup = true) {
48
55
  // First, perform idempotent write of AGENTS.md via base class
49
56
  await super.applyRulerConfig(concatenatedRules, projectRoot, null, {
@@ -63,10 +70,9 @@ class QwenCodeAgent extends AgentsMdAgent_1.AgentsMdAgent {
63
70
  }
64
71
  const updated = {
65
72
  ...existingSettings,
66
- contextFileName: 'AGENTS.md',
73
+ contextFileName: this.getContextFileName(projectRoot, agentConfig),
67
74
  };
68
- await fs_1.promises.mkdir(path.dirname(settingsPath), { recursive: true });
69
- await fs_1.promises.writeFile(settingsPath, JSON.stringify(updated, null, 2));
75
+ await (0, FileSystemUtils_1.writeGeneratedFile)(settingsPath, JSON.stringify(updated, null, 2), projectRoot);
70
76
  }
71
77
  // Ensure MCP merging uses the correct key for Qwen Code (.qwen/settings.json)
72
78
  getMcpServerKey() {
@@ -69,6 +69,7 @@ class RooCodeAgent {
69
69
  // Now handle .roo/mcp.json configuration
70
70
  const outputPaths = this.getDefaultOutputPath(projectRoot);
71
71
  const mcpPath = path.resolve(projectRoot, agentConfig?.outputPathConfig ?? outputPaths['mcp']);
72
+ await (0, FileSystemUtils_1.assertManagedPathInsideRoot)(mcpPath, projectRoot, 'Refusing to write generated file outside project');
72
73
  await (0, FileSystemUtils_1.ensureDirExists)(path.dirname(mcpPath));
73
74
  // Create base structure with mcpServers
74
75
  let finalMcpConfig = {
@@ -122,9 +123,9 @@ class RooCodeAgent {
122
123
  }
123
124
  // Backup (only if file existed and backup is enabled) then write new content
124
125
  if (backup) {
125
- await (0, FileSystemUtils_1.backupFile)(mcpPath);
126
+ await (0, FileSystemUtils_1.backupFile)(mcpPath, projectRoot);
126
127
  }
127
- await (0, FileSystemUtils_1.writeGeneratedFile)(mcpPath, newContent);
128
+ await (0, FileSystemUtils_1.writeGeneratedFile)(mcpPath, newContent, projectRoot);
128
129
  }
129
130
  supportsMcpStdio() {
130
131
  return true;
@@ -37,6 +37,7 @@ exports.ZedAgent = void 0;
37
37
  const path = __importStar(require("path"));
38
38
  const fs_1 = require("fs");
39
39
  const AgentsMdAgent_1 = require("./AgentsMdAgent");
40
+ const FileSystemUtils_1 = require("../core/FileSystemUtils");
40
41
  /**
41
42
  * Zed editor agent adapter.
42
43
  * Inherits from AgentsMdAgent to write instructions to AGENTS.md and handles
@@ -57,7 +58,7 @@ class ZedAgent extends AgentsMdAgent_1.AgentsMdAgent {
57
58
  // Handle MCP server configuration if enabled and provided
58
59
  const mcpEnabled = agentConfig?.mcp?.enabled ?? true;
59
60
  if (mcpEnabled && rulerMcpJson) {
60
- const zedSettingsPath = path.join(projectRoot, '.zed', 'settings.json');
61
+ const zedSettingsPath = path.resolve(projectRoot, agentConfig?.outputPathConfig ?? path.join('.zed', 'settings.json'));
61
62
  // Read existing settings
62
63
  let existingSettings = {};
63
64
  try {
@@ -103,8 +104,7 @@ class ZedAgent extends AgentsMdAgent_1.AgentsMdAgent {
103
104
  };
104
105
  }
105
106
  // Write updated settings
106
- await fs_1.promises.mkdir(path.dirname(zedSettingsPath), { recursive: true });
107
- await fs_1.promises.writeFile(zedSettingsPath, JSON.stringify(mergedSettings, null, 2));
107
+ await (0, FileSystemUtils_1.writeGeneratedFile)(zedSettingsPath, JSON.stringify(mergedSettings, null, 2), projectRoot);
108
108
  }
109
109
  }
110
110
  getMcpServerKey() {
@@ -15,7 +15,7 @@ export declare function logVerboseInfo(message: string, isVerbose: boolean, dryR
15
15
  export declare const SKILLS_DIR = "skills";
16
16
  export declare const RULER_SKILLS_PATH = ".ruler/skills";
17
17
  export declare const CLAUDE_SKILLS_PATH = ".claude/skills";
18
- export declare const CODEX_SKILLS_PATH = ".codex/skills";
18
+ export declare const CODEX_SKILLS_PATH = ".agents/skills";
19
19
  export declare const OPENCODE_SKILLS_PATH = ".opencode/skills";
20
20
  export declare const PI_SKILLS_PATH = ".pi/skills";
21
21
  export declare const GOOSE_SKILLS_PATH = ".agents/skills";