use crate::services::shared::{AgentServiceError, Result};
use std::path::{Path, PathBuf};
use systemprompt_config::ProfileBootstrap;
use systemprompt_identifiers::SkillId;
use systemprompt_loader::ServicesRootBootstrap;
use systemprompt_manifest::{DiskSkillConfig, SKILL_CONFIG_FILENAME, strip_frontmatter};
pub(super) struct LoadedDiskSkill {
pub(super) skill_id: SkillId,
pub(super) name: String,
pub(super) description: String,
pub(super) instructions: String,
}
pub(super) fn resolve_skills_root() -> Result<PathBuf> {
let profile = ProfileBootstrap::get()
.map_err(|e| AgentServiceError::operation("Profile not initialized for SkillService", e))?;
Ok(ServicesRootBootstrap::active_path_or(
&profile.paths.services,
"skills",
))
}
pub(super) fn load_disk_skill(skills_root: &Path, skill_id: &SkillId) -> Result<LoadedDiskSkill> {
let id_str = skill_id.as_str();
let skill_dir = skills_root.join(id_str);
let config_path = skill_dir.join(SKILL_CONFIG_FILENAME);
if !config_path.exists() {
return Err(AgentServiceError::SkillNotOnDisk {
skill_id: skill_id.clone(),
path: config_path,
});
}
let config_text = std::fs::read_to_string(&config_path).map_err(|e| {
AgentServiceError::operation(format!("Failed to read {}", config_path.display()), e)
})?;
let config: DiskSkillConfig = serde_yaml::from_str(&config_text).map_err(|e| {
AgentServiceError::operation(format!("Invalid YAML in {}", config_path.display()), e)
})?;
let resolved_id = config.id.clone().unwrap_or_else(|| skill_id.clone());
let content_path = skill_dir.join(config.content_file());
let instructions = if content_path.exists() {
let raw = std::fs::read_to_string(&content_path).map_err(|e| {
AgentServiceError::operation(format!("Failed to read {}", content_path.display()), e)
})?;
strip_frontmatter(&raw)
} else {
String::new()
};
let name = if config.name.is_empty() {
id_str.to_owned()
} else {
config.name
};
Ok(LoadedDiskSkill {
skill_id: resolved_id,
name,
description: config.description,
instructions,
})
}