@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.
- package/README.md +97 -10
- package/dist/agents/AbstractAgent.js +3 -2
- package/dist/agents/AgentsMdAgent.js +3 -2
- package/dist/agents/AiderAgent.js +4 -3
- package/dist/agents/AmazonQCliAgent.js +6 -4
- package/dist/agents/AugmentCodeAgent.js +3 -2
- package/dist/agents/CodexCliAgent.js +1 -1
- package/dist/agents/CrushAgent.d.ts +1 -1
- package/dist/agents/CrushAgent.js +15 -6
- package/dist/agents/FirebenderAgent.js +5 -4
- package/dist/agents/GeminiCliAgent.d.ts +1 -0
- package/dist/agents/GeminiCliAgent.js +11 -5
- package/dist/agents/IAgent.d.ts +2 -0
- package/dist/agents/MistralVibeAgent.js +14 -3
- package/dist/agents/OpenCodeAgent.d.ts +1 -1
- package/dist/agents/OpenCodeAgent.js +10 -3
- package/dist/agents/QwenCodeAgent.d.ts +1 -0
- package/dist/agents/QwenCodeAgent.js +9 -3
- package/dist/agents/RooCodeAgent.js +3 -2
- package/dist/agents/ZedAgent.js +3 -3
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/core/ConfigLoader.d.ts +2 -0
- package/dist/core/ConfigLoader.js +73 -6
- package/dist/core/FileSystemUtils.d.ts +4 -2
- package/dist/core/FileSystemUtils.js +120 -3
- package/dist/core/GitignoreUtils.d.ts +10 -0
- package/dist/core/GitignoreUtils.js +62 -31
- package/dist/core/SkillsProcessor.d.ts +2 -2
- package/dist/core/SkillsProcessor.js +46 -37
- package/dist/core/SubagentsProcessor.js +8 -5
- package/dist/core/UnifiedConfigLoader.js +54 -2
- package/dist/core/UnifiedConfigTypes.d.ts +3 -1
- package/dist/core/agent-selection.js +6 -4
- package/dist/core/apply-engine.d.ts +1 -0
- package/dist/core/apply-engine.js +38 -15
- package/dist/core/revert-engine.d.ts +2 -1
- package/dist/core/revert-engine.js +73 -26
- package/dist/lib.js +9 -6
- package/dist/mcp/merge.js +28 -26
- package/dist/mcp/propagateOpenCodeMcp.d.ts +1 -1
- package/dist/mcp/propagateOpenCodeMcp.js +10 -3
- package/dist/mcp/propagateOpenHandsMcp.d.ts +1 -1
- package/dist/mcp/propagateOpenHandsMcp.js +18 -7
- package/dist/paths/mcp.d.ts +1 -1
- package/dist/paths/mcp.js +11 -4
- package/dist/revert.js +27 -27
- package/dist/vscode/settings.d.ts +1 -1
- package/dist/vscode/settings.js +3 -3
- 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` | `.
|
|
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**: `.
|
|
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
|
-
- `.
|
|
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: .
|
|
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 &
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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)(
|
|
54
|
+
await (0, FileSystemUtils_1.backupFile)(absolutePath, projectRoot);
|
|
54
55
|
}
|
|
55
|
-
await (0, FileSystemUtils_1.writeGeneratedFile)(
|
|
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.
|
|
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.
|
|
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
|
|
53
|
-
const settingsPath = path.
|
|
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:
|
|
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
|
|
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() {
|
package/dist/agents/IAgent.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
73
|
+
contextFileName: this.getContextFileName(projectRoot, agentConfig),
|
|
67
74
|
};
|
|
68
|
-
await
|
|
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;
|
package/dist/agents/ZedAgent.js
CHANGED
|
@@ -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.
|
|
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
|
|
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() {
|
package/dist/constants.d.ts
CHANGED
|
@@ -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 = ".
|
|
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";
|