use crate::{Env, host::Host, skill::loader};
use serde::Deserialize;
use wcore::{
agent::{AsTool, ToolDescription},
model::Tool,
};
#[derive(Deserialize, schemars::JsonSchema)]
pub struct Skill {
pub name: String,
}
impl ToolDescription for Skill {
const DESCRIPTION: &'static str = "Load a skill by name. Returns its instructions on exact match, or lists matching skills otherwise.";
}
pub fn tools() -> Vec<Tool> {
vec![Skill::as_tool()]
}
impl<H: Host> Env<H> {
pub async fn dispatch_skill(&self, args: &str, agent: &str) -> String {
let input: Skill = match serde_json::from_str(args) {
Ok(v) => v,
Err(e) => return format!("invalid arguments: {e}"),
};
let name = &input.name;
if let Some(scope) = self.scopes.get(agent)
&& !scope.skills.is_empty()
&& !scope.skills.iter().any(|s| s == name)
{
return format!("skill not available: {name}");
}
if name.contains("..") || name.contains('/') || name.contains('\\') {
return format!("invalid skill name: {name}");
}
if !name.is_empty() {
for dir in &self.skills.skill_dirs {
let skill_dir = dir.join(name);
let skill_file = skill_dir.join("SKILL.md");
if let Ok(content) = tokio::fs::read_to_string(&skill_file).await {
return match loader::parse_skill_md(&content) {
Ok(skill) => {
let body = skill.body.clone();
self.skills.registry.lock().await.upsert(skill);
let dir_path = skill_dir.display();
format!("{body}\n\nSkill directory: {dir_path}")
}
Err(e) => format!("failed to parse skill: {e}"),
};
}
}
}
let query = name.to_lowercase();
let allowed = self.scopes.get(agent).map(|s| &s.skills);
let registry = self.skills.registry.lock().await;
let matches: Vec<String> = registry
.skills
.iter()
.filter(|s| {
if let Some(allowed) = allowed
&& !allowed.is_empty()
&& !allowed.iter().any(|a| a == s.name.as_str())
{
return false;
}
query.is_empty()
|| s.name.to_lowercase().contains(&query)
|| s.description.to_lowercase().contains(&query)
})
.map(|s| format!("{}: {}", s.name, s.description))
.collect();
if matches.is_empty() {
"no skills found".to_owned()
} else {
matches.join("\n")
}
}
}