@intellectronica/ruler 0.3.9 → 0.3.11
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 +198 -44
- package/dist/agents/AbstractAgent.js +7 -0
- package/dist/agents/ClaudeAgent.js +3 -0
- package/dist/agents/FirebenderAgent.js +205 -0
- package/dist/agents/WindsurfAgent.js +6 -137
- package/dist/agents/index.js +2 -0
- package/dist/cli/commands.js +5 -2
- package/dist/cli/handlers.js +31 -2
- package/dist/constants.js +8 -1
- package/dist/core/ConfigLoader.js +16 -1
- package/dist/core/SkillsProcessor.js +301 -0
- package/dist/core/SkillsUtils.js +161 -0
- package/dist/core/UnifiedConfigLoader.js +12 -0
- package/dist/core/apply-engine.js +162 -31
- package/dist/lib.js +105 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -54,16 +54,16 @@ Ruler solves this by providing a **single source of truth** for all your AI agen
|
|
|
54
54
|
|
|
55
55
|
## Supported AI Agents
|
|
56
56
|
|
|
57
|
-
| Agent | Rules File(s)
|
|
58
|
-
| ---------------- |
|
|
59
|
-
| AGENTS.md | `AGENTS.md`
|
|
60
|
-
| GitHub Copilot | `AGENTS.md`
|
|
61
|
-
| Claude Code | `CLAUDE.md`
|
|
62
|
-
| OpenAI Codex CLI | `AGENTS.md`
|
|
63
|
-
| Jules | `AGENTS.md`
|
|
64
|
-
| Cursor | `.cursor/rules/ruler_cursor_instructions.mdc`
|
|
65
|
-
| Windsurf | `.windsurf/
|
|
66
|
-
| Cline | `.clinerules`
|
|
57
|
+
| Agent | Rules File(s) | MCP Configuration / Notes |
|
|
58
|
+
| ---------------- | --------------------------------------------- | ------------------------------------------------ |
|
|
59
|
+
| AGENTS.md | `AGENTS.md` | (pseudo-agent ensuring root `AGENTS.md` exists) |
|
|
60
|
+
| GitHub Copilot | `AGENTS.md` | `.vscode/mcp.json` |
|
|
61
|
+
| Claude Code | `CLAUDE.md` | `.mcp.json` |
|
|
62
|
+
| OpenAI Codex CLI | `AGENTS.md` | `.codex/config.toml` |
|
|
63
|
+
| Jules | `AGENTS.md` | - |
|
|
64
|
+
| Cursor | `.cursor/rules/ruler_cursor_instructions.mdc` | `.cursor/mcp.json` |
|
|
65
|
+
| Windsurf | `AGENTS.md` | `.windsurf/mcp_config.json` |
|
|
66
|
+
| Cline | `.clinerules` | - |
|
|
67
67
|
| Crush | `CRUSH.md` | `.crush.json` |
|
|
68
68
|
| Amp | `AGENTS.md` | - |
|
|
69
69
|
| Amazon Q CLI | `.amazonq/rules/ruler_q_rules.md` | `.amazonq/mcp.json` |
|
|
@@ -82,6 +82,7 @@ Ruler solves this by providing a **single source of truth** for all your AI agen
|
|
|
82
82
|
| Trae AI | `.trae/rules/project_rules.md` | - |
|
|
83
83
|
| Warp | `WARP.md` | - |
|
|
84
84
|
| Kiro | `.kiro/steering/ruler_kiro_instructions.md` | - |
|
|
85
|
+
| Firebender | `firebender.json` | - |
|
|
85
86
|
|
|
86
87
|
## Getting Started
|
|
87
88
|
|
|
@@ -104,10 +105,11 @@ npx @intellectronica/ruler apply
|
|
|
104
105
|
1. Navigate to your project's root directory
|
|
105
106
|
2. Run `ruler init`
|
|
106
107
|
3. This creates:
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
108
|
+
|
|
109
|
+
- `.ruler/` directory
|
|
110
|
+
- `.ruler/AGENTS.md`: The primary starter Markdown file for your rules
|
|
111
|
+
- `.ruler/ruler.toml`: The main configuration file for Ruler (now contains sample MCP server sections; legacy `.ruler/mcp.json` no longer scaffolded)
|
|
112
|
+
- (Optional legacy fallback) If you previously used `.ruler/instructions.md`, it is still respected when `AGENTS.md` is absent. (The prior runtime warning was removed.)
|
|
111
113
|
|
|
112
114
|
Additionally, you can create a global configuration to use when no local `.ruler/` directory is found:
|
|
113
115
|
|
|
@@ -159,7 +161,15 @@ project/
|
|
|
159
161
|
|
|
160
162
|
- Discover all `.ruler/` directories in the project hierarchy
|
|
161
163
|
- Load and concatenate rules from each directory in order
|
|
162
|
-
-
|
|
164
|
+
- Decide whether nested mode is enabled using the following precedence:
|
|
165
|
+
1. `ruler apply --nested` (or `--no-nested`) takes top priority
|
|
166
|
+
2. `nested = true` in `ruler.toml`
|
|
167
|
+
3. Default to disabled when neither option is provided
|
|
168
|
+
- When a run is nested, downstream configs are forced to keep `nested = true`. If a child config attempts to disable it, Ruler keeps nested processing active and emits a warning in the logs.
|
|
169
|
+
- Nested processing carries forward each directory's own MCP bundle and configuration settings so that generated files remain scoped to their source directories while being normalized back to the project root.
|
|
170
|
+
|
|
171
|
+
> [!CAUTION]
|
|
172
|
+
> Nested mode is experimental and may change in future releases. The CLI logs this warning the first time a nested run is detected so you know the behavior may evolve.
|
|
163
173
|
|
|
164
174
|
**Perfect for:**
|
|
165
175
|
|
|
@@ -211,21 +221,22 @@ The `apply` command looks for `.ruler/` in the current directory tree, reading t
|
|
|
211
221
|
|
|
212
222
|
### Options
|
|
213
223
|
|
|
214
|
-
| Option | Description
|
|
215
|
-
| ------------------------------ |
|
|
216
|
-
| `--project-root <path>` |
|
|
217
|
-
| `--agents <agent1,agent2,...>` | Comma-separated
|
|
218
|
-
| `--config <path>` |
|
|
219
|
-
| `--mcp` / `--with-mcp` | Enable applying MCP server configurations (default: true)
|
|
220
|
-
| `--no-mcp` | Disable applying MCP server configurations
|
|
221
|
-
| `--mcp-overwrite` | Overwrite native MCP config
|
|
222
|
-
| `--gitignore` | Enable automatic .gitignore updates (default: true)
|
|
223
|
-
| `--no-gitignore` | Disable automatic .gitignore updates
|
|
224
|
-
| `--nested` | Enable nested rule loading
|
|
225
|
-
| `--
|
|
226
|
-
| `--
|
|
227
|
-
| `--
|
|
228
|
-
| `--
|
|
224
|
+
| Option | Description |
|
|
225
|
+
| ------------------------------ | ---------------------------------------------------------------------- |
|
|
226
|
+
| `--project-root <path>` | Project root path (default: current directory). |
|
|
227
|
+
| `--agents <agent1,agent2,...>` | Comma-separated agent names to target (see supported list below). |
|
|
228
|
+
| `--config <path>` | Custom `ruler.toml` path. |
|
|
229
|
+
| `--mcp` / `--with-mcp` | Enable applying MCP server configurations (default: true). |
|
|
230
|
+
| `--no-mcp` | Disable applying MCP server configurations. |
|
|
231
|
+
| `--mcp-overwrite` | Overwrite native MCP config instead of merging. |
|
|
232
|
+
| `--gitignore` | Enable automatic .gitignore updates (default: true). |
|
|
233
|
+
| `--no-gitignore` | Disable automatic .gitignore updates. |
|
|
234
|
+
| `--nested` | Enable nested rule loading (default: inherit from config or disabled). |
|
|
235
|
+
| `--no-nested` | Disable nested rule loading even if `nested = true` in config. |
|
|
236
|
+
| `--backup` | Toggle creation of `.bak` backup files (default: enabled). |
|
|
237
|
+
| `--dry-run` | Preview changes without writing files. |
|
|
238
|
+
| `--local-only` | Skip `$XDG_CONFIG_HOME` when looking for configuration. |
|
|
239
|
+
| `--verbose` / `-v` | Display detailed output during execution. |
|
|
229
240
|
|
|
230
241
|
### Common Examples
|
|
231
242
|
|
|
@@ -304,15 +315,15 @@ ruler revert [options]
|
|
|
304
315
|
|
|
305
316
|
### Options
|
|
306
317
|
|
|
307
|
-
| Option | Description
|
|
308
|
-
| ------------------------------ |
|
|
309
|
-
| `--project-root <path>` | Path to your project's root (default: current directory)
|
|
310
|
-
| `--agents <agent1,agent2,...>` | Comma-separated list of agent names to revert (agentsmd, aider, amazonqcli, amp, augmentcode, claude, cline, codex, copilot, crush, cursor, firebase, gemini-cli, goose, jules, junie, kilocode, kiro, opencode, openhands, qwen, roo, trae, warp, windsurf, zed) |
|
|
311
|
-
| `--config <path>` | Path to a custom `ruler.toml` configuration file
|
|
312
|
-
| `--keep-backups` | Keep backup files (.bak) after restoration (default: false)
|
|
313
|
-
| `--dry-run` | Preview changes without actually reverting files
|
|
314
|
-
| `--verbose` / `-v` | Display detailed output during execution
|
|
315
|
-
| `--local-only` | Only search for local .ruler directories, ignore global config
|
|
318
|
+
| Option | Description |
|
|
319
|
+
| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
320
|
+
| `--project-root <path>` | Path to your project's root (default: current directory) |
|
|
321
|
+
| `--agents <agent1,agent2,...>` | Comma-separated list of agent names to revert (agentsmd, aider, amazonqcli, amp, augmentcode, claude, cline, codex, copilot, crush, cursor, firebase, firebender, gemini-cli, goose, jules, junie, kilocode, kiro, opencode, openhands, qwen, roo, trae, warp, windsurf, zed) |
|
|
322
|
+
| `--config <path>` | Path to a custom `ruler.toml` configuration file |
|
|
323
|
+
| `--keep-backups` | Keep backup files (.bak) after restoration (default: false) |
|
|
324
|
+
| `--dry-run` | Preview changes without actually reverting files |
|
|
325
|
+
| `--verbose` / `-v` | Display detailed output during execution |
|
|
326
|
+
| `--local-only` | Only search for local .ruler directories, ignore global config |
|
|
316
327
|
|
|
317
328
|
### Common Examples
|
|
318
329
|
|
|
@@ -524,6 +535,7 @@ DEBUG = "1"
|
|
|
524
535
|
```
|
|
525
536
|
|
|
526
537
|
**Remote servers** require a `url` field (headers optional; bearer Authorization token auto-extracted for OpenHands when possible):
|
|
538
|
+
|
|
527
539
|
```toml
|
|
528
540
|
[mcp_servers.remote_server]
|
|
529
541
|
url = "https://api.example.com"
|
|
@@ -542,6 +554,135 @@ Ruler uses this configuration with the `merge` (default) or `overwrite` strategy
|
|
|
542
554
|
export CODEX_HOME="$(pwd)/.codex"
|
|
543
555
|
```
|
|
544
556
|
|
|
557
|
+
## Skills Support (Experimental)
|
|
558
|
+
|
|
559
|
+
**⚠️ Experimental Feature**: Skills support is currently experimental and requires `uv` (the Python package manager) to be installed on your system for MCP-based agent integration.
|
|
560
|
+
|
|
561
|
+
Ruler can manage and propagate Claude Code-compatible skills to supported AI agents. Skills are stored in `.ruler/skills/` and are automatically distributed to compatible agents when you run `ruler apply`.
|
|
562
|
+
|
|
563
|
+
### How It Works
|
|
564
|
+
|
|
565
|
+
Skills are specialized knowledge packages that extend AI agent capabilities with domain-specific expertise, workflows, or tool integrations. Ruler discovers skills in your `.ruler/skills/` directory and propagates them to compatible agents:
|
|
566
|
+
|
|
567
|
+
- **Claude Code agents**: Skills are copied to `.claude/skills/` in their native format
|
|
568
|
+
- **Other MCP-compatible agents**: Skills are copied to `.skillz/` and a Skillz MCP server is automatically configured via `uvx`
|
|
569
|
+
|
|
570
|
+
### Skills Directory Structure
|
|
571
|
+
|
|
572
|
+
Skills can be organized flat or nested:
|
|
573
|
+
|
|
574
|
+
```
|
|
575
|
+
.ruler/skills/
|
|
576
|
+
├── my-skill/
|
|
577
|
+
│ ├── SKILL.md # Required: skill instructions/knowledge
|
|
578
|
+
│ ├── helper.py # Optional: additional resources (scripts)
|
|
579
|
+
│ └── reference.md # Optional: additional resources (docs)
|
|
580
|
+
└── another-skill/
|
|
581
|
+
└── SKILL.md
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
Each skill must contain:
|
|
585
|
+
- `SKILL.md` - Primary skill file with instructions or knowledge base
|
|
586
|
+
|
|
587
|
+
Skills can optionally include additional resources like:
|
|
588
|
+
- Markdown files with supplementary documentation
|
|
589
|
+
- Python, JavaScript, or other scripts
|
|
590
|
+
- Configuration files or data
|
|
591
|
+
|
|
592
|
+
### Configuration
|
|
593
|
+
|
|
594
|
+
Skills support is **enabled by default** but can be controlled via:
|
|
595
|
+
|
|
596
|
+
**CLI flags:**
|
|
597
|
+
```bash
|
|
598
|
+
# Enable skills (default)
|
|
599
|
+
ruler apply --skills
|
|
600
|
+
|
|
601
|
+
# Disable skills
|
|
602
|
+
ruler apply --no-skills
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
**Configuration in `ruler.toml`:**
|
|
606
|
+
```toml
|
|
607
|
+
[skills]
|
|
608
|
+
enabled = true # or false to disable
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
### Skillz MCP Server
|
|
612
|
+
|
|
613
|
+
For agents that support MCP but don't have native skills support (all agents except Claude Code), Ruler automatically:
|
|
614
|
+
|
|
615
|
+
1. Copies skills to `.skillz/` directory
|
|
616
|
+
2. Configures a Skillz MCP server in the agent's configuration
|
|
617
|
+
3. Uses `uvx` to launch the server with the absolute path to `.skillz`
|
|
618
|
+
|
|
619
|
+
Example auto-generated MCP server configuration:
|
|
620
|
+
```toml
|
|
621
|
+
[mcp_servers.skillz]
|
|
622
|
+
command = "uvx"
|
|
623
|
+
args = ["skillz@latest", "/absolute/path/to/project/.skillz"]
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
### `.gitignore` Integration
|
|
627
|
+
|
|
628
|
+
When skills support is enabled and gitignore integration is active, Ruler automatically adds:
|
|
629
|
+
- `.claude/skills/` (for Claude Code agents)
|
|
630
|
+
- `.skillz/` (for MCP-based agents)
|
|
631
|
+
|
|
632
|
+
to your `.gitignore` file within the managed Ruler block.
|
|
633
|
+
|
|
634
|
+
### Requirements
|
|
635
|
+
|
|
636
|
+
- **For Claude Code**: No additional requirements
|
|
637
|
+
- **For MCP agents**: `uv` must be installed and available in your PATH
|
|
638
|
+
```bash
|
|
639
|
+
# Install uv if needed
|
|
640
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
### Validation
|
|
644
|
+
|
|
645
|
+
Ruler validates discovered skills and issues warnings for:
|
|
646
|
+
- Missing required file (`SKILL.md`)
|
|
647
|
+
- Invalid directory structures (directories without `SKILL.md` and no sub-skills)
|
|
648
|
+
|
|
649
|
+
Warnings don't prevent propagation but help identify potential issues.
|
|
650
|
+
|
|
651
|
+
### Dry-Run Mode
|
|
652
|
+
|
|
653
|
+
Test skills propagation without making changes:
|
|
654
|
+
```bash
|
|
655
|
+
ruler apply --dry-run
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
This shows which skills would be copied and which MCP servers would be configured.
|
|
659
|
+
|
|
660
|
+
### Example Workflow
|
|
661
|
+
|
|
662
|
+
```bash
|
|
663
|
+
# 1. Add a skill to your project
|
|
664
|
+
mkdir -p .ruler/skills/my-skill
|
|
665
|
+
cat > .ruler/skills/my-skill/SKILL.md << 'EOF'
|
|
666
|
+
# My Custom Skill
|
|
667
|
+
|
|
668
|
+
This skill provides specialized knowledge for...
|
|
669
|
+
|
|
670
|
+
## Usage
|
|
671
|
+
|
|
672
|
+
When working on this project, always follow these guidelines:
|
|
673
|
+
- Use TypeScript for all new code
|
|
674
|
+
- Write tests for all features
|
|
675
|
+
- Follow the existing code style
|
|
676
|
+
EOF
|
|
677
|
+
|
|
678
|
+
# 2. Apply to all agents (skills enabled by default)
|
|
679
|
+
ruler apply
|
|
680
|
+
|
|
681
|
+
# 3. Skills are now available to compatible agents:
|
|
682
|
+
# - Claude Code: .claude/skills/my-skill/
|
|
683
|
+
# - Other MCP agents: .skillz/my-skill/ + Skillz MCP server configured
|
|
684
|
+
```
|
|
685
|
+
|
|
545
686
|
## `.gitignore` Integration
|
|
546
687
|
|
|
547
688
|
Ruler automatically manages your `.gitignore` file to keep generated agent configuration files out of version control.
|
|
@@ -598,7 +739,7 @@ ruler apply
|
|
|
598
739
|
|
|
599
740
|
### Scenario 2: Complex Projects with Nested Rules
|
|
600
741
|
|
|
601
|
-
For large projects with multiple components or services,
|
|
742
|
+
For large projects with multiple components or services, enable nested rule loading so each directory keeps its own rules and MCP bundle:
|
|
602
743
|
|
|
603
744
|
```bash
|
|
604
745
|
# Set up nested .ruler directories
|
|
@@ -608,12 +749,25 @@ mkdir -p src/.ruler tests/.ruler docs/.ruler
|
|
|
608
749
|
echo "# API Design Guidelines" > src/.ruler/api_rules.md
|
|
609
750
|
echo "# Testing Best Practices" > tests/.ruler/test_rules.md
|
|
610
751
|
echo "# Documentation Standards" > docs/.ruler/docs_rules.md
|
|
752
|
+
```
|
|
611
753
|
|
|
612
|
-
|
|
613
|
-
ruler
|
|
754
|
+
```toml
|
|
755
|
+
# .ruler/ruler.toml
|
|
756
|
+
nested = true
|
|
614
757
|
```
|
|
615
758
|
|
|
616
|
-
|
|
759
|
+
```bash
|
|
760
|
+
# The CLI inherits nested mode from ruler.toml
|
|
761
|
+
ruler apply --verbose
|
|
762
|
+
|
|
763
|
+
# Override from the CLI at any time
|
|
764
|
+
ruler apply --no-nested
|
|
765
|
+
```
|
|
766
|
+
|
|
767
|
+
This creates context-specific instructions for different parts of your project while maintaining global rules in the root `.ruler/` directory. Nested runs automatically keep every nested config enabled even if a child tries to disable it.
|
|
768
|
+
|
|
769
|
+
> [!NOTE]
|
|
770
|
+
> The CLI prints "Nested mode is experimental and may change in future releases." the first time nested processing runs. Expect refinements in future versions.
|
|
617
771
|
|
|
618
772
|
### Scenario 3: Team Standardization
|
|
619
773
|
|
|
@@ -718,7 +872,7 @@ This shows:
|
|
|
718
872
|
A: Currently, all agents receive the same concatenated rules. For agent-specific instructions, include sections in your rule files like "## GitHub Copilot Specific" or "## Aider Configuration".
|
|
719
873
|
|
|
720
874
|
**Q: How do I set up different instructions for different parts of my project?**
|
|
721
|
-
A:
|
|
875
|
+
A: Enable nested mode either by setting `nested = true` in `ruler.toml` or by passing `ruler apply --nested`. The CLI inherits the config setting by default, but `--no-nested` always wins if you need to opt out for a run. Nested mode keeps loading rules (and MCP settings) from every `.ruler/` directory in the hierarchy, forces child configs to remain nested, and logs "Nested mode is experimental and may change in future releases." if any nested processing occurs.
|
|
722
876
|
|
|
723
877
|
**Q: How do I temporarily disable Ruler for an agent?**
|
|
724
878
|
A: Set `enabled = false` in `ruler.toml` under `[agents.agentname]`, or use `--agents` flag to specify only the agents you want.
|
|
@@ -80,5 +80,12 @@ class AbstractAgent {
|
|
|
80
80
|
supportsMcpRemote() {
|
|
81
81
|
return false;
|
|
82
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Returns whether this agent has native skills support.
|
|
85
|
+
* Defaults to false if not overridden.
|
|
86
|
+
*/
|
|
87
|
+
supportsNativeSkills() {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
83
90
|
}
|
|
84
91
|
exports.AbstractAgent = AbstractAgent;
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.FirebenderAgent = void 0;
|
|
37
|
+
const path = __importStar(require("path"));
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const FileSystemUtils_1 = require("../core/FileSystemUtils");
|
|
40
|
+
/**
|
|
41
|
+
* Firebender agent adapter.
|
|
42
|
+
*/
|
|
43
|
+
class FirebenderAgent {
|
|
44
|
+
/**
|
|
45
|
+
* Type guard function to safely check if an object is a FirebenderRule.
|
|
46
|
+
*/
|
|
47
|
+
isFirebenderRule(rule) {
|
|
48
|
+
return (typeof rule === 'object' &&
|
|
49
|
+
rule !== null &&
|
|
50
|
+
'filePathMatches' in rule &&
|
|
51
|
+
'rulesPaths' in rule &&
|
|
52
|
+
typeof rule.filePathMatches === 'string' &&
|
|
53
|
+
typeof rule.rulesPaths === 'string');
|
|
54
|
+
}
|
|
55
|
+
getIdentifier() {
|
|
56
|
+
return 'firebender';
|
|
57
|
+
}
|
|
58
|
+
getName() {
|
|
59
|
+
return 'Firebender';
|
|
60
|
+
}
|
|
61
|
+
async applyRulerConfig(concatenatedRules, projectRoot, rulerMcpJson, agentConfig, backup = true) {
|
|
62
|
+
const rulesPath = this.resolveOutputPath(projectRoot, agentConfig);
|
|
63
|
+
await (0, FileSystemUtils_1.ensureDirExists)(path.dirname(rulesPath));
|
|
64
|
+
const firebenderConfig = await this.loadExistingConfig(rulesPath);
|
|
65
|
+
const newRules = this.createRulesFromConcatenatedRules(concatenatedRules, projectRoot);
|
|
66
|
+
firebenderConfig.rules.push(...newRules);
|
|
67
|
+
this.removeDuplicateRules(firebenderConfig);
|
|
68
|
+
const mcpEnabled = agentConfig?.mcp?.enabled ?? true;
|
|
69
|
+
if (mcpEnabled && rulerMcpJson) {
|
|
70
|
+
await this.handleMcpConfiguration(firebenderConfig, rulerMcpJson, agentConfig);
|
|
71
|
+
}
|
|
72
|
+
await this.saveConfig(rulesPath, firebenderConfig, backup);
|
|
73
|
+
}
|
|
74
|
+
resolveOutputPath(projectRoot, agentConfig) {
|
|
75
|
+
const outputPaths = this.getDefaultOutputPath(projectRoot);
|
|
76
|
+
const output = agentConfig?.outputPath ??
|
|
77
|
+
agentConfig?.outputPathInstructions ??
|
|
78
|
+
outputPaths['instructions'];
|
|
79
|
+
return path.resolve(projectRoot, output);
|
|
80
|
+
}
|
|
81
|
+
async loadExistingConfig(rulesPath) {
|
|
82
|
+
try {
|
|
83
|
+
const existingContent = await fs.promises.readFile(rulesPath, 'utf8');
|
|
84
|
+
const config = JSON.parse(existingContent);
|
|
85
|
+
if (!config.rules) {
|
|
86
|
+
config.rules = [];
|
|
87
|
+
}
|
|
88
|
+
return config;
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
if (error &&
|
|
92
|
+
typeof error === 'object' &&
|
|
93
|
+
'code' in error &&
|
|
94
|
+
error.code === 'ENOENT') {
|
|
95
|
+
return { rules: [] };
|
|
96
|
+
}
|
|
97
|
+
console.warn(`Failed to read/parse existing firebender.json: ${error}`);
|
|
98
|
+
return { rules: [] };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
createRulesFromConcatenatedRules(concatenatedRules, projectRoot) {
|
|
102
|
+
const filePaths = this.extractFilePathsFromRules(concatenatedRules, projectRoot);
|
|
103
|
+
if (filePaths.length > 0) {
|
|
104
|
+
return this.createRuleObjectsFromFilePaths(filePaths);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
return this.createRulesFromPlainText(concatenatedRules);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
createRuleObjectsFromFilePaths(filePaths) {
|
|
111
|
+
return filePaths.map((filePath) => ({
|
|
112
|
+
filePathMatches: '**/*',
|
|
113
|
+
rulesPaths: filePath,
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
116
|
+
createRulesFromPlainText(concatenatedRules) {
|
|
117
|
+
return concatenatedRules.split('\n').filter((rule) => rule.trim());
|
|
118
|
+
}
|
|
119
|
+
removeDuplicateRules(firebenderConfig) {
|
|
120
|
+
const seen = new Set();
|
|
121
|
+
firebenderConfig.rules = firebenderConfig.rules.filter((rule) => {
|
|
122
|
+
let key;
|
|
123
|
+
if (this.isFirebenderRule(rule)) {
|
|
124
|
+
const filePathMatchesPart = rule.filePathMatches;
|
|
125
|
+
const rulesPathsPart = rule.rulesPaths;
|
|
126
|
+
key = `${filePathMatchesPart}::${rulesPathsPart}`;
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
key = String(rule);
|
|
130
|
+
}
|
|
131
|
+
if (seen.has(key)) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
seen.add(key);
|
|
135
|
+
return true;
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
async saveConfig(rulesPath, config, backup) {
|
|
139
|
+
const updatedContent = JSON.stringify(config, null, 2);
|
|
140
|
+
if (backup) {
|
|
141
|
+
await (0, FileSystemUtils_1.backupFile)(rulesPath);
|
|
142
|
+
}
|
|
143
|
+
await (0, FileSystemUtils_1.writeGeneratedFile)(rulesPath, updatedContent);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Handle MCP server configuration for Firebender.
|
|
147
|
+
* Merges or overwrites MCP servers in the firebender.json configuration based on strategy.
|
|
148
|
+
*/
|
|
149
|
+
async handleMcpConfiguration(firebenderConfig, rulerMcpJson, agentConfig) {
|
|
150
|
+
const strategy = agentConfig?.mcp?.strategy ?? 'merge';
|
|
151
|
+
const incomingServers = rulerMcpJson.mcpServers || {};
|
|
152
|
+
if (!firebenderConfig.mcpServers) {
|
|
153
|
+
firebenderConfig.mcpServers = {};
|
|
154
|
+
}
|
|
155
|
+
if (strategy === 'overwrite') {
|
|
156
|
+
firebenderConfig.mcpServers = { ...incomingServers };
|
|
157
|
+
}
|
|
158
|
+
else if (strategy === 'merge') {
|
|
159
|
+
const existingServers = firebenderConfig.mcpServers || {};
|
|
160
|
+
firebenderConfig.mcpServers = { ...existingServers, ...incomingServers };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
getDefaultOutputPath(projectRoot) {
|
|
164
|
+
return {
|
|
165
|
+
instructions: path.join(projectRoot, 'firebender.json'),
|
|
166
|
+
mcp: path.join(projectRoot, 'firebender.json'),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
getMcpServerKey() {
|
|
170
|
+
return 'mcpServers';
|
|
171
|
+
}
|
|
172
|
+
supportsMcpStdio() {
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
supportsMcpRemote() {
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Extracts file paths from concatenated rules by parsing HTML source comments.
|
|
180
|
+
* @param concatenatedRules The concatenated rules string with HTML comments
|
|
181
|
+
* @param projectRoot The project root directory
|
|
182
|
+
* @returns Array of file paths relative to project root
|
|
183
|
+
*/
|
|
184
|
+
extractFilePathsFromRules(concatenatedRules, projectRoot) {
|
|
185
|
+
const sourceCommentRegex = /<!-- Source: (.+?) -->/g;
|
|
186
|
+
const filePaths = [];
|
|
187
|
+
let match;
|
|
188
|
+
while ((match = sourceCommentRegex.exec(concatenatedRules)) !== null) {
|
|
189
|
+
const relativePath = match[1];
|
|
190
|
+
const absolutePath = path.resolve(projectRoot, relativePath);
|
|
191
|
+
const normalizedProjectRoot = path.resolve(projectRoot);
|
|
192
|
+
// Ensure the absolutePath is within the project root (cross-platform compatible)
|
|
193
|
+
// This prevents path traversal attacks while handling Windows/Unix path differences
|
|
194
|
+
const isWithinProject = absolutePath.startsWith(normalizedProjectRoot) &&
|
|
195
|
+
(absolutePath.length === normalizedProjectRoot.length ||
|
|
196
|
+
absolutePath[normalizedProjectRoot.length] === path.sep);
|
|
197
|
+
if (isWithinProject) {
|
|
198
|
+
const projectRelativePath = path.relative(projectRoot, absolutePath);
|
|
199
|
+
filePaths.push(projectRelativePath);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return filePaths;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
exports.FirebenderAgent = FirebenderAgent;
|