use crate::error::{SkillError, SkillResult};
use crate::index::build_document;
use crate::model::{SkillDocument, SkillIndex};
use crate::parser::parse_skill_markdown;
use crate::registry::client::{SkillContent, SkillRegistryClient};
use std::path::PathBuf;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RegistrySkillSelector {
Query {
query: String,
top_k: Option<u32>,
},
Names(Vec<String>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RegistrySkillFilter {
pub selector: RegistrySkillSelector,
pub revision: Option<String>,
}
impl RegistrySkillFilter {
pub fn by_query(query: impl Into<String>) -> Self {
Self {
selector: RegistrySkillSelector::Query { query: query.into(), top_k: None },
revision: None,
}
}
pub fn by_names<I, S>(names: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
Self {
selector: RegistrySkillSelector::Names(names.into_iter().map(Into::into).collect()),
revision: None,
}
}
#[must_use]
pub fn with_top_k(mut self, top_k: u32) -> Self {
if let RegistrySkillSelector::Query { top_k: slot, .. } = &mut self.selector {
*slot = Some(top_k);
}
self
}
#[must_use]
pub fn with_revision(mut self, revision: impl Into<String>) -> Self {
self.revision = Some(revision.into());
self
}
}
pub async fn load_skill_index_from_registry(
client: &SkillRegistryClient,
filter: RegistrySkillFilter,
) -> SkillResult<SkillIndex> {
let names: Vec<String> = match &filter.selector {
RegistrySkillSelector::Query { query, top_k } => client
.search_skills(query, *top_k)
.await?
.into_iter()
.map(|retrieved| retrieved.skill_name)
.collect(),
RegistrySkillSelector::Names(names) => names.clone(),
};
let mut skills = Vec::with_capacity(names.len());
for name in names {
let content = match &filter.revision {
Some(revision) => client.fetch_skill_revision_content(&name, revision).await?,
None => client.fetch_skill_content(&name).await?,
};
skills.push(document_from_content(&content)?);
}
skills.sort_by(|a, b| a.name.cmp(&b.name).then_with(|| a.path.cmp(&b.path)));
tracing::debug!(skill.count = skills.len(), "loaded skill index from registry");
Ok(SkillIndex::new(skills))
}
pub fn merge_skill_indexes(local: SkillIndex, remote: SkillIndex) -> SkillIndex {
let mut skills: Vec<SkillDocument> = local.skills().to_vec();
for skill in remote.skills() {
if local.find_by_name(&skill.name).is_none() {
skills.push(skill.clone());
}
}
skills.sort_by(|a, b| a.name.cmp(&b.name).then_with(|| a.path.cmp(&b.path)));
SkillIndex::new(skills)
}
pub(crate) fn document_from_content(content: &SkillContent) -> SkillResult<SkillDocument> {
let skill_md = content.skill_md().ok_or_else(|| {
SkillError::Validation(format!(
"skill `{}` package has no SKILL.md at its root; repackage the skill with SKILL.md at the top level",
content.skill.name,
))
})?;
let text = std::str::from_utf8(skill_md).map_err(|_| {
SkillError::Validation(format!(
"SKILL.md in skill `{}` is not valid UTF-8",
content.skill.name,
))
})?;
let path = PathBuf::from(format!("{}/{}", content.skill.name, SkillContent::SKILL_MD));
let parsed = parse_skill_markdown(&path, text)?;
Ok(build_document(parsed, path, text, None))
}