use crate::model::settings::{self, Agent};
use bezel::ui::icons::Icon;
use cacp_agents::{Distribution, Installed, registry};
use std::{
collections::HashMap,
path::{Path, PathBuf},
};
pub mod acp;
pub mod context;
pub mod mcp;
pub mod path;
pub mod serve;
pub fn cache_dir() -> Option<PathBuf> {
settings::dir().ok().map(|dir| dir.join("cache"))
}
pub fn icons(configured: &[settings::Agent]) -> HashMap<String, Icon> {
let Some(cache) = cache_dir() else {
return HashMap::new();
};
let Some(catalog) = registry::catalog(&cache) else {
return HashMap::new();
};
let icons: HashMap<&str, &str> = catalog
.agents
.iter()
.filter_map(|agent| Some((agent.id.as_str(), agent.icon.as_deref()?)))
.collect();
let by_package: HashMap<&str, &str> = catalog
.agents
.iter()
.filter_map(|agent| match &agent.distribution {
Distribution::Npm { package, .. } => {
Some((cacp_agents::package_name(package), agent.id.as_str()))
}
_ => None,
})
.collect();
let dir = cache.join("icons");
configured
.iter()
.filter_map(|entry| {
let id = match entry.id.as_deref() {
Some(id) => id,
None => entry
.args
.iter()
.find_map(|arg| by_package.get(cacp_agents::package_name(arg)).copied())?,
};
let path = fetch(&dir, id, icons.get(id)?)?;
Some((entry.name.clone(), Icon::file(path)))
})
.collect()
}
fn fetch(dir: &Path, id: &str, url: &str) -> Option<String> {
if let Some(path) = cached(dir, id) {
return Some(path);
}
let path = cacp_agents::contained(dir, &format!("{id}.svg")).ok()?;
let body = ureq::get(url).call().ok()?.body_mut().read_to_vec().ok()?;
std::fs::create_dir_all(dir).ok()?;
std::fs::write(&path, body).ok()?;
Some(path.to_str()?.to_owned())
}
fn cached(dir: &Path, id: &str) -> Option<String> {
let path = cacp_agents::contained(dir, &format!("{id}.svg")).ok()?;
path.exists().then(|| path.to_str())?.map(str::to_owned)
}
pub struct Listing {
pub agent: registry::Agent,
pub installed: Option<String>,
pub icon: Option<Icon>,
}
pub fn listings() -> Vec<Listing> {
let (Some(cache), Ok(data)) = (cache_dir(), settings::data_dir()) else {
return Vec::new();
};
let Some(catalog) = registry::catalog(&cache) else {
return Vec::new();
};
let dir = cache.join("icons");
catalog
.agents
.into_iter()
.map(|agent| {
let installed = Installed::find(&data, &agent.id).map(|found| found.version);
let icon = cached(&dir, &agent.id).map(Icon::file);
Listing {
agent,
installed,
icon,
}
})
.collect()
}
pub fn prefetch_icons() {
let Some(cache) = cache_dir() else {
return;
};
let Some(catalog) = registry::catalog(&cache) else {
return;
};
let dir = cache.join("icons");
for agent in &catalog.agents {
if let Some(url) = agent.icon.as_deref() {
fetch(&dir, &agent.id, url);
}
}
}
pub const OUTPUT_KEEP: usize = 40;
pub fn record(held: &mut Vec<String>, line: String) {
held.push(line);
if held.len() > OUTPUT_KEEP {
held.remove(0);
}
}
pub fn source_mark(agent: ®istry::Agent) -> String {
match &agent.distribution {
Distribution::Npm { package, .. } => {
format!("npm:{}", cacp_agents::package_name(package))
}
Distribution::Binary(binary) => format!("binary:{}", archive_host(&binary.archive)),
Distribution::Unsupported { kind } => format!("unsupported:{kind}"),
}
}
fn archive_host(archive: &str) -> &str {
let rest = archive
.strip_prefix("https://")
.or_else(|| archive.strip_prefix("http://"));
match rest {
Some(rest) => rest.split('/').next().unwrap_or(archive),
None => archive,
}
}
pub fn install(agent: ®istry::Agent, on_line: impl FnMut(&str)) -> anyhow::Result<()> {
let installed = agent.install(&settings::data_dir()?, on_line)?;
let entry = Agent {
name: agent.name.clone(),
id: Some(agent.id.clone()),
command: installed.command,
args: installed.args,
env: installed.env,
};
let package = match &agent.distribution {
Distribution::Npm { package, .. } => Some(cacp_agents::package_name(package)),
_ => None,
};
settings::put_agent(&entry, package)
}
pub fn remove(id: &str) -> anyhow::Result<()> {
Installed::remove(&settings::data_dir()?, id)?;
settings::remove_agent(id)
}
#[cfg(test)]
#[path = "../../tests/unit/agent_trust.rs"]
mod trust_tests;