diff --git a/src/gateway/http.rs b/src/gateway/http.rs index c26d6e2..a455a6b 100644 --- a/src/gateway/http.rs +++ b/src/gateway/http.rs @@ -844,6 +844,23 @@ pub async fn get_tools(State(state): State>) -> Result>) -> Result, ApiError> { + let loader = state.session_manager.skills_loader(); + let skills: Vec = loader + .get_loaded_skills() + .iter() + .map(|s| { + json!({ + "name": s.name, + "description": s.description, + "always": s.always, + "source": loader.source_of(s.path.as_deref()), + }) + }) + .collect(); + Ok(Json(json!({ "skills": skills }))) +} + pub async fn get_tasks( State(state): State>, Query(query): Query, diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index 9153051..7cd21de 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -595,6 +595,7 @@ fn build_router(state: Arc) -> Router { .route("/api/tasks", routing::get(http::get_tasks)) .route("/api/status", routing::get(http::get_status)) .route("/api/tools", routing::get(http::get_tools)) + .route("/api/skills", routing::get(http::get_skills)) .route("/api/jobs", routing::get(http::get_jobs)) .route("/api/jobs/{id}/runs", routing::get(http::get_job_runs)) .route("/api/memories", routing::get(http::get_memories)) diff --git a/src/session/session.rs b/src/session/session.rs index 0d8c340..7bbbd27 100644 --- a/src/session/session.rs +++ b/src/session/session.rs @@ -1708,6 +1708,10 @@ impl SessionManager { self.tools.clone() } + pub fn skills_loader(&self) -> Arc { + self.skills_loader.clone() + } + pub fn work_manager(&self) -> Arc { self.work_manager.clone() } diff --git a/src/skills/mod.rs b/src/skills/mod.rs index f4698d3..8f352a4 100644 --- a/src/skills/mod.rs +++ b/src/skills/mod.rs @@ -248,6 +248,24 @@ impl SkillsLoader { max_mtime } + pub fn source_of(&self, path: Option<&Path>) -> &'static str { + let Some(path) = path else { + return "unknown"; + }; + if let Some(ws) = &self.workspace_skills_dir + && path.starts_with(ws) + { + return "workspace"; + } + if path.starts_with(&self.picobot_skills_dir) { + return "picobot"; + } + if path.starts_with(&self.agent_skills_dir) { + return "agent"; + } + "other" + } + /// Get a copy of loaded skills (checks for changes first) pub fn get_loaded_skills(&self) -> Vec { self.reload_if_changed(); @@ -586,4 +604,31 @@ This is the content. let beta_pos = prompt.find("**beta**").unwrap(); assert!(alpha_pos < beta_pos); } + + #[test] + fn test_source_of() { + let mut loader = SkillsLoader::new_for_testing( + PathBuf::from("/home/user/.picobot/skills"), + PathBuf::from("/home/user/.agents/skills"), + ); + loader.set_workspace_skills_dir(PathBuf::from("/workspace")); + + assert_eq!(loader.source_of(None), "unknown"); + assert_eq!( + loader.source_of(Some(Path::new("/workspace/skills/my-skill"))), + "workspace" + ); + assert_eq!( + loader.source_of(Some(Path::new("/home/user/.picobot/skills/foo"))), + "picobot" + ); + assert_eq!( + loader.source_of(Some(Path::new("/home/user/.agents/skills/bar"))), + "agent" + ); + assert_eq!( + loader.source_of(Some(Path::new("/opt/other/skill"))), + "other" + ); + } }