magi-code 0.96.2

Repository-aware CLI coding agent for terminal work
Documentation
use std::{
    collections::{BTreeMap, BTreeSet},
    fs,
    path::{Path, PathBuf},
};

pub const DISCOVERY_SKILL_MAX_BYTES: u64 = 1024 * 1024;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DiscoveredSkill {
    pub name: String,
    pub path: PathBuf,
    pub frontmatter: BTreeMap<String, String>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SkillDiagnostic {
    pub path: PathBuf,
    pub message: String,
}

#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SkillDiscovery {
    pub skills: BTreeMap<String, DiscoveredSkill>,
    pub diagnostics: Vec<SkillDiagnostic>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SkillRoots {
    pub mc_skills: PathBuf,
    pub repo_skills: PathBuf,
    pub legacy_agents_skills: PathBuf,
    pub additional_paths: Vec<PathBuf>,
}

impl SkillRoots {
    pub fn from_paths(mc_root: &Path, project_root: &Path) -> Self {
        let home = dirs::home_dir();
        Self {
            mc_skills: mc_root.join("skills"),
            repo_skills: project_root.join(".agents").join("skills"),
            legacy_agents_skills: legacy_agents_skills_root(home),
            additional_paths: Vec::new(),
        }
    }
}

fn legacy_agents_skills_root(home: Option<PathBuf>) -> PathBuf {
    home.map(|home| home.join(".agents").join("skills"))
        .unwrap_or_else(|| PathBuf::from("/.magi-code-no-home-skills-sentinel"))
}

pub fn discover_skills(roots: &SkillRoots) -> SkillDiscovery {
    let mut discovery = SkillDiscovery::default();
    // Deterministic collision policy: higher-priority roots overwrite lower-priority
    // roots for the same skill directory name. Priority is:
    // repo `.agents/skills` > configured additional paths in listed order >
    // `~/.magi-code/skills` > legacy `~/.agents/skills`.
    discover_root(&roots.legacy_agents_skills, &mut discovery);
    discover_root(&roots.mc_skills, &mut discovery);
    for root in &roots.additional_paths {
        discover_additional_root(root, &mut discovery);
    }
    discover_root(&roots.repo_skills, &mut discovery);
    discovery
}

fn discover_additional_root(root: &Path, discovery: &mut SkillDiscovery) {
    if !root.is_absolute() {
        discovery.diagnostics.push(SkillDiagnostic {
            path: root.to_path_buf(),
            message: "additional skill path must be absolute".to_string(),
        });
        return;
    }
    discover_root(root, discovery);
}

fn discover_root(root: &Path, discovery: &mut SkillDiscovery) {
    let Ok(root_metadata) = fs::metadata(root) else {
        return;
    };
    if !root_metadata.is_dir() {
        return;
    }

    let mut skill_files = Vec::new();
    collect_shallow_skill_files(root, discovery, &mut skill_files);
    skill_files.sort();

    let mut seen_in_root = BTreeMap::new();
    for path in skill_files {
        match load_skill_descriptor(&path) {
            Ok(skill) => {
                if let Some(previous_path) = seen_in_root.insert(skill.name.clone(), path.clone()) {
                    discovery.diagnostics.push(SkillDiagnostic {
                        path: path.clone(),
                        message: format!(
                            "duplicate skill name '{}' in same root: {} and {}",
                            skill.name,
                            previous_path.display(),
                            path.display()
                        ),
                    });
                }
                discovery.skills.insert(skill.name.clone(), skill);
            }
            Err(error) => discovery.diagnostics.push(SkillDiagnostic {
                path,
                message: error.to_string(),
            }),
        }
    }
}

fn collect_shallow_skill_files(
    root: &Path,
    discovery: &mut SkillDiscovery,
    skill_files: &mut Vec<PathBuf>,
) {
    let entries = match sorted_read_dir(root) {
        Ok(entries) => entries,
        Err(error) => {
            discovery.diagnostics.push(SkillDiagnostic {
                path: root.to_path_buf(),
                message: error.to_string(),
            });
            return;
        }
    };

    for entry in entries {
        let path = entry.path();
        let Ok(file_type) = entry.file_type() else {
            discovery.diagnostics.push(SkillDiagnostic {
                path,
                message: "could not read skill path file type".to_string(),
            });
            continue;
        };
        if !file_type.is_dir() {
            continue;
        }

        let nested_entries = match sorted_read_dir(&path) {
            Ok(entries) => entries,
            Err(error) => {
                discovery.diagnostics.push(SkillDiagnostic {
                    path,
                    message: error.to_string(),
                });
                continue;
            }
        };
        let mut nested_dirs = Vec::new();
        for nested in nested_entries {
            let nested_path = nested.path();
            let Ok(nested_type) = nested.file_type() else {
                discovery.diagnostics.push(SkillDiagnostic {
                    path: nested_path,
                    message: "could not read skill path file type".to_string(),
                });
                continue;
            };
            if nested.file_name() == "SKILL.md" {
                skill_files.push(nested_path);
            } else if nested_type.is_dir() {
                nested_dirs.push(nested_path);
            }
        }

        for nested_path in nested_dirs {
            let nested_entries = match sorted_read_dir(&nested_path) {
                Ok(entries) => entries,
                Err(error) => {
                    discovery.diagnostics.push(SkillDiagnostic {
                        path: nested_path,
                        message: error.to_string(),
                    });
                    continue;
                }
            };
            for nested in nested_entries {
                let nested_skill_path = nested.path();
                let Ok(_nested_type) = nested.file_type() else {
                    discovery.diagnostics.push(SkillDiagnostic {
                        path: nested_skill_path,
                        message: "could not read skill path file type".to_string(),
                    });
                    continue;
                };
                if nested.file_name() == "SKILL.md" {
                    skill_files.push(nested_skill_path);
                }
            }
        }
    }
}

fn sorted_read_dir(path: &Path) -> std::io::Result<Vec<fs::DirEntry>> {
    let mut entries = fs::read_dir(path)?.collect::<Result<Vec<_>, _>>()?;
    entries.sort_by_key(|entry| entry.path());
    Ok(entries)
}

pub fn filter_enabled_skills(
    discovery: &SkillDiscovery,
    disabled: &BTreeSet<String>,
) -> SkillDiscovery {
    let skills = discovery
        .skills
        .iter()
        .filter(|(name, _)| !disabled.contains(*name))
        .map(|(name, skill)| (name.clone(), skill.clone()))
        .collect();
    SkillDiscovery {
        skills,
        diagnostics: discovery.diagnostics.clone(),
    }
}

pub fn load_skill_descriptor(path: &Path) -> anyhow::Result<DiscoveredSkill> {
    let file = crate::prompt_file::read_prompt_file(path, DISCOVERY_SKILL_MAX_BYTES, true)
        .map_err(|error| {
            let message = error.to_string();
            if message.contains("symlink") {
                anyhow::anyhow!("skill discovery requires regular file: {}", path.display())
            } else if message.contains("exceeds") {
                anyhow::anyhow!("skill discovery read limit exceeded: {message}")
            } else {
                error
            }
        })?;
    let frontmatter = parse_skill_frontmatter(&file.text)?;
    let name = path
        .parent()
        .and_then(Path::file_name)
        .ok_or_else(|| {
            anyhow::anyhow!(
                "skill path has no parent directory name: {}",
                path.display()
            )
        })?
        .to_string_lossy()
        .to_string();
    Ok(DiscoveredSkill {
        name,
        path: path.to_path_buf(),
        frontmatter,
    })
}

pub fn parse_skill_frontmatter(text: &str) -> anyhow::Result<BTreeMap<String, String>> {
    let normalized;
    let text = if text.contains("\r\n") {
        normalized = text.replace("\r\n", "\n");
        normalized.as_str()
    } else {
        text
    };
    if !text.starts_with("---\n") {
        return Ok(BTreeMap::new());
    }
    let rest = &text[4..];
    let Some(end) = rest.find("\n---\n") else {
        anyhow::bail!("frontmatter start marker without closing marker");
    };
    let frontmatter_text = &rest[..end];
    let mut frontmatter = BTreeMap::new();
    for line in frontmatter_text
        .lines()
        .filter(|line| !line.trim().is_empty())
    {
        let Some((key, value)) = line.split_once(':') else {
            anyhow::bail!("malformed frontmatter line: {line}");
        };
        let key = key.trim();
        let value = parse_flat_frontmatter_scalar(value.trim())?;
        frontmatter.insert(key.to_string(), value);
    }
    Ok(frontmatter)
}

fn parse_flat_frontmatter_scalar(value: &str) -> anyhow::Result<String> {
    let value = value.trim();
    let Some(&first) = value.as_bytes().first() else {
        return Ok(String::new());
    };
    if matches!(first, b'"' | b'\'') {
        if value.len() < 2 || value.as_bytes().last() != Some(&first) {
            anyhow::bail!("frontmatter scalar has an unmatched quote");
        }
        return Ok(value[1..value.len() - 1].to_string());
    }
    Ok(value.to_string())
}