use std::path::Path;
use gray_matter::{engine::YAML, Matter};
use tracing::{debug, warn};
use crate::error::{AgentError, Result};
use super::types::{Skill, SkillMetadata};
const SKILL_FILE: &str = "SKILL.md";
const EXECUTABLE_NAMES: &[&str] = &["run.sh", "run.py", "run", "bin"];
pub fn parse_skill_file(path: &Path) -> Result<Skill> {
let content = std::fs::read_to_string(path)
.map_err(|e| AgentError::SkillError(format!("Cannot read {}: {}", path.display(), e)))?;
let matter: Matter<YAML> = Matter::new();
let parsed = matter.parse::<SkillMetadata>(&content).map_err(|e| {
AgentError::SkillError(format!(
"Frontmatter parse error in {}: {}",
path.display(),
e
))
})?;
let metadata = parsed.data.ok_or_else(|| {
AgentError::SkillError(format!(
"No valid YAML frontmatter found in {}",
path.display()
))
})?;
if metadata.name.is_empty() {
return Err(AgentError::SkillError(format!(
"Skill metadata in {} is missing a 'name' field",
path.display()
)));
}
let instructions = parsed.content;
let directory = path.parent().unwrap_or(path).to_path_buf();
let executable_path = find_executable(&directory);
if let Some(ref exe) = executable_path {
debug!(
skill = %metadata.name,
"Found executable for skill: {}",
exe.display()
);
}
Ok(Skill {
metadata,
instructions,
executable_path,
directory,
})
}
fn find_executable(dir: &Path) -> Option<std::path::PathBuf> {
if !dir.is_dir() {
return None;
}
let read_dir = match std::fs::read_dir(dir) {
Ok(rd) => rd,
Err(e) => {
warn!("Cannot read skill directory {}: {}", dir.display(), e);
return None;
}
};
for name in EXECUTABLE_NAMES {
let candidate = dir.join(name);
if candidate.is_file() {
return Some(candidate);
}
}
for entry in read_dir.flatten() {
let path = entry.path();
if path.is_file() {
use std::os::unix::fs::PermissionsExt;
let mode = entry.metadata().ok()?.permissions().mode();
if mode & 0o111 != 0 {
debug!("Found executable by permission bit: {}", path.display());
return Some(path);
}
}
}
None
}
pub fn serialize_skill_md(metadata: &SkillMetadata, body: &str) -> String {
let yaml = serde_yaml::to_string(metadata).unwrap_or_default();
format!("---\n{yaml}---\n{body}")
}
pub fn read_skill_parts(path: &Path) -> Result<(String, String)> {
let content = std::fs::read_to_string(path)
.map_err(|e| AgentError::SkillError(format!("Cannot read {}: {}", path.display(), e)))?;
let matter: Matter<YAML> = Matter::new();
let parsed = matter.parse::<serde_yaml::Value>(&content).map_err(|e| {
AgentError::SkillError(format!(
"Frontmatter parse error in {}: {}",
path.display(),
e
))
})?;
let frontmatter = if parsed.matter.is_empty() {
String::new()
} else {
format!("---\n{}\n---\n", parsed.matter)
};
Ok((frontmatter, parsed.content))
}
pub fn skill_file_name() -> &'static str {
SKILL_FILE
}