1use crate::agent::{AgentConfig, AgentFrontmatter};
2use anyhow::Context;
3use dirs;
4use glob::glob;
5use std::path::{Path, PathBuf};
6
7pub struct AgentLoader {
8 discovery_paths: Vec<PathBuf>,
9}
10
11impl AgentLoader {
12 pub fn new() -> anyhow::Result<Self> {
13 let cwd = std::env::current_dir()?;
14
15 let discovery_paths = vec![
16 cwd.join(".agents/agents"),
18 cwd.join(".claude/agents"),
19 dirs::home_dir()
21 .map(|h| h.join(".agents/agents"))
22 .unwrap_or_else(|| PathBuf::from("~/.agents/agents")),
23 dirs::home_dir()
24 .map(|h| h.join(".claude/agents"))
25 .unwrap_or_else(|| PathBuf::from("~/.claude/agents")),
26 ];
27
28 Ok(Self { discovery_paths })
29 }
30
31 pub fn load_agents(&self) -> anyhow::Result<Vec<AgentConfig>> {
32 let mut agents = vec![
33 AgentConfig::builtin_build(),
35 AgentConfig::builtin_plan(),
36 AgentConfig::builtin_explorer(),
37 AgentConfig::builtin_general(),
38 ];
39
40 for path in &self.discovery_paths {
42 if path.exists() {
43 self.load_agents_from_dir(&mut agents, path)?;
44 }
45 }
46
47 Ok(agents)
48 }
49
50 fn load_agents_from_dir(
51 &self,
52 agents: &mut Vec<AgentConfig>,
53 dir: &Path,
54 ) -> anyhow::Result<()> {
55 let pattern = dir.join("*.md").to_string_lossy().to_string();
56
57 for entry in glob(&pattern).context("Failed to read glob pattern")? {
58 let entry = entry.context("Failed to read glob entry")?;
59 if let Some(agent) = self.load_agent_from_file(&entry)? {
60 if let Some(pos) = agents.iter().position(|a| a.name == agent.name) {
62 agents[pos] = agent;
64 } else {
65 agents.push(agent);
66 }
67 }
68 }
69
70 Ok(())
71 }
72
73 fn load_agent_from_file(&self, path: &Path) -> anyhow::Result<Option<AgentConfig>> {
74 let content = std::fs::read_to_string(path)?;
75
76 let (frontmatter, body) = self.parse_frontmatter(&content)?;
78
79 let name = path
81 .file_stem()
82 .and_then(|s| s.to_str())
83 .context("Invalid agent filename")?
84 .to_string();
85
86 Ok(Some(frontmatter.to_agent_config(name, body)))
87 }
88
89 fn parse_frontmatter(
90 &self,
91 content: &str,
92 ) -> anyhow::Result<(AgentFrontmatter, Option<String>)> {
93 if !content.starts_with("---") {
95 anyhow::bail!("Agent file must start with YAML frontmatter delimited by ---");
96 }
97
98 let rest = &content[3..]; let Some(frontmatter_end) = rest.find("---") else {
100 anyhow::bail!("Missing closing --- for frontmatter");
101 };
102
103 let frontmatter_yaml = &rest[..frontmatter_end];
104 let body = if frontmatter_end + 3 < rest.len() {
105 Some(rest[frontmatter_end + 3..].trim().to_string())
106 } else {
107 None
108 };
109
110 let frontmatter: AgentFrontmatter =
111 serde_yaml::from_str(frontmatter_yaml).context("Failed to parse agent frontmatter")?;
112
113 Ok((frontmatter, body))
114 }
115}
116
117impl Default for AgentLoader {
118 fn default() -> Self {
119 Self::new().expect("Failed to create AgentLoader")
120 }
121}