Skip to main content

mur_common/skill/
local.rs

1//! Local skill store helpers — list installed, resolve, remove, search, trust.
2
3use crate::skill::store::{agent_skill_dir, global_skill_dir};
4use crate::skill::types::TrustLevel;
5use crate::skill::{SkillManifest, StoreError, read_from_dir};
6use crate::trust::skills::SkillTrustStore;
7use std::fs;
8use std::path::{Path, PathBuf};
9
10pub fn list_installed(mur_home: &Path) -> Result<Vec<String>, StoreError> {
11    let skills_dir = mur_home.join("skills");
12    if !skills_dir.exists() {
13        return Ok(vec![]);
14    }
15    let mut names: Vec<_> = fs::read_dir(&skills_dir)
16        .map_err(StoreError::Io)?
17        .filter_map(|e| {
18            let e = e.ok()?;
19            if e.file_type().ok()?.is_dir() {
20                e.file_name().to_str().map(|s| s.to_string())
21            } else {
22                None
23            }
24        })
25        .collect();
26    names.sort();
27    Ok(names)
28}
29
30pub fn load_installed(mur_home: &Path, name: &str) -> Result<SkillManifest, StoreError> {
31    read_from_dir(&global_skill_dir(mur_home, name))
32}
33
34pub fn list_installed_agent(mur_home: &Path, agent_name: &str) -> Result<Vec<String>, StoreError> {
35    let dir = agent_skill_dir(mur_home, agent_name);
36    if !dir.exists() {
37        return Ok(vec![]);
38    }
39    let mut names: Vec<_> = fs::read_dir(&dir)
40        .map_err(StoreError::Io)?
41        .filter_map(|e| {
42            let e = e.ok()?;
43            if e.file_type().ok()?.is_dir() {
44                e.file_name().to_str().map(str::to_string)
45            } else {
46                None
47            }
48        })
49        .collect();
50    names.sort();
51    Ok(names)
52}
53
54pub fn load_installed_agent(
55    mur_home: &Path,
56    agent_name: &str,
57    skill: &str,
58) -> Result<SkillManifest, StoreError> {
59    read_from_dir(&agent_skill_dir(mur_home, agent_name).join(skill))
60}
61
62pub fn installed_path(mur_home: &Path, name: &str) -> PathBuf {
63    global_skill_dir(mur_home, name)
64}
65
66pub fn remove_installed(mur_home: &Path, name: &str) -> Result<(), StoreError> {
67    let dir = installed_path(mur_home, name);
68    if dir.exists() {
69        fs::remove_dir_all(&dir).map_err(StoreError::Io)?;
70    }
71    // Remove trust entry by name
72    if let Ok(mut trust) = SkillTrustStore::load(mur_home) {
73        trust.entries.retain(|_k, v| v.name != name);
74        let _ = trust.save(mur_home);
75    }
76    Ok(())
77}
78
79pub fn search_installed(
80    mur_home: &Path,
81    query: &str,
82) -> Result<Vec<(String, SkillManifest)>, StoreError> {
83    let q = query.to_lowercase();
84    let mut results = Vec::new();
85    for name in list_installed(mur_home)? {
86        if let Ok(m) = load_installed(mur_home, &name)
87            && (name.to_lowercase().contains(&q)
88                || m.description.to_lowercase().contains(&q)
89                || m.tags.iter().any(|t| t.to_lowercase().contains(&q)))
90        {
91            results.push((name, m));
92        }
93    }
94    Ok(results)
95}
96
97pub fn set_trust_level(
98    mur_home: &Path,
99    name: &str,
100    level: TrustLevel,
101) -> Result<(), Box<dyn std::error::Error>> {
102    let mut trust = SkillTrustStore::load(mur_home)?;
103    let keys: Vec<String> = trust
104        .entries
105        .iter()
106        .filter(|(_k, v)| v.name == name)
107        .map(|(k, _)| k.clone())
108        .collect();
109    for k in keys {
110        if let Some(e) = trust.entries.get_mut(&k) {
111            e.level = level;
112        }
113    }
114    trust.save(mur_home)?;
115    Ok(())
116}
117
118pub fn get_trust_level(
119    mur_home: &Path,
120    name: &str,
121) -> Result<TrustLevel, Box<dyn std::error::Error>> {
122    let trust = SkillTrustStore::load(mur_home)?;
123    for entry in trust.entries.values() {
124        if entry.name == name {
125            return Ok(entry.level);
126        }
127    }
128    Ok(TrustLevel::Sandboxed)
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134    use crate::skill::{parse_canonical, write_to_dir};
135    use tempfile::tempdir;
136
137    fn sample(name: &str) -> SkillManifest {
138        parse_canonical(&format!(
139            r#"name: {name}
140version: 1.0.0
141publisher: human:t
142description: test skill for {name}
143category: context
144content:
145  abstract: hi
146  context: body
147tags: [test, {name}]
148"#
149        ))
150        .unwrap()
151    }
152
153    #[test]
154    fn list_returns_installed() {
155        let dir = tempdir().unwrap();
156        write_to_dir(&global_skill_dir(dir.path(), "a"), &sample("a")).unwrap();
157        write_to_dir(&global_skill_dir(dir.path(), "b"), &sample("b")).unwrap();
158        assert_eq!(list_installed(dir.path()).unwrap(), vec!["a", "b"]);
159    }
160
161    #[test]
162    fn empty_dir_returns_empty() {
163        assert!(
164            list_installed(tempdir().unwrap().path())
165                .unwrap()
166                .is_empty()
167        );
168    }
169
170    #[test]
171    fn search_finds_by_name() {
172        let dir = tempdir().unwrap();
173        write_to_dir(
174            &global_skill_dir(dir.path(), "my-prices"),
175            &sample("my-prices"),
176        )
177        .unwrap();
178        assert_eq!(search_installed(dir.path(), "price").unwrap().len(), 1);
179    }
180
181    #[test]
182    fn search_finds_by_tag() {
183        let dir = tempdir().unwrap();
184        write_to_dir(&global_skill_dir(dir.path(), "web"), &sample("web")).unwrap();
185        assert_eq!(search_installed(dir.path(), "test").unwrap().len(), 1);
186    }
187
188    #[test]
189    fn remove_cleans_dir() {
190        let dir = tempdir().unwrap();
191        write_to_dir(&global_skill_dir(dir.path(), "rm-me"), &sample("rm-me")).unwrap();
192        remove_installed(dir.path(), "rm-me").unwrap();
193        assert!(list_installed(dir.path()).unwrap().is_empty());
194    }
195
196    #[test]
197    fn list_installed_agent_finds_agent_skills() {
198        let dir = tempdir().unwrap();
199        let agent_dir = agent_skill_dir(dir.path(), "alice");
200        write_to_dir(&agent_dir.join("foo"), &sample("foo")).unwrap();
201        let names = list_installed_agent(dir.path(), "alice").unwrap();
202        assert_eq!(names, vec!["foo"]);
203    }
204
205    #[test]
206    fn list_installed_agent_empty_when_dir_missing() {
207        let dir = tempdir().unwrap();
208        let names = list_installed_agent(dir.path(), "nobody").unwrap();
209        assert!(names.is_empty());
210    }
211}