mur_common/skill/
local.rs1use 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 let name = e.file_name().to_str()?.to_string();
21 (!name.starts_with("fleet:")).then_some(name)
28 } else {
29 None
30 }
31 })
32 .collect();
33 names.sort();
34 Ok(names)
35}
36
37pub fn load_installed(mur_home: &Path, name: &str) -> Result<SkillManifest, StoreError> {
38 read_from_dir(&global_skill_dir(mur_home, name))
39}
40
41pub fn list_installed_agent(mur_home: &Path, agent_name: &str) -> Result<Vec<String>, StoreError> {
42 let dir = agent_skill_dir(mur_home, agent_name);
43 if !dir.exists() {
44 return Ok(vec![]);
45 }
46 let mut names: Vec<_> = fs::read_dir(&dir)
47 .map_err(StoreError::Io)?
48 .filter_map(|e| {
49 let e = e.ok()?;
50 if e.file_type().ok()?.is_dir() {
51 e.file_name().to_str().map(str::to_string)
52 } else {
53 None
54 }
55 })
56 .collect();
57 names.sort();
58 Ok(names)
59}
60
61pub fn load_installed_agent(
62 mur_home: &Path,
63 agent_name: &str,
64 skill: &str,
65) -> Result<SkillManifest, StoreError> {
66 read_from_dir(&agent_skill_dir(mur_home, agent_name).join(skill))
67}
68
69pub fn installed_path(mur_home: &Path, name: &str) -> PathBuf {
70 global_skill_dir(mur_home, name)
71}
72
73pub fn remove_installed(mur_home: &Path, name: &str) -> Result<(), StoreError> {
74 let dir = installed_path(mur_home, name);
75 if dir.exists() {
76 fs::remove_dir_all(&dir).map_err(StoreError::Io)?;
77 }
78 if let Ok(mut trust) = SkillTrustStore::load(mur_home) {
80 trust.entries.retain(|_k, v| v.name != name);
81 let _ = trust.save(mur_home);
82 }
83 Ok(())
84}
85
86pub fn search_installed(
87 mur_home: &Path,
88 query: &str,
89) -> Result<Vec<(String, SkillManifest)>, StoreError> {
90 let q = query.to_lowercase();
91 let mut results = Vec::new();
92 for name in list_installed(mur_home)? {
93 if let Ok(m) = load_installed(mur_home, &name)
94 && (name.to_lowercase().contains(&q)
95 || m.description.to_lowercase().contains(&q)
96 || m.tags.iter().any(|t| t.to_lowercase().contains(&q)))
97 {
98 results.push((name, m));
99 }
100 }
101 Ok(results)
102}
103
104pub fn set_trust_level(
105 mur_home: &Path,
106 name: &str,
107 level: TrustLevel,
108) -> Result<(), Box<dyn std::error::Error>> {
109 let mut trust = SkillTrustStore::load(mur_home)?;
110 let keys: Vec<String> = trust
111 .entries
112 .iter()
113 .filter(|(_k, v)| v.name == name)
114 .map(|(k, _)| k.clone())
115 .collect();
116 for k in keys {
117 if let Some(e) = trust.entries.get_mut(&k) {
118 e.level = level;
119 }
120 }
121 trust.save(mur_home)?;
122 Ok(())
123}
124
125pub fn get_trust_level(
126 mur_home: &Path,
127 name: &str,
128) -> Result<TrustLevel, Box<dyn std::error::Error>> {
129 let trust = SkillTrustStore::load(mur_home)?;
130 for entry in trust.entries.values() {
131 if entry.name == name {
132 return Ok(entry.level);
133 }
134 }
135 Ok(TrustLevel::Sandboxed)
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use crate::skill::{parse_canonical, write_to_dir};
142 use tempfile::tempdir;
143
144 fn sample(name: &str) -> SkillManifest {
145 parse_canonical(&format!(
146 r#"name: {name}
147version: 1.0.0
148publisher: human:t
149description: test skill for {name}
150category: context
151content:
152 abstract: hi
153 context: body
154tags: [test, {name}]
155"#
156 ))
157 .unwrap()
158 }
159
160 #[test]
161 fn list_returns_installed() {
162 let dir = tempdir().unwrap();
163 write_to_dir(&global_skill_dir(dir.path(), "a"), &sample("a")).unwrap();
164 write_to_dir(&global_skill_dir(dir.path(), "b"), &sample("b")).unwrap();
165 assert_eq!(list_installed(dir.path()).unwrap(), vec!["a", "b"]);
166 }
167
168 #[test]
169 fn empty_dir_returns_empty() {
170 assert!(
171 list_installed(tempdir().unwrap().path())
172 .unwrap()
173 .is_empty()
174 );
175 }
176
177 #[test]
178 fn search_finds_by_name() {
179 let dir = tempdir().unwrap();
180 write_to_dir(
181 &global_skill_dir(dir.path(), "my-prices"),
182 &sample("my-prices"),
183 )
184 .unwrap();
185 assert_eq!(search_installed(dir.path(), "price").unwrap().len(), 1);
186 }
187
188 #[test]
189 fn search_finds_by_tag() {
190 let dir = tempdir().unwrap();
191 write_to_dir(&global_skill_dir(dir.path(), "web"), &sample("web")).unwrap();
192 assert_eq!(search_installed(dir.path(), "test").unwrap().len(), 1);
193 }
194
195 #[test]
196 fn remove_cleans_dir() {
197 let dir = tempdir().unwrap();
198 write_to_dir(&global_skill_dir(dir.path(), "rm-me"), &sample("rm-me")).unwrap();
199 remove_installed(dir.path(), "rm-me").unwrap();
200 assert!(list_installed(dir.path()).unwrap().is_empty());
201 }
202
203 #[cfg(unix)]
210 #[test]
211 fn list_installed_ignores_legacy_fleet_ledgers() {
212 let dir = tempdir().unwrap();
213 write_to_dir(&global_skill_dir(dir.path(), "real"), &sample("real")).unwrap();
214 let legacy = dir.path().join("skills").join("fleet:builder");
216 fs::create_dir_all(&legacy).unwrap();
217 fs::write(legacy.join("events.jsonl"), "{}\n").unwrap();
218 assert_eq!(list_installed(dir.path()).unwrap(), vec!["real"]);
219 }
220
221 #[test]
222 fn list_installed_agent_finds_agent_skills() {
223 let dir = tempdir().unwrap();
224 let agent_dir = agent_skill_dir(dir.path(), "alice");
225 write_to_dir(&agent_dir.join("foo"), &sample("foo")).unwrap();
226 let names = list_installed_agent(dir.path(), "alice").unwrap();
227 assert_eq!(names, vec!["foo"]);
228 }
229
230 #[test]
231 fn list_installed_agent_empty_when_dir_missing() {
232 let dir = tempdir().unwrap();
233 let names = list_installed_agent(dir.path(), "nobody").unwrap();
234 assert!(names.is_empty());
235 }
236}