Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Example/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
**/.cursor/commands/ai-rules/
**/.cursor/mcp.json
**/.cursor/skills/ai-rules-generated-*
**/.firebender/skills/ai-rules-generated-*
**/.firebender/commands/*-ai-rules.mdc
**/.gemini/settings.json
**/.mcp.json
**/.roo/mcp.json
Expand Down
6 changes: 4 additions & 2 deletions docs/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
| **Cline** | `.clinerules/AGENTS.md` -> inlined file | `.clinerules/AGENTS.md` -> `../ai-rules/AGENTS.md` | - | |
| **Codex** | `AGENTS.md` -> inlined file | `AGENTS.md` -> `ai-rules/AGENTS.md` | - | |
| **Copilot** | `AGENTS.md` -> inlined file | `AGENTS.md` -> `ai-rules/AGENTS.md` | - | |
| **Cursor** | `AGENTS.md` -> inlined file | `AGENTS.md` -> `ai-rules/AGENTS.md` | `.cursor/mcp.json` | |
| **Firebender** | `firebender.json` | `firebender.json` (references `ai-rules/AGENTS.md`) | Embedded in `firebender.json` | Supports overlay files |
| **Cursor** | `.cursor/rules/*.mdc` | `AGENTS.md` -> `ai-rules/AGENTS.md` | `.cursor/mcp.json` | Symlink mode: only project root level |
| **Firebender** | `AGENTS.md` -> inlined file | `AGENTS.md` -> `ai-rules/AGENTS.md` | `firebender.json` | Commands live in `.firebender/`; skills reuse `.agents/skills`; overlay supported |
| **Gemini** | `GEMINI.md` -> inlined file | `GEMINI.md` -> `ai-rules/AGENTS.md` | Embedded in `.gemini/settings.json` | |
| **Goose** | `AGENTS.md` -> inlined file | `AGENTS.md` -> `ai-rules/AGENTS.md` | - | |
| **Kilocode** | `.kilocode/rules/AGENTS.md` -> inlined file | `.kilocode/rules/AGENTS.md` -> `../../ai-rules/AGENTS.md` | - | |
Expand All @@ -23,4 +23,6 @@ Firebender supports overlay configuration files. To customize your configuration
1. Create `ai-rules/firebender-overlay.json` in the same parent directory as your generated `firebender.json`
2. Any values defined in the overlay file will be merged into the base configuration, with overlay values taking precedence

`firebender.json` is generated as supplemental config when `ai-rules/mcp.json` and/or `ai-rules/firebender-overlay.json` exists.

**MCP Integration:** If you have `ai-rules/mcp.json`, the MCP servers are merged into `firebender.json` first, then the overlay is applied. This allows you to override MCP configurations in the overlay if needed.
3 changes: 2 additions & 1 deletion docs/commands-and-skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Command files support optional YAML frontmatter:
| **AMP** | `.agents/commands/{name}-ai-rules.md` | Stripped |
| **Claude Code** | `.claude/commands/ai-rules/*.md` | Preserved |
| **Cursor** | `.cursor/commands/ai-rules/*.md` | Stripped |
| **Firebender** | `firebender.json` (commands array) | Stripped |
| **Firebender** | `.firebender/commands/{name}-ai-rules.mdc` | Preserved |

### Documentation

Expand Down Expand Up @@ -79,5 +79,6 @@ When you run `ai-rules generate`, symlinks are created:
| Claude | `.claude/skills/ai-rules-generated-debugging` -> `../../ai-rules/skills/debugging` |
| Codex | `.codex/skills/ai-rules-generated-debugging` -> `../../ai-rules/skills/debugging` |
| Cursor | `.cursor/skills/ai-rules-generated-debugging` -> `../../ai-rules/skills/debugging` |
| Firebender | `.agents/skills/ai-rules-generated-debugging` -> `../../ai-rules/skills/debugging` |

Skill folders without a `SKILL.md` file are skipped with a warning.
2 changes: 1 addition & 1 deletion docs/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,6 @@ See the [Supported Agents](agents.md) table for which agents support MCP and the
|-------|-------------------|
| Claude Code | `.mcp.json` |
| Cursor | `.cursor/mcp.json` |
| Firebender | Embedded in `firebender.json` |
| Firebender | `firebender.json` (generated when MCP and/or overlay config exists) |
| Gemini | Embedded in `.gemini/settings.json` |
| Roo | `.roo/mcp.json` |
12 changes: 10 additions & 2 deletions docs/project-structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,12 @@ monorepo/
├── CLAUDE.md # Symlink -> ai-rules/.generated-ai-rules/ai-rules-generated-AGENTS.md
├── AGENTS.md # Symlink -> ai-rules/.generated-ai-rules/ai-rules-generated-AGENTS.md
├── .clinerules/ # Root Cline rules (symlink)
├── .firebender/
│ ├── commands/ # Firebender commands (generated symlinks)
├── .agents/
│ └── skills/ # Shared AMP/Firebender skills (generated symlinks)
├── .mcp.json # Root MCP config (generated)
└── firebender.json # Root Firebender config (generated)
└── firebender.json # Root Firebender supplemental config (generated when needed)
```

## Symlink Mode
Expand All @@ -62,7 +66,11 @@ project/
├── CLAUDE.md # Symlink -> ai-rules/AGENTS.md
├── GEMINI.md # Symlink -> ai-rules/AGENTS.md
├── AGENTS.md # Symlink -> ai-rules/AGENTS.md
├── firebender.json # References ai-rules/AGENTS.md
├── .firebender/
│ ├── commands/ # Firebender commands (generated symlinks)
├── .agents/
│ └── skills/ # Shared AMP/Firebender skills (generated symlinks)
├── firebender.json # Supplemental Firebender config (if mcp.json or overlay exists)
├── .clinerules/
│ └── AGENTS.md # Symlink -> ../ai-rules/AGENTS.md
├── .cursor/
Expand Down
2 changes: 0 additions & 2 deletions docs/rule-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,6 @@ In Standard Mode, `ai-rules generate` produces a single inlined file at `ai-rule

