creamtop 0.1.0

MCP server that manages a GitHub-backed shared skill library for Claude Code.
//! GitHub client for fetching and publishing skills.

use anyhow::{anyhow, Context, Result};
use octocrab::Octocrab;

use crate::skills::{parse_skill_meta, SkillMeta, Source};

pub fn build_client(token: Option<&str>) -> Result<Octocrab> {
    let mut builder = Octocrab::builder();
    if let Some(token) = token {
        builder = builder.personal_token(token.to_string());
    }
    builder.build().context("build octocrab client")
}

pub struct FetchedSkill {
    pub meta: SkillMeta,
    pub content: String,
}

pub async fn fetch_repo_skills(
    client: &Octocrab,
    owner: &str,
    repo: &str,
) -> Result<Vec<FetchedSkill>> {
    let repos = client.repos(owner, repo);
    let mut root = repos
        .get_content()
        .path("")
        .send()
        .await
        .with_context(|| format!("list contents of {owner}/{repo}"))?;

    let mut skills = Vec::new();
    for item in root.take_items() {
        if item.r#type != "dir" {
            continue;
        }
        match fetch_skill_content(client, owner, repo, &item.name).await {
            Ok(content) => {
                let meta = parse_skill_meta(&item.name, &content, Source::Remote);
                skills.push(FetchedSkill { meta, content });
            }
            Err(_) => continue, // Skip directories without SKILL.md.
        }
    }
    Ok(skills)
}

pub async fn fetch_skill_content(
    client: &Octocrab,
    owner: &str,
    repo: &str,
    name: &str,
) -> Result<String> {
    let path = format!("{name}/SKILL.md");
    let mut resp = client
        .repos(owner, repo)
        .get_content()
        .path(&path)
        .send()
        .await
        .with_context(|| format!("get {path}"))?;

    let items = resp.take_items();
    let file = items
        .into_iter()
        .find(|c| c.r#type == "file")
        .ok_or_else(|| anyhow!("{path} not found or not a file"))?;

    file.decoded_content()
        .ok_or_else(|| anyhow!("{path} has no content"))
}

pub async fn push_skill_content(
    client: &Octocrab,
    owner: &str,
    repo: &str,
    name: &str,
    content: &str,
    message: &str,
) -> Result<()> {
    let path = format!("{name}/SKILL.md");
    let repos = client.repos(owner, repo);

    // Look up existing SHA for an update; treat any failure as "does not exist".
    let existing_sha = match repos.get_content().path(&path).send().await {
        Ok(mut resp) => resp
            .take_items()
            .into_iter()
            .find(|c| c.r#type == "file")
            .map(|c| c.sha),
        Err(_) => None,
    };

    match existing_sha {
        Some(sha) => {
            repos
                .update_file(&path, message, content, sha)
                .send()
                .await
                .with_context(|| format!("update {path}"))?;
        }
        None => {
            repos
                .create_file(&path, message, content)
                .send()
                .await
                .with_context(|| format!("create {path}"))?;
        }
    }
    Ok(())
}