Skip to main content

xtalforge_agent/
skill.rs

1//! Skill documents shipped with the CLI (Task 8.2.3).
2//!
3//! The files under `skill/` are compiled into the crate. `xtalforge skill
4//! show` prints one, `xtalforge skill install` writes them into a directory an
5//! assistant can read, and `xtalforge skill path` prints the default place.
6//! The same material is published as the automation guide.
7
8use std::path::{Path, PathBuf};
9
10use crate::Error;
11
12/// One embedded skill document.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub struct SkillDocument {
15    /// Relative path, for example `references/porosity.md`.
16    pub name: &'static str,
17    /// One-line title.
18    pub title: &'static str,
19    /// Markdown body.
20    pub body: &'static str,
21}
22
23macro_rules! document {
24    ($name:literal, $title:literal) => {
25        SkillDocument {
26            name: $name,
27            title: $title,
28            body: include_str!(concat!("../skill/", $name)),
29        }
30    };
31}
32
33/// Every embedded document, main skill first.
34pub const DOCUMENTS: &[SkillDocument] = &[
35    document!("SKILL.md", "How to drive xtalforge"),
36    document!("mcp.md", "MCP client setup"),
37    document!("prompts/build-mof.md", "Prompt: build a MOF"),
38    document!("prompts/porosity-report.md", "Prompt: porosity report"),
39    document!(
40        "prompts/prepare-deposited-cif.md",
41        "Prompt: prepare a deposited CIF"
42    ),
43    document!("references/api.md", "Reference: API and surfaces"),
44    document!("references/calculations.md", "Reference: calculations"),
45    document!("references/carbon.md", "Reference: carbon builders"),
46    document!("references/diagnostics.md", "Reference: diagnostics"),
47    document!("references/mof.md", "Reference: MOF builder"),
48    document!("references/polymer.md", "Reference: polymers"),
49    document!("references/porosity.md", "Reference: porosity"),
50    document!("references/prepare.md", "Reference: preparation"),
51    document!(
52        "references/workspace.md",
53        "Reference: documents and history"
54    ),
55];
56
57/// Looks up a document by path or by base name without extension.
58#[must_use]
59pub fn find(name: &str) -> Option<&'static SkillDocument> {
60    let wanted = name.trim().trim_start_matches('/');
61    DOCUMENTS.iter().find(|document| {
62        document.name == wanted
63            || document
64                .name
65                .strip_suffix(".md")
66                .is_some_and(|stem| stem == wanted)
67    })
68}
69
70/// Default install directory: `$XDG_DATA_HOME/xtalforge/skill`, or
71/// `~/.local/share/xtalforge/skill` when the variable is unset.
72#[must_use]
73pub fn default_directory() -> PathBuf {
74    if let Some(base) = std::env::var_os("XDG_DATA_HOME")
75        && !base.is_empty()
76    {
77        return PathBuf::from(base).join("xtalforge").join("skill");
78    }
79    if let Some(home) = std::env::var_os("HOME")
80        && !home.is_empty()
81    {
82        return PathBuf::from(home)
83            .join(".local")
84            .join("share")
85            .join("xtalforge")
86            .join("skill");
87    }
88    PathBuf::from("xtalforge-skill")
89}
90
91/// Writes every document under `directory`, creating parents.
92///
93/// # Errors
94///
95/// Returns [`Error::Io`] when a directory cannot be created or a file cannot
96/// be written.
97pub fn install(directory: &Path) -> Result<Vec<PathBuf>, Error> {
98    let mut written = Vec::with_capacity(DOCUMENTS.len());
99    for document in DOCUMENTS {
100        let path = directory.join(document.name);
101        if let Some(parent) = path.parent() {
102            std::fs::create_dir_all(parent)
103                .map_err(|error| Error::Io(format!("{}: {error}", parent.display())))?;
104        }
105        std::fs::write(&path, document.body)
106            .map_err(|error| Error::Io(format!("{}: {error}", path.display())))?;
107        written.push(path);
108    }
109    Ok(written)
110}