1use std::collections::HashMap;
6use std::fs;
7use std::path::{Path, PathBuf};
8
9use regex::Regex;
10
11use super::types::{AgentsMdSection, CustomRule, ProjectRules, RuleAction};
12
13const AGENTS_MD_FILES: &[&str] = &[
15 "AGENTS.md",
16 ".agents.md",
17 "agents.md",
18 ".aster/AGENTS.md",
19 ".aster/instructions.md",
20];
21
22const SETTINGS_FILES: &[&str] = &[".aster/settings.json", ".aster/settings.local.json"];
24
25pub fn find_agents_md(start_dir: Option<&Path>) -> Option<PathBuf> {
27 let mut dir = start_dir
28 .map(|p| p.to_path_buf())
29 .unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
30
31 loop {
33 for filename in AGENTS_MD_FILES {
34 let file_path = dir.join(filename);
35 if file_path.exists() {
36 return Some(file_path);
37 }
38 }
39
40 match dir.parent() {
41 Some(parent) if parent != dir => dir = parent.to_path_buf(),
42 _ => break,
43 }
44 }
45
46 if let Some(home) = dirs::home_dir() {
48 let home_agents_md = home.join(".aster").join("AGENTS.md");
49 if home_agents_md.exists() {
50 return Some(home_agents_md);
51 }
52 }
53
54 None
55}
56
57pub fn find_settings_files(start_dir: Option<&Path>) -> Vec<PathBuf> {
59 let dir = start_dir
60 .map(|p| p.to_path_buf())
61 .unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
62
63 let mut found = Vec::new();
64
65 for filename in SETTINGS_FILES {
67 let file_path = dir.join(filename);
68 if file_path.exists() {
69 found.push(file_path);
70 }
71 }
72
73 if let Some(home) = dirs::home_dir() {
75 let global_settings = home.join(".aster").join("settings.json");
76 if global_settings.exists() {
77 found.push(global_settings);
78 }
79 }
80
81 found
82}
83
84pub fn parse_agents_md(file_path: &Path) -> Vec<AgentsMdSection> {
86 let content = match fs::read_to_string(file_path) {
87 Ok(c) => c,
88 Err(_) => return Vec::new(),
89 };
90
91 let mut sections = Vec::new();
92 let lines: Vec<&str> = content.lines().collect();
93
94 let heading_re = Regex::new(r"^(#{1,6})\s+(.+)$").unwrap();
95
96 let mut current_section: Option<AgentsMdSection> = None;
97 let mut content_lines: Vec<&str> = Vec::new();
98
99 for line in lines {
100 if let Some(caps) = heading_re.captures(line) {
101 if let Some(mut section) = current_section.take() {
103 section.content = content_lines.join("\n").trim().to_string();
104 sections.push(section);
105 }
106
107 current_section = Some(AgentsMdSection {
109 title: caps.get(2).unwrap().as_str().trim().to_string(),
110 content: String::new(),
111 level: caps.get(1).unwrap().as_str().len(),
112 });
113 content_lines.clear();
114 } else if current_section.is_some() {
115 content_lines.push(line);
116 } else if !line.trim().is_empty() {
117 current_section = Some(AgentsMdSection {
119 title: "Instructions".to_string(),
120 content: String::new(),
121 level: 0,
122 });
123 content_lines.push(line);
124 }
125 }
126
127 if let Some(mut section) = current_section {
129 section.content = content_lines.join("\n").trim().to_string();
130 sections.push(section);
131 }
132
133 sections
134}
135
136pub fn extract_rules(sections: &[AgentsMdSection]) -> ProjectRules {
138 let mut rules = ProjectRules::default();
139
140 for section in sections {
141 let title_lower = section.title.to_lowercase();
142
143 if title_lower.contains("instruction") || section.level == 0 {
144 let instructions = rules.instructions.get_or_insert_with(String::new);
145 instructions.push_str(§ion.content);
146 instructions.push('\n');
147 } else if title_lower.contains("allowed tool") {
148 rules.allowed_tools = Some(parse_list_from_content(§ion.content));
149 } else if title_lower.contains("disallowed tool") || title_lower.contains("forbidden tool")
150 {
151 rules.disallowed_tools = Some(parse_list_from_content(§ion.content));
152 } else if title_lower.contains("permission") {
153 let mode = section.content.lines().next().unwrap_or("").trim();
154 if ["default", "acceptEdits", "bypassPermissions", "plan"].contains(&mode) {
155 rules.permission_mode = Some(mode.to_string());
156 }
157 } else if title_lower.contains("model") {
158 rules.model = section.content.lines().next().map(|s| s.trim().to_string());
159 } else if title_lower.contains("system prompt") {
160 rules.system_prompt = Some(section.content.clone());
161 } else if title_lower.contains("rule") {
162 rules.custom_rules = Some(parse_custom_rules(§ion.content));
163 } else if title_lower.contains("memory") || title_lower.contains("context") {
164 rules.memory = Some(parse_memory_from_content(§ion.content));
165 }
166 }
167
168 rules
169}
170
171fn parse_list_from_content(content: &str) -> Vec<String> {
173 let list_re = Regex::new(r"^\s*[-*+]\s+(.+)$").unwrap();
174 let mut items = Vec::new();
175
176 for line in content.lines() {
177 if let Some(caps) = list_re.captures(line) {
178 items.push(caps.get(1).unwrap().as_str().trim().to_string());
179 }
180 }
181
182 items
183}
184
185fn parse_custom_rules(content: &str) -> Vec<CustomRule> {
187 let rule_re = Regex::new(r"^\s*[-*+]\s+\*\*(.+?)\*\*:\s*(.+)$").unwrap();
188 let action_re = Regex::new(r"(?i)action:\s*(allow|deny|warn|transform)").unwrap();
189 let pattern_re = Regex::new(r"(?i)pattern:\s*(.+)").unwrap();
190
191 let mut rules = Vec::new();
192 let mut current_rule: Option<CustomRule> = None;
193
194 for line in content.lines() {
195 if let Some(caps) = rule_re.captures(line) {
196 if let Some(rule) = current_rule.take() {
198 rules.push(rule);
199 }
200
201 current_rule = Some(CustomRule {
202 name: caps.get(1).unwrap().as_str().trim().to_string(),
203 pattern: None,
204 action: RuleAction::Warn,
205 message: Some(caps.get(2).unwrap().as_str().trim().to_string()),
206 transform: None,
207 });
208 } else if let Some(ref mut rule) = current_rule {
209 if let Some(caps) = action_re.captures(line) {
210 rule.action = match caps.get(1).unwrap().as_str().to_lowercase().as_str() {
211 "allow" => RuleAction::Allow,
212 "deny" => RuleAction::Deny,
213 "transform" => RuleAction::Transform,
214 _ => RuleAction::Warn,
215 };
216 }
217
218 if let Some(caps) = pattern_re.captures(line) {
219 rule.pattern = Some(caps.get(1).unwrap().as_str().trim().to_string());
220 }
221 }
222 }
223
224 if let Some(rule) = current_rule {
225 rules.push(rule);
226 }
227
228 rules
229}
230
231fn parse_memory_from_content(content: &str) -> HashMap<String, String> {
233 let memory_re = Regex::new(r"^\s*[-*+]\s+\*\*(.+?)\*\*:\s*(.+)$").unwrap();
234 let mut memory = HashMap::new();
235
236 for line in content.lines() {
237 if let Some(caps) = memory_re.captures(line) {
238 memory.insert(
239 caps.get(1).unwrap().as_str().trim().to_string(),
240 caps.get(2).unwrap().as_str().trim().to_string(),
241 );
242 }
243 }
244
245 memory
246}
247
248pub fn load_project_rules(project_dir: Option<&Path>) -> ProjectRules {
250 let dir = project_dir
251 .map(|p| p.to_path_buf())
252 .unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
253
254 let mut rules = ProjectRules::default();
255
256 if let Some(agents_md_path) = find_agents_md(Some(&dir)) {
258 let sections = parse_agents_md(&agents_md_path);
259 rules = merge_rules(rules, extract_rules(§ions));
260 }
261
262 for settings_path in find_settings_files(Some(&dir)) {
264 if let Ok(content) = fs::read_to_string(&settings_path) {
265 if let Ok(settings) = serde_json::from_str::<ProjectRules>(&content) {
266 rules = merge_rules(rules, settings);
267 }
268 }
269 }
270
271 rules
272}
273
274fn merge_rules(base: ProjectRules, override_rules: ProjectRules) -> ProjectRules {
276 ProjectRules {
277 instructions: override_rules.instructions.or(base.instructions),
278 allowed_tools: override_rules.allowed_tools.or(base.allowed_tools),
279 disallowed_tools: override_rules.disallowed_tools.or(base.disallowed_tools),
280 permission_mode: override_rules.permission_mode.or(base.permission_mode),
281 model: override_rules.model.or(base.model),
282 system_prompt: override_rules.system_prompt.or(base.system_prompt),
283 custom_rules: match (base.custom_rules, override_rules.custom_rules) {
284 (Some(mut b), Some(o)) => {
285 b.extend(o);
286 Some(b)
287 }
288 (b, o) => o.or(b),
289 },
290 memory: match (base.memory, override_rules.memory) {
291 (Some(mut b), Some(o)) => {
292 b.extend(o);
293 Some(b)
294 }
295 (b, o) => o.or(b),
296 },
297 }
298}