Skip to main content

bake_agent_context/
lib.rs

1// Released under the MIT License.
2// Copyright, 2026, by Samuel Williams.
3
4//! Bake tasks and APIs for consuming context directories shipped by Cargo packages.
5mod exclude;
6mod index;
7mod installer;
8mod skill;
9
10pub use index::ContextIndex;
11pub use installer::{ContextFile, ContextPackage, Installer};
12pub use skill::{Skill, install_skills, list_skills};
13
14use bake::{Context, Error, Result};
15
16fn installer(context: &mut Context) -> Result<Installer> {
17    if let Some(installer) = context.get::<Installer>() {
18        return Ok(installer.clone());
19    }
20
21    let installer = Installer::new(context.root())?;
22    context.insert(installer.clone());
23    Ok(installer)
24}
25
26/// List dependency crates that ship context, or list one crate's context files.
27#[bake::task(name = "agent:context:list")]
28pub fn list(context: &mut Context, package: Option<String>) -> Result<String> {
29    let installer = installer(context)?;
30
31    if let Some(package) = package {
32        let Some(package) = installer.find_package(&package)? else {
33            return Ok(format!("No context found for crate '{package}'"));
34        };
35
36        let files = installer.list_context_files(&package)?;
37        if files.is_empty() {
38            return Ok(format!(
39                "No context files found for crate '{}'.",
40                package.selector()
41            ));
42        }
43        let mut output = format!("Context files for crate '{}':", package.selector());
44        for file in files {
45            output.push_str(&format!("\n  {}", file.path.display()));
46        }
47        return Ok(output);
48    }
49
50    let mut packages = Vec::new();
51    for package in installer.packages() {
52        if !installer.list_context_files(package)?.is_empty() {
53            packages.push(package);
54        }
55    }
56    if packages.is_empty() {
57        return Ok("No dependency context files found".to_owned());
58    }
59
60    let mut output = String::from("Crates with context available:");
61    for package in packages {
62        output.push_str(&format!("\n  {} ({})", package.selector(), package.version));
63    }
64    Ok(output)
65}
66
67/// Show a context file from one resolved dependency crate.
68#[bake::task(name = "agent:context:show")]
69pub fn show(
70    context: &mut Context,
71    #[bake(named)] package: String,
72    #[bake(named)] file: String,
73) -> Result<String> {
74    let installer = installer(context)?;
75    let Some(content) = installer.show_context_file(&package, &file)? else {
76        return Err(Error::new(format!(
77            "context file {file:?} was not found in crate {package:?}"
78        )));
79    };
80    Ok(content)
81}
82
83/// Install context and skills from one crate or all dependencies, then update the context index.
84#[bake::task(name = "agent:context:install")]
85pub fn install(context: &mut Context, package: Option<String>) -> Result<String> {
86    let installer = installer(context)?;
87    let installed_context = if let Some(package) = package.as_deref() {
88        if installer.install_package(package)? {
89            vec![package.to_owned()]
90        } else {
91            Vec::new()
92        }
93    } else {
94        installer.install_all()?
95    };
96    let installed_skills = install_skills(&installer, package.as_deref(), None)?;
97
98    ContextIndex::new(context.root())
99        .with_packages(installer.packages())
100        .update_index()?;
101
102    let mut output = Vec::new();
103    if !installed_context.is_empty() {
104        output.push(format!(
105            "Installed context from: {}",
106            installed_context.join(", ")
107        ));
108    }
109    if !installed_skills.is_empty() {
110        output.push(format!("Installed skills: {}", installed_skills.join(", ")));
111    }
112
113    if output.is_empty() {
114        Ok("No dependency context or skills were installed".to_owned())
115    } else {
116        Ok(output.join("\n"))
117    }
118}
119
120/// List skills declared by dependency context documents.
121#[bake::task(name = "agent:context:skill:list")]
122pub fn skill_list(context: &mut Context, package: Option<String>) -> Result<String> {
123    let installer = installer(context)?;
124    let skills = list_skills(&installer, package.as_deref())?;
125    if skills.is_empty() {
126        return Ok("No dependency skills found".to_owned());
127    }
128
129    let mut output = String::from("Available dependency skills:");
130    for skill in skills {
131        output.push_str(&format!(
132            "\n  {} ({}) — {}",
133            skill.name,
134            skill.package_selector(),
135            skill.description
136        ));
137    }
138    Ok(output)
139}
140
141/// Install skills from all dependencies or select a crate and/or package-prefixed skill name.
142#[bake::task(name = "agent:context:skill:install")]
143pub fn skill_install(
144    context: &mut Context,
145    package: Option<String>,
146    skill: Option<String>,
147) -> Result<String> {
148    let installer = installer(context)?;
149    let installed = install_skills(&installer, package.as_deref(), skill.as_deref())?;
150    if installed.is_empty() {
151        Ok("No dependency skills were installed".to_owned())
152    } else {
153        Ok(format!("Installed skills: {}", installed.join(", ")))
154    }
155}
156
157/// Create or update `.agents/context/index.md` from installed context files.
158#[bake::task(name = "agent:context:index")]
159pub fn index(context: &mut Context) -> Result<String> {
160    let installer = installer(context)?;
161    ContextIndex::new(context.root())
162        .with_packages(installer.packages())
163        .update_index()?;
164    Ok("Updated .agents/context/index.md".to_owned())
165}