Skip to main content

bamboo_engine/skills/store/
storage.rs

1use std::path::{Path, PathBuf};
2
3use tokio::fs;
4use tracing::{debug, info, warn};
5
6use crate::skills::store::parser::{parse_markdown_skill, render_skill_markdown};
7use crate::skills::types::{SkillDefinition, SkillResult};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum SkillDirectorySource {
11    Global,
12    Project,
13}
14
15#[derive(Debug, Clone)]
16pub struct SkillDiscoveryDir {
17    pub dir: PathBuf,
18    pub source: SkillDirectorySource,
19    pub mode: Option<String>,
20}
21
22#[derive(Debug, Clone)]
23pub struct LoadedSkillRecord {
24    pub skill: SkillDefinition,
25    pub skill_root: PathBuf,
26    pub source: SkillDirectorySource,
27    pub mode: Option<String>,
28}
29
30pub async fn ensure_skills_dir(skills_dir: &Path) -> SkillResult<()> {
31    fs::create_dir_all(skills_dir).await?;
32    Ok(())
33}
34
35/// Recursively find all SKILL.md files in the skills directory
36async fn find_skill_files(dir: &Path) -> SkillResult<Vec<PathBuf>> {
37    let mut skill_files = Vec::new();
38    let mut entries = fs::read_dir(dir).await?;
39
40    while let Some(entry) = entries.next_entry().await? {
41        let path = entry.path();
42
43        if path.is_dir() {
44            // Check if this directory contains SKILL.md
45            let skill_file = path.join("SKILL.md");
46            match fs::try_exists(&skill_file).await {
47                Ok(true) => {
48                    skill_files.push(skill_file);
49                    continue; // Don't recurse into skill directories
50                }
51                Ok(false) => {
52                    // Not a skill directory, recurse into it
53                    let sub_skills = Box::pin(find_skill_files(&path)).await?;
54                    skill_files.extend(sub_skills);
55                }
56                Err(_) => {
57                    debug!("Cannot check {:?}, skipping", path);
58                }
59            }
60        }
61    }
62
63    Ok(skill_files)
64}
65
66pub async fn load_skills_from_discovery_dirs(
67    discovery_dirs: &[SkillDiscoveryDir],
68) -> SkillResult<Vec<LoadedSkillRecord>> {
69    let mut loaded = Vec::new();
70
71    for discovery in discovery_dirs {
72        match fs::try_exists(&discovery.dir).await {
73            Ok(true) => {}
74            Ok(false) => {
75                debug!(
76                    "Skill discovery dir not found, skipping: {:?}",
77                    discovery.dir
78                );
79                continue;
80            }
81            Err(error) => {
82                warn!(
83                    "Failed to check skill discovery dir {:?}: {}",
84                    discovery.dir, error
85                );
86                continue;
87            }
88        }
89
90        debug!(
91            "Loading skills from {:?} (source={:?}, mode={})",
92            discovery.dir,
93            discovery.source,
94            discovery.mode.as_deref().unwrap_or("generic")
95        );
96
97        let skill_files = find_skill_files(&discovery.dir).await?;
98        for skill_file in skill_files {
99            match fs::read_to_string(&skill_file).await {
100                Ok(content) => match parse_markdown_skill(&skill_file, &content) {
101                    Ok(skill) => {
102                        let skill_root = skill_file
103                            .parent()
104                            .map(Path::to_path_buf)
105                            .unwrap_or_else(|| discovery.dir.clone());
106                        loaded.push(LoadedSkillRecord {
107                            skill,
108                            skill_root,
109                            source: discovery.source,
110                            mode: discovery.mode.clone(),
111                        });
112                    }
113                    Err(error) => {
114                        warn!("Failed to parse skill file {:?}: {}", skill_file, error);
115                    }
116                },
117                Err(error) => {
118                    warn!("Failed to read skill file {:?}: {}", skill_file, error);
119                }
120            }
121        }
122    }
123
124    info!("Loaded {} skill records from discovery dirs", loaded.len());
125    Ok(loaded)
126}
127
128pub fn skill_path(skills_dir: &Path, skill_id: &str) -> PathBuf {
129    skills_dir.join(skill_id).join("SKILL.md")
130}
131
132pub async fn write_skill_file(skills_dir: &Path, skill: &SkillDefinition) -> SkillResult<()> {
133    let path = skill_path(skills_dir, &skill.id);
134
135    // Ensure parent directory exists
136    if let Some(parent) = path.parent() {
137        fs::create_dir_all(parent).await?;
138    }
139
140    let content = render_skill_markdown(skill)?;
141    fs::write(path, content).await?;
142    Ok(())
143}