use include_dir::{include_dir, Dir, DirEntry, File};
use serde::Deserialize;
use std::path::Path;
static BUILTIN_SKILLS_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/skills/built-in");
pub fn ensure_builtin_skills(skills_root: &Path) -> std::io::Result<()> {
std::fs::create_dir_all(skills_root)?;
copy_compatible_skills(&BUILTIN_SKILLS_DIR, skills_root)
}
fn copy_compatible_skills(embedded: &Dir<'_>, destination: &Path) -> std::io::Result<()> {
for entry in embedded.entries() {
let DirEntry::Dir(skill_dir) = entry else {
continue;
};
let Some(skill_name) = skill_dir.path().file_name() else {
continue;
};
let skill_name_str = skill_name.to_string_lossy();
tracing::debug!("Found built-in skill: {}", skill_name_str);
let Some(skill_md) = get_file_with_relative_dir(skill_dir, "SKILL.md") else {
tracing::debug!("Skipping {} - missing SKILL.md", skill_name_str);
continue;
};
let content = String::from_utf8_lossy(skill_md.contents());
if let Some(reason) = skill_skip_reason(&content) {
tracing::debug!(
"Skipping built-in skill '{}' on this host: {}",
skill_name_str,
reason
);
continue;
}
let next_dest = destination.join(skill_name);
if !next_dest.exists() {
tracing::debug!(
"Installing skill {} to {}",
skill_name_str,
next_dest.display()
);
std::fs::create_dir_all(&next_dest)?;
copy_missing_entries(skill_dir, &next_dest)?;
} else {
tracing::debug!(
"Skill {} already exists at {}",
skill_name_str,
next_dest.display()
);
copy_missing_entries(skill_dir, &next_dest)?;
}
}
Ok(())
}
fn get_file_with_relative_dir<'a>(dir: &'a Dir<'a>, relative_path: &str) -> Option<&'a File<'a>> {
let full_path = dir.path().join(relative_path);
dir.get_file(full_path)
}
fn copy_missing_entries(embedded: &Dir<'_>, destination: &Path) -> std::io::Result<()> {
for entry in embedded.entries() {
match entry {
DirEntry::Dir(dir) => {
let Some(name) = dir.path().file_name() else {
continue;
};
let next_dest = destination.join(name);
if !next_dest.exists() {
std::fs::create_dir_all(&next_dest)?;
}
copy_missing_entries(dir, &next_dest)?;
}
DirEntry::File(file) => {
let Some(name) = file.path().file_name() else {
continue;
};
let out_path = destination.join(name);
if !out_path.exists() {
tracing::debug!("Writing file: {}", out_path.display());
std::fs::write(out_path, file.contents())?;
}
}
}
}
Ok(())
}
#[derive(Debug, Deserialize, Default)]
struct SkillFrontmatter {
#[serde(default)]
platforms: Vec<String>,
#[serde(default)]
deps: Vec<String>,
#[serde(default)]
compatibility: SkillCompatibility,
}
#[derive(Debug, Deserialize, Default)]
struct SkillCompatibility {
#[serde(default)]
os: Vec<String>,
#[serde(default)]
deps: Vec<String>,
}
fn parse_frontmatter(content: &str) -> Option<SkillFrontmatter> {
let normalized = content.replace("\r\n", "\n");
let trimmed = normalized.trim_start_matches('\u{feff}');
let rest = trimmed.strip_prefix("---\n")?;
let end_idx = rest.find("\n---\n").or_else(|| rest.find("\n...\n"))?;
let yaml = &rest[..end_idx];
serde_yaml::from_str(yaml).ok()
}
fn skill_skip_reason(content: &str) -> Option<String> {
let fm = parse_frontmatter(content)?;
let mut supported = fm.platforms;
supported.extend(fm.compatibility.os);
supported.sort();
supported.dedup();
if !platform_allowed(&supported) {
return Some(format!(
"unsupported platform (current: {}, supported: {})",
current_platform(),
supported.join(", ")
));
}
let mut deps = fm.deps;
deps.extend(fm.compatibility.deps);
deps.sort();
deps.dedup();
let missing: Vec<String> = deps.into_iter().filter(|d| !command_exists(d)).collect();
if !missing.is_empty() {
return Some(format!("missing dependencies: {}", missing.join(", ")));
}
None
}
fn current_platform() -> &'static str {
if cfg!(target_os = "macos") {
"darwin"
} else if cfg!(target_os = "linux") {
"linux"
} else if cfg!(target_os = "windows") {
"windows"
} else {
"unknown"
}
}
fn normalize_platform(value: &str) -> String {
let v = value.trim().to_ascii_lowercase();
match v.as_str() {
"macos" | "osx" => "darwin".to_string(),
_ => v,
}
}
fn platform_allowed(platforms: &[String]) -> bool {
if platforms.is_empty() {
return true;
}
let current = current_platform();
platforms.iter().any(|p| {
let p = normalize_platform(p);
p == "all" || p == "*" || p == current
})
}
fn command_exists(command: &str) -> bool {
if command.trim().is_empty() {
return true;
}
let path_var = std::env::var_os("PATH").unwrap_or_default();
let paths = std::env::split_paths(&path_var);
#[cfg(target_os = "windows")]
let candidates: Vec<String> = {
let exts = std::env::var("PATHEXT").unwrap_or_else(|_| ".COM;.EXE;.BAT;.CMD".into());
let ext_list: Vec<String> = exts
.split(';')
.map(|s| s.trim().to_ascii_lowercase())
.filter(|s| !s.is_empty())
.collect();
let lower = command.to_ascii_lowercase();
if ext_list.iter().any(|ext| lower.ends_with(ext)) {
vec![command.to_string()]
} else {
let mut c = vec![command.to_string()];
for ext in ext_list {
c.push(format!("{command}{ext}"));
}
c
}
};
#[cfg(not(target_os = "windows"))]
let candidates: Vec<String> = vec![command.to_string()];
for base in paths {
for candidate in &candidates {
let full = base.join(candidate);
if full.is_file() {
return true;
}
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
fn temp_root() -> std::path::PathBuf {
std::env::temp_dir().join(format!(
"microclaw_builtin_skills_test_{}",
uuid::Uuid::new_v4()
))
}
fn cleanup(path: &Path) {
let _ = std::fs::remove_dir_all(path);
}
#[test]
fn test_ensure_builtin_skills_writes_missing_files() {
let root = temp_root();
let skills_root = root.join("skills");
ensure_builtin_skills(&skills_root).unwrap();
let sample = skills_root.join("pdf").join("SKILL.md");
assert!(sample.exists());
let content = std::fs::read_to_string(sample).unwrap();
assert!(!content.trim().is_empty());
cleanup(&root);
}
#[test]
fn test_ensure_builtin_skills_does_not_overwrite_existing_file() {
let root = temp_root();
let skills_root = root.join("skills");
let custom_pdf = skills_root.join("pdf");
std::fs::create_dir_all(&custom_pdf).unwrap();
let custom_file = custom_pdf.join("SKILL.md");
std::fs::write(&custom_file, "custom-content").unwrap();
ensure_builtin_skills(&skills_root).unwrap();
let content = std::fs::read_to_string(custom_file).unwrap();
assert_eq!(content, "custom-content");
cleanup(&root);
}
#[test]
fn test_get_file_with_relative_dir_handles_include_dir_subdirs() {
let pdf_dir = BUILTIN_SKILLS_DIR.get_dir("pdf").unwrap();
assert!(pdf_dir.get_file("SKILL.md").is_none());
assert!(get_file_with_relative_dir(pdf_dir, "SKILL.md").is_some());
}
#[test]
fn test_parse_frontmatter_accepts_crlf() {
let frontmatter =
"---\r\nname: mac-only\r\nplatforms:\r\n - darwin\r\ndeps: []\r\n---\r\n";
let parsed = parse_frontmatter(frontmatter).unwrap();
assert_eq!(parsed.platforms, vec!["darwin"]);
}
#[test]
fn test_ensure_builtin_skills_includes_new_macos_and_weather_skills() {
let root = temp_root();
let skills_root = root.join("skills");
ensure_builtin_skills(&skills_root).unwrap();
for skill in [
"pdf",
"docx",
"xlsx",
"pptx",
"skill-creator",
"calculator",
"planning",
"code-review",
"regex",
"unit-converter",
"datetime",
"csv-tools",
"json-tools",
"debugging",
"shell-scripting",
"api-design",
"testing",
"git",
"research",
"wikipedia",
"define",
"brainstorming",
"decision-matrix",
"meeting-notes",
"goal-setting",
"mermaid",
"color-tools",
"writing-editor",
"summarize",
"email-drafting",
"translate",
"sql",
"qrcode",
"data-analysis",
"algorithmic-art",
] {
let skill_file = skills_root.join(skill).join("SKILL.md");
assert!(skill_file.exists(), "missing built-in skill: {skill}");
let content = std::fs::read_to_string(skill_file).unwrap();
assert!(!content.trim().is_empty(), "empty skill file: {skill}");
}
#[cfg(not(target_os = "macos"))]
{
assert!(!skills_root.join("apple-notes").exists());
assert!(!skills_root.join("apple-reminders").exists());
assert!(!skills_root.join("apple-calendar").exists());
}
#[cfg(target_os = "macos")]
{
if command_exists("memo") {
assert!(skills_root.join("apple-notes").exists());
} else {
assert!(!skills_root.join("apple-notes").exists());
}
if command_exists("remindctl") {
assert!(skills_root.join("apple-reminders").exists());
} else {
assert!(!skills_root.join("apple-reminders").exists());
}
if command_exists("icalBuddy") {
assert!(skills_root.join("apple-calendar").exists());
} else {
assert!(!skills_root.join("apple-calendar").exists());
}
}
if command_exists("curl") {
assert!(skills_root.join("find-skills").exists());
assert!(skills_root.join("weather").exists());
}
cleanup(&root);
}
#[test]
fn test_skill_skip_reason_parses_compatibility() {
let content = r#"---
name: x
description: x
compatibility:
os: [darwin]
deps: [curl]
---
body
"#;
let reason = skill_skip_reason(content);
if cfg!(target_os = "macos") && command_exists("curl") {
assert!(reason.is_none());
} else {
assert!(reason.is_some());
}
}
#[test]
fn test_ensure_builtin_skills_includes_contract_governed_research_workflow() {
let root = temp_root();
let skills_root = root.join("skills");
ensure_builtin_skills(&skills_root).unwrap();
let workflow =
std::fs::read_to_string(skills_root.join("deep-research-workflow").join("SKILL.md"))
.unwrap();
assert!(workflow.contains("subagents_orchestrate"));
assert!(workflow.contains("VERDICT: PASS"));
assert!(workflow.contains("Citation coverage"));
std::fs::remove_dir_all(root).unwrap();
}
}