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 mcp;
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)
}
const ALLOWED: [&str; 7] = [
"claude-acp", "codex-acp", "cursor", "gemini", "antigravity-acp", "kimi", "opencode", ];
fn listed(agent: ®istry::Agent) -> bool {
ALLOWED.contains(&agent.id.as_str())
}
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()
.filter(listed)
.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 !listed(agent) {
continue;
}
if let Some(url) = agent.icon.as_deref() {
fetch(&dir, &agent.id, url);
}
}
}
pub fn install(agent: ®istry::Agent) -> anyhow::Result<()> {
let installed = agent.install(&settings::data_dir()?, |_| {})?;
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)
}