Skip to main content

agent_rules_tool/
discover.rs

1//! Known tool rule directories and format inference from paths.
2
3use crate::RuleFormat;
4use std::path::{Path, PathBuf};
5
6/// Metadata for a known agent tool rule directory.
7#[derive(Debug, Clone)]
8pub struct ToolRuleDir {
9    /// Relative path segment (e.g. `.cursor/rules`).
10    pub path: &'static str,
11    /// Rule format stored in this directory.
12    pub format: RuleFormat,
13    /// Allowed file extensions when no suffix is set.
14    pub extensions: &'static [&'static str],
15    /// Optional filename suffix (e.g. Copilot `.instructions.md`).
16    pub suffix: Option<&'static str>,
17}
18
19/// Known tool rule directories from [mapping.md](https://github.com/rameshsunkara/agent-rules-spec/blob/main/compatibility/mapping.md).
20pub const TOOL_RULE_DIRS: &[ToolRuleDir] = &[
21    ToolRuleDir {
22        path: ".agents/rules",
23        format: RuleFormat::Agents,
24        extensions: &["md"],
25        suffix: None,
26    },
27    ToolRuleDir {
28        path: ".cursor/rules",
29        format: RuleFormat::Cursor,
30        extensions: &["mdc", "md"],
31        suffix: None,
32    },
33    ToolRuleDir {
34        path: ".windsurf/rules",
35        format: RuleFormat::Windsurf,
36        extensions: &["md"],
37        suffix: None,
38    },
39    ToolRuleDir {
40        path: ".github/instructions",
41        format: RuleFormat::Copilot,
42        extensions: &["md"],
43        suffix: Some(".instructions.md"),
44    },
45    ToolRuleDir {
46        path: ".clinerules",
47        format: RuleFormat::Cline,
48        extensions: &["md"],
49        suffix: None,
50    },
51    ToolRuleDir {
52        path: ".claude/rules",
53        format: RuleFormat::Claude,
54        extensions: &["md"],
55        suffix: None,
56    },
57    ToolRuleDir {
58        path: ".aiassistant/rules",
59        format: RuleFormat::Jetbrains,
60        extensions: &["md"],
61        suffix: None,
62    },
63    ToolRuleDir {
64        path: ".amazonq/rules",
65        format: RuleFormat::AmazonQ,
66        extensions: &["md"],
67        suffix: None,
68    },
69];
70
71/// Infer [`RuleFormat`] when `path` contains a known tool directory segment.
72pub fn infer_format_from_path(path: &Path) -> Option<RuleFormat> {
73    let path_str = path.to_string_lossy().replace('\\', "/");
74    for dir in TOOL_RULE_DIRS {
75        if path_str.contains(dir.path) {
76            return Some(dir.format);
77        }
78    }
79    None
80}
81
82/// Return the [`ToolRuleDir`] entry matching `path`, if any.
83pub fn find_tool_dir(path: &Path) -> Option<&'static ToolRuleDir> {
84    let path_str = path.to_string_lossy().replace('\\', "/");
85    TOOL_RULE_DIRS
86        .iter()
87        .find(|dir| path_str.contains(dir.path))
88}
89
90/// List tool rule directories that exist under `project_root`.
91pub fn existing_tool_dirs(project_root: &Path) -> Vec<(PathBuf, &'static ToolRuleDir)> {
92    TOOL_RULE_DIRS
93        .iter()
94        .filter_map(|dir| {
95            let full = project_root.join(dir.path);
96            if full.exists() {
97                Some((full, dir))
98            } else {
99                None
100            }
101        })
102        .collect()
103}