use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use super::frontmatter::{parse_frontmatter, split_frontmatter, SkillManifest};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Layer {
Cli,
Env,
Project,
Manifest,
User,
Package,
System,
Host,
}
impl Layer {
pub fn label(self) -> &'static str {
match self {
Layer::Cli => "cli",
Layer::Env => "env",
Layer::Project => "project",
Layer::Manifest => "manifest",
Layer::User => "user",
Layer::Package => "package",
Layer::System => "system",
Layer::Host => "host",
}
}
pub fn from_label(label: &str) -> Option<Layer> {
match label {
"cli" => Some(Layer::Cli),
"env" => Some(Layer::Env),
"project" => Some(Layer::Project),
"manifest" => Some(Layer::Manifest),
"user" => Some(Layer::User),
"package" => Some(Layer::Package),
"system" => Some(Layer::System),
"host" => Some(Layer::Host),
_ => None,
}
}
pub const fn all() -> &'static [Layer] {
&[
Layer::Cli,
Layer::Env,
Layer::Project,
Layer::Manifest,
Layer::User,
Layer::Package,
Layer::System,
Layer::Host,
]
}
}
#[derive(Debug, Clone)]
pub struct Skill {
pub manifest: SkillManifest,
pub body: String,
pub skill_dir: Option<PathBuf>,
pub layer: Layer,
pub namespace: Option<String>,
pub unknown_fields: Vec<String>,
}
impl Skill {
pub fn id(&self) -> String {
match &self.namespace {
Some(ns) if !ns.is_empty() => format!("{ns}/{}", self.manifest.name),
_ => self.manifest.name.clone(),
}
}
}
pub trait SkillSource: Send + Sync {
fn list(&self) -> Vec<SkillManifestRef>;
fn fetch(&self, id: &str) -> Result<Skill, String>;
fn layer(&self) -> Layer;
fn describe(&self) -> String;
}
#[derive(Debug, Clone)]
pub struct SkillManifestRef {
pub id: String,
pub manifest: SkillManifest,
pub layer: Layer,
pub namespace: Option<String>,
pub origin: String,
}
#[derive(Debug, Clone)]
pub struct FsSkillSource {
pub root: PathBuf,
pub layer: Layer,
pub namespace: Option<String>,
}
impl FsSkillSource {
pub fn new(root: impl Into<PathBuf>, layer: Layer) -> Self {
Self {
root: root.into(),
layer,
namespace: None,
}
}
pub fn with_namespace(mut self, namespace: impl Into<String>) -> Self {
let ns = namespace.into();
self.namespace = if ns.is_empty() { None } else { Some(ns) };
self
}
fn iter_skill_dirs(&self) -> Vec<PathBuf> {
let mut results = Vec::new();
if !self.root.is_dir() {
return results;
}
if self.root.join("SKILL.md").is_file() {
results.push(self.root.clone());
return results;
}
let Ok(entries) = fs::read_dir(&self.root) else {
return results;
};
for entry in entries.flatten() {
let path = entry.path();
if !path.is_dir() {
continue;
}
if path.join("SKILL.md").is_file() {
results.push(path);
}
}
results.sort();
results
}
fn load_from_dir(&self, dir: &Path) -> Result<Skill, String> {
let skill_file = dir.join("SKILL.md");
let source = fs::read_to_string(&skill_file)
.map_err(|e| format!("failed to read {}: {e}", skill_file.display()))?;
let (fm, body) = split_frontmatter(&source);
let parsed = parse_frontmatter(fm).map_err(|e| format!("{}: {e}", skill_file.display()))?;
let mut manifest = parsed.manifest;
if manifest.name.is_empty() {
if let Some(name) = dir.file_name().and_then(|n| n.to_str()) {
manifest.name = name.to_string();
}
}
let skill = Skill {
body: body.to_string(),
skill_dir: Some(dir.to_path_buf()),
layer: self.layer,
namespace: self.namespace.clone(),
unknown_fields: parsed.unknown_fields,
manifest,
};
if skill.manifest.name.is_empty() {
return Err(format!(
"{}: SKILL.md has no `name` field and directory has no basename",
skill_file.display()
));
}
Ok(skill)
}
}
impl SkillSource for FsSkillSource {
fn list(&self) -> Vec<SkillManifestRef> {
let mut out = Vec::new();
for dir in self.iter_skill_dirs() {
match self.load_from_dir(&dir) {
Ok(skill) => {
let id = skill.id();
out.push(SkillManifestRef {
id,
manifest: skill.manifest,
layer: skill.layer,
namespace: skill.namespace,
origin: dir.display().to_string(),
});
}
Err(err) => {
eprintln!("warning: skills: {err}");
}
}
}
out
}
fn fetch(&self, id: &str) -> Result<Skill, String> {
let target_name = match id.rsplit_once('/') {
Some((_, n)) => n,
None => id,
};
for dir in self.iter_skill_dirs() {
let skill = self.load_from_dir(&dir)?;
if skill.id() == id || skill.manifest.name == target_name {
return Ok(skill);
}
}
Err(format!(
"skill '{id}' not found under {}",
self.root.display()
))
}
fn layer(&self) -> Layer {
self.layer
}
fn describe(&self) -> String {
match &self.namespace {
Some(ns) => format!("{} [{}] ns={ns}", self.root.display(), self.layer.label()),
None => format!("{} [{}]", self.root.display(), self.layer.label()),
}
}
}
pub type HostSkillLister = Arc<dyn Fn() -> Vec<SkillManifestRef> + Send + Sync>;
pub type HostSkillFetcher = Arc<dyn Fn(&str) -> Result<Skill, String> + Send + Sync>;
pub struct HostSkillSource {
loader: HostSkillLister,
fetcher: HostSkillFetcher,
}
impl HostSkillSource {
pub fn new<L, F>(loader: L, fetcher: F) -> Self
where
L: Fn() -> Vec<SkillManifestRef> + Send + Sync + 'static,
F: Fn(&str) -> Result<Skill, String> + Send + Sync + 'static,
{
Self {
loader: Arc::new(loader),
fetcher: Arc::new(fetcher),
}
}
}
impl std::fmt::Debug for HostSkillSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("HostSkillSource").finish_non_exhaustive()
}
}
impl SkillSource for HostSkillSource {
fn list(&self) -> Vec<SkillManifestRef> {
(self.loader)()
}
fn fetch(&self, id: &str) -> Result<Skill, String> {
(self.fetcher)(id)
}
fn layer(&self) -> Layer {
Layer::Host
}
fn describe(&self) -> String {
"host-provided [host]".to_string()
}
}
pub fn skill_entry_to_vm(skill: &Skill) -> crate::value::VmValue {
use crate::value::VmValue;
use std::rc::Rc;
let mut entry: BTreeMap<String, VmValue> = BTreeMap::new();
entry.insert(
"name".to_string(),
VmValue::String(Rc::from(skill.manifest.name.as_str())),
);
entry.insert(
"description".to_string(),
VmValue::String(Rc::from(skill.manifest.description.as_str())),
);
if let Some(when) = &skill.manifest.when_to_use {
entry.insert(
"when_to_use".to_string(),
VmValue::String(Rc::from(when.as_str())),
);
}
if skill.manifest.disable_model_invocation {
entry.insert("disable_model_invocation".to_string(), VmValue::Bool(true));
}
if !skill.manifest.allowed_tools.is_empty() {
entry.insert(
"allowed_tools".to_string(),
VmValue::List(Rc::new(
skill
.manifest
.allowed_tools
.iter()
.map(|t| VmValue::String(Rc::from(t.as_str())))
.collect(),
)),
);
}
if skill.manifest.user_invocable {
entry.insert("user_invocable".to_string(), VmValue::Bool(true));
}
if !skill.manifest.paths.is_empty() {
entry.insert(
"paths".to_string(),
VmValue::List(Rc::new(
skill
.manifest
.paths
.iter()
.map(|p| VmValue::String(Rc::from(p.as_str())))
.collect(),
)),
);
}
if let Some(context) = &skill.manifest.context {
entry.insert(
"context".to_string(),
VmValue::String(Rc::from(context.as_str())),
);
}
if let Some(agent) = &skill.manifest.agent {
entry.insert(
"agent".to_string(),
VmValue::String(Rc::from(agent.as_str())),
);
}
if !skill.manifest.hooks.is_empty() {
let mut hooks: BTreeMap<String, VmValue> = BTreeMap::new();
for (k, v) in &skill.manifest.hooks {
hooks.insert(k.clone(), VmValue::String(Rc::from(v.as_str())));
}
entry.insert("hooks".to_string(), VmValue::Dict(Rc::new(hooks)));
}
if let Some(model) = &skill.manifest.model {
entry.insert(
"model".to_string(),
VmValue::String(Rc::from(model.as_str())),
);
}
if let Some(effort) = &skill.manifest.effort {
entry.insert(
"effort".to_string(),
VmValue::String(Rc::from(effort.as_str())),
);
}
if let Some(shell) = &skill.manifest.shell {
entry.insert(
"shell".to_string(),
VmValue::String(Rc::from(shell.as_str())),
);
}
if let Some(hint) = &skill.manifest.argument_hint {
entry.insert(
"argument_hint".to_string(),
VmValue::String(Rc::from(hint.as_str())),
);
}
entry.insert(
"body".to_string(),
VmValue::String(Rc::from(skill.body.as_str())),
);
if let Some(dir) = &skill.skill_dir {
entry.insert(
"skill_dir".to_string(),
VmValue::String(Rc::from(dir.display().to_string().as_str())),
);
}
entry.insert(
"source".to_string(),
VmValue::String(Rc::from(skill.layer.label())),
);
if let Some(ns) = &skill.namespace {
entry.insert(
"namespace".to_string(),
VmValue::String(Rc::from(ns.as_str())),
);
}
VmValue::Dict(Rc::new(entry))
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
fn write(tmp: &Path, rel: &str, body: &str) {
let p = tmp.join(rel);
fs::create_dir_all(p.parent().unwrap()).unwrap();
fs::write(p, body).unwrap();
}
#[test]
fn fs_source_walks_one_level_deep() {
let tmp = tempfile::tempdir().unwrap();
write(
tmp.path(),
"deploy/SKILL.md",
"---\nname: deploy\ndescription: ship it\n---\nrun deploy",
);
write(
tmp.path(),
"review/SKILL.md",
"---\nname: review\n---\nbody",
);
write(tmp.path(), "not-a-skill.txt", "no");
let src = FsSkillSource::new(tmp.path(), Layer::Project);
let listed = src.list();
assert_eq!(listed.len(), 2);
let names: Vec<_> = listed.iter().map(|s| s.manifest.name.clone()).collect();
assert!(names.contains(&"deploy".to_string()));
assert!(names.contains(&"review".to_string()));
let skill = src.fetch("deploy").unwrap();
assert_eq!(skill.manifest.description, "ship it");
assert_eq!(skill.body, "run deploy");
}
#[test]
fn fs_source_accepts_root_as_single_skill() {
let tmp = tempfile::tempdir().unwrap();
write(tmp.path(), "SKILL.md", "---\nname: solo\n---\n(body)");
let src = FsSkillSource::new(tmp.path(), Layer::Cli);
let listed = src.list();
assert_eq!(listed.len(), 1);
assert_eq!(listed[0].manifest.name, "solo");
}
#[test]
fn fs_source_defaults_name_to_directory() {
let tmp = tempfile::tempdir().unwrap();
write(tmp.path(), "nameless/SKILL.md", "---\n---\nbody only");
let src = FsSkillSource::new(tmp.path(), Layer::User);
let skill = src.fetch("nameless").unwrap();
assert_eq!(skill.manifest.name, "nameless");
}
#[test]
fn fs_source_namespace_prefixes_id() {
let tmp = tempfile::tempdir().unwrap();
write(
tmp.path(),
"deploy/SKILL.md",
"---\nname: deploy\n---\nbody",
);
let src = FsSkillSource::new(tmp.path(), Layer::Manifest).with_namespace("acme/ops");
let listed = src.list();
assert_eq!(listed[0].id, "acme/ops/deploy");
let skill = src.fetch("acme/ops/deploy").unwrap();
assert_eq!(skill.id(), "acme/ops/deploy");
}
#[test]
fn fs_source_missing_root_is_empty_not_error() {
let src = FsSkillSource::new("/does/not/exist/anywhere", Layer::System);
assert!(src.list().is_empty());
assert!(src.fetch("nope").is_err());
}
#[test]
fn host_source_wraps_closures() {
let host = HostSkillSource::new(
|| {
vec![SkillManifestRef {
id: "h1".into(),
manifest: SkillManifest {
name: "h1".into(),
..Default::default()
},
layer: Layer::Host,
namespace: None,
origin: "host".into(),
}]
},
|id| {
Ok(Skill {
manifest: SkillManifest {
name: id.to_string(),
..Default::default()
},
body: "host body".into(),
skill_dir: None,
layer: Layer::Host,
namespace: None,
unknown_fields: Vec::new(),
})
},
);
assert_eq!(host.list().len(), 1);
let s = host.fetch("h1").unwrap();
assert_eq!(s.body, "host body");
assert_eq!(s.layer, Layer::Host);
}
#[test]
fn layer_label_roundtrips() {
for layer in Layer::all() {
assert_eq!(Layer::from_label(layer.label()), Some(*layer));
}
}
}