use std::collections::HashMap;
use std::path::Path;
use agent_contracts::SkillContext;
use serde::Deserialize;
use crate::error::SkillError;
use crate::types::{Skill, SkillToolDef, SkillToolKind};
pub fn load_skill_toml(path: &Path, skill_dir: &Path) -> Result<Skill, SkillError> {
let content = std::fs::read_to_string(path).map_err(|e| SkillError::ReadFile {
path: path.to_path_buf(),
source: e,
})?;
let manifest: SkillManifest = toml::from_str(&content).map_err(|e| SkillError::TomlParse {
path: path.to_path_buf(),
message: e.to_string(),
})?;
let skill_section = manifest.skill;
let name = skill_section.name.ok_or_else(|| SkillError::MissingField {
path: path.to_path_buf(),
field: "skill.name".into(),
})?;
let context = match skill_section.context.as_deref() {
Some("fork") => SkillContext::Fork,
_ => SkillContext::Inline,
};
let tools: Vec<SkillToolDef> = manifest
.tools
.unwrap_or_default()
.into_iter()
.map(|t| SkillToolDef {
name: t.name,
description: t.description.unwrap_or_default(),
kind: match t.kind.as_str() {
"http" => SkillToolKind::Http,
"script" => SkillToolKind::Script,
_ => SkillToolKind::Shell,
},
command: t.command,
args: t.args.unwrap_or_default(),
})
.collect();
let prompt_md_path = skill_dir.join("SKILL.md");
let prompt = if prompt_md_path.exists() {
let content = std::fs::read_to_string(&prompt_md_path).unwrap_or_default();
let trimmed = content.trim_start();
if trimmed.starts_with("---") {
let after_first = &trimmed[3..];
if let Some(end_pos) = after_first.find("\n---") {
let rest_start = end_pos + 4;
if rest_start < after_first.len() {
let rest = &after_first[rest_start..];
rest.strip_prefix('\n').unwrap_or(rest).to_string()
} else {
String::new()
}
} else {
content
}
} else {
content
}
} else {
skill_section
.prompts
.map(|p| p.join("\n\n"))
.unwrap_or_default()
};
Ok(Skill {
name,
description: skill_section.description.unwrap_or_default(),
version: skill_section.version,
author: skill_section.author,
tags: skill_section.tags.unwrap_or_default(),
location: Some(skill_dir.to_path_buf()),
prompt,
user_invocable: skill_section.user_invocable.unwrap_or(true),
disable_model_invocation: skill_section.disable_model_invocation.unwrap_or(false),
context,
argument_hint: skill_section.argument_hint,
arguments: skill_section.arguments.unwrap_or_default(),
paths: skill_section.paths.unwrap_or_default(),
tools,
})
}
#[derive(Deserialize)]
struct SkillManifest {
skill: SkillSection,
#[serde(rename = "tools")]
tools: Option<Vec<ToolEntry>>,
}
#[derive(Deserialize)]
struct SkillSection {
name: Option<String>,
description: Option<String>,
version: Option<String>,
author: Option<String>,
tags: Option<Vec<String>>,
user_invocable: Option<bool>,
disable_model_invocation: Option<bool>,
context: Option<String>,
argument_hint: Option<String>,
arguments: Option<Vec<String>>,
paths: Option<Vec<String>>,
prompts: Option<Vec<String>>,
}
#[derive(Deserialize)]
struct ToolEntry {
name: String,
description: Option<String>,
kind: String,
command: String,
args: Option<HashMap<String, String>>,
}