Most agent output files (e.g., `CLAUDE.md`, `AGENTS.md`, `GEMINI.md`) are created as **symlinks** pointing to this inlined file. This ensures every agent reads the same complete content without needing `@` file expansion support.

Firebender generates its own JSON format and does not use the inlined file. Cursor uses the inlined `AGENTS.md` path.

## Symlink Mode

Use Symlink Mode for simple setups where all agents share the same rules.
Expand Down
55 changes: 47 additions & 8 deletions src/agents/external_commands_generator.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use crate::agents::command_generator::CommandGeneratorTrait;
use crate::operations::command_reader::{
check_command_symlinks_in_subdir_in_sync, check_command_symlinks_in_sync,
create_command_symlinks, create_command_symlinks_in_subdir, get_command_gitignore_patterns,
get_command_gitignore_patterns_subdir, remove_command_symlinks_in_subdir,
remove_generated_command_symlinks,
check_command_symlinks_in_subdir_in_sync, check_command_symlinks_in_sync_with_extension,
create_command_symlinks_in_subdir, create_command_symlinks_with_extension,
get_command_gitignore_patterns_subdir, get_command_gitignore_patterns_with_extension,
remove_command_symlinks_in_subdir, remove_generated_command_symlinks_with_extension,
};
use anyhow::Result;
use std::path::{Path, PathBuf};
Expand All @@ -13,6 +13,7 @@ pub struct ExternalCommandsGenerator {
/// Optional subdirectory for symlinks (e.g., "ai-rules" for .claude/commands/ai-rules/)
/// When None, uses flat structure with -ai-rules.md suffix
subdir: Option<String>,
extension: String,
}

impl ExternalCommandsGenerator {
Expand All @@ -21,6 +22,16 @@ impl ExternalCommandsGenerator {
Self {
target_dir: target_dir.to_string(),
subdir: None,
extension: "md".to_string(),
}
}

/// Create a generator with flat structure and a custom extension.
pub fn with_extension(target_dir: &str, extension: &str) -> Self {
Self {
target_dir: target_dir.to_string(),
subdir: None,
extension: extension.to_string(),
}
}

Expand All @@ -29,6 +40,7 @@ impl ExternalCommandsGenerator {
Self {
target_dir: target_dir.to_string(),
subdir: Some(subdir.to_string()),
extension: "md".to_string(),
}
}
}
Expand All @@ -39,7 +51,11 @@ impl CommandGeneratorTrait for ExternalCommandsGenerator {
Some(subdir) => {
create_command_symlinks_in_subdir(current_dir, &self.target_dir, subdir)
}
None => create_command_symlinks(current_dir, &self.target_dir),
None => create_command_symlinks_with_extension(
current_dir,
&self.target_dir,
&self.extension,
),
}
}

Expand All @@ -48,7 +64,11 @@ impl CommandGeneratorTrait for ExternalCommandsGenerator {
Some(subdir) => {
remove_command_symlinks_in_subdir(current_dir, &self.target_dir, subdir)
}
None => remove_generated_command_symlinks(current_dir, &self.target_dir),
None => remove_generated_command_symlinks_with_extension(
current_dir,
&self.target_dir,
&self.extension,
),
}
}

Expand All @@ -57,14 +77,20 @@ impl CommandGeneratorTrait for ExternalCommandsGenerator {
Some(subdir) => {
check_command_symlinks_in_subdir_in_sync(current_dir, &self.target_dir, subdir)
}
None => check_command_symlinks_in_sync(current_dir, &self.target_dir),
None => check_command_symlinks_in_sync_with_extension(
current_dir,
&self.target_dir,
&self.extension,
),
}
}

fn command_gitignore_patterns(&self) -> Vec<String> {
match &self.subdir {
Some(subdir) => get_command_gitignore_patterns_subdir(&self.target_dir, subdir),
None => get_command_gitignore_patterns(&self.target_dir),
None => {
get_command_gitignore_patterns_with_extension(&self.target_dir, &self.extension)
}
}
}
}
Expand All @@ -91,6 +117,7 @@ mod tests {
let generator = ExternalCommandsGenerator::new(".agents/commands");
assert_eq!(generator.target_dir, ".agents/commands");
assert!(generator.subdir.is_none());
assert_eq!(generator.extension, "md");
}

#[test]
Expand Down Expand Up @@ -178,6 +205,7 @@ mod tests {
let generator = ExternalCommandsGenerator::with_subdir(".claude/commands", "ai-rules");
assert_eq!(generator.target_dir, ".claude/commands");
assert_eq!(generator.subdir, Some("ai-rules".to_string()));
assert_eq!(generator.extension, "md");
}

#[test]
Expand Down Expand Up @@ -287,5 +315,16 @@ mod tests {
GENERATED_COMMAND_SUFFIX
)]
);

// Firebender uses flat structure with .mdc files
let firebender_gen =
ExternalCommandsGenerator::with_extension(".firebender/commands", "mdc");
assert_eq!(
firebender_gen.command_gitignore_patterns(),
vec![format!(
".firebender/commands/*-{}.mdc",
GENERATED_COMMAND_SUFFIX
)]
);
}
}
Loading
Loading