bamboo_engine/skills/store/
storage.rs1use 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
35async 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 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; }
51 Ok(false) => {
52 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 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}