cascade_agent/skills/
parser.rs1use std::path::Path;
4
5use gray_matter::{engine::YAML, Matter};
6use tracing::{debug, warn};
7
8use crate::error::{AgentError, Result};
9
10use super::types::{Skill, SkillMetadata};
11
12const SKILL_FILE: &str = "SKILL.md";
13
14const EXECUTABLE_NAMES: &[&str] = &["run.sh", "run.py", "run", "bin"];
16
17pub fn parse_skill_file(path: &Path) -> Result<Skill> {
23 let content = std::fs::read_to_string(path)
24 .map_err(|e| AgentError::SkillError(format!("Cannot read {}: {}", path.display(), e)))?;
25
26 let matter: Matter<YAML> = Matter::new();
27 let parsed = matter.parse::<SkillMetadata>(&content).map_err(|e| {
28 AgentError::SkillError(format!(
29 "Frontmatter parse error in {}: {}",
30 path.display(),
31 e
32 ))
33 })?;
34
35 let metadata = parsed.data.ok_or_else(|| {
36 AgentError::SkillError(format!(
37 "No valid YAML frontmatter found in {}",
38 path.display()
39 ))
40 })?;
41
42 if metadata.name.is_empty() {
43 return Err(AgentError::SkillError(format!(
44 "Skill metadata in {} is missing a 'name' field",
45 path.display()
46 )));
47 }
48
49 let instructions = parsed.content;
50 let directory = path.parent().unwrap_or(path).to_path_buf();
51
52 let executable_path = find_executable(&directory);
54
55 if let Some(ref exe) = executable_path {
56 debug!(
57 skill = %metadata.name,
58 "Found executable for skill: {}",
59 exe.display()
60 );
61 }
62
63 Ok(Skill {
64 metadata,
65 instructions,
66 executable_path,
67 directory,
68 })
69}
70
71fn find_executable(dir: &Path) -> Option<std::path::PathBuf> {
75 if !dir.is_dir() {
76 return None;
77 }
78
79 let read_dir = match std::fs::read_dir(dir) {
80 Ok(rd) => rd,
81 Err(e) => {
82 warn!("Cannot read skill directory {}: {}", dir.display(), e);
83 return None;
84 }
85 };
86
87 for name in EXECUTABLE_NAMES {
89 let candidate = dir.join(name);
90 if candidate.is_file() {
91 return Some(candidate);
92 }
93 }
94
95 for entry in read_dir.flatten() {
97 let path = entry.path();
98 if path.is_file() {
99 use std::os::unix::fs::PermissionsExt;
100 let mode = entry.metadata().ok()?.permissions().mode();
101 if mode & 0o111 != 0 {
102 debug!("Found executable by permission bit: {}", path.display());
103 return Some(path);
104 }
105 }
106 }
107
108 None
109}
110
111pub fn serialize_skill_md(metadata: &SkillMetadata, body: &str) -> String {
113 let yaml = serde_yaml::to_string(metadata).unwrap_or_default();
114 format!("---\n{yaml}---\n{body}")
115}
116
117pub fn read_skill_parts(path: &Path) -> Result<(String, String)> {
119 let content = std::fs::read_to_string(path)
120 .map_err(|e| AgentError::SkillError(format!("Cannot read {}: {}", path.display(), e)))?;
121
122 let matter: Matter<YAML> = Matter::new();
123 let parsed = matter.parse::<serde_yaml::Value>(&content).map_err(|e| {
124 AgentError::SkillError(format!(
125 "Frontmatter parse error in {}: {}",
126 path.display(),
127 e
128 ))
129 })?;
130
131 let frontmatter = if parsed.matter.is_empty() {
133 String::new()
134 } else {
135 format!("---\n{}\n---\n", parsed.matter)
136 };
137
138 Ok((frontmatter, parsed.content))
139}
140
141pub fn skill_file_name() -> &'static str {
143 SKILL_FILE
144}