agent-manager v0.2.0 : gestionnaire d'agents IA locaux

- CLI Rust (am) : list/install/uninstall/update/start/stop/restart/run/status/search/info/config/doctor/completion/export/import/self-update + shell interactif (Tab, historique)
- 9 methodes d'installation (npm, pip, uv, cargo, go, bun, curl, binary, git)
- catalogue de 72 agents, dependances, processus, securite (dry-run, checksums)
- sondes de version paralleles + cache (am list ~25 ms a chaud)
- installateurs one-liner : install.sh, install.ps1
This commit is contained in:
2026-08-15 11:26:34 -04:00
commit 8ee3ddf3db
79 changed files with 15724 additions and 0 deletions
+159
View File
@@ -0,0 +1,159 @@
//! The merged agent catalog: lookup, aliases, groups and search.
use crate::config::AgentDef;
use std::collections::{BTreeMap, HashMap};
#[derive(Debug, Clone, Default)]
pub struct Catalog {
agents: Vec<AgentDef>,
by_name: HashMap<String, usize>,
aliases: BTreeMap<String, String>,
groups: BTreeMap<String, Vec<String>>,
}
impl Catalog {
pub fn new(
agents: Vec<AgentDef>,
aliases: BTreeMap<String, String>,
groups: BTreeMap<String, Vec<String>>,
) -> Self {
let mut by_name = HashMap::new();
for (i, a) in agents.iter().enumerate() {
by_name.insert(a.name.clone(), i);
}
Self {
agents,
by_name,
aliases,
groups,
}
}
pub fn agents(&self) -> &[AgentDef] {
&self.agents
}
pub fn len(&self) -> usize {
self.agents.len()
}
pub fn is_empty(&self) -> bool {
self.agents.is_empty()
}
pub fn get(&self, name: &str) -> Option<&AgentDef> {
self.by_name.get(name).map(|&i| &self.agents[i])
}
/// Resolve an agent name or an alias to the agent definition.
pub fn resolve(&self, name_or_alias: &str) -> Option<&AgentDef> {
if let Some(target) = self.aliases.get(name_or_alias) {
return self.get(target);
}
self.get(name_or_alias)
}
pub fn alias_target(&self, alias: &str) -> Option<&str> {
self.aliases.get(alias).map(|s| s.as_str())
}
/// Members of a group, in definition order (missing names are skipped).
pub fn group_members(&self, group: &str) -> Vec<&AgentDef> {
let mut out = Vec::new();
if let Some(names) = self.groups.get(group) {
for n in names {
if let Some(a) = self.get(n) {
out.push(a);
}
}
}
out
}
pub fn has_group(&self, group: &str) -> bool {
self.groups.contains_key(group)
}
/// Is this token a group selector ("group:<name>")?
pub fn parse_group_selector(s: &str) -> Option<&str> {
s.strip_prefix("group:")
}
/// Case-insensitive search over name, display name, description,
/// category, tags and note.
pub fn search(&self, keyword: &str, category: Option<&str>) -> Vec<&AgentDef> {
let kw = keyword.to_lowercase();
self.agents
.iter()
.filter(|a| {
if a.hidden {
return false;
}
if let Some(cat) = category {
if a.category.as_deref() != Some(cat) {
return false;
}
}
let hay = format!(
"{} {} {} {} {} {}",
a.name,
a.title(),
a.description.as_deref().unwrap_or(""),
a.category.as_deref().unwrap_or(""),
a.tags.join(" "),
a.note.as_deref().unwrap_or("")
)
.to_lowercase();
hay.contains(&kw)
})
.collect()
}
/// Sorted list of all categories present in the catalog.
pub fn categories(&self) -> Vec<String> {
let mut cats: Vec<String> = self
.agents
.iter()
.filter_map(|a| a.category.clone())
.collect();
cats.sort();
cats.dedup();
cats
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::Config;
fn catalog() -> Catalog {
let cfg: Config = serde_yaml::from_str(crate::config::DEFAULT_CONFIG).unwrap();
Catalog::new(cfg.agents, cfg.aliases, cfg.groups)
}
#[test]
fn resolves_aliases() {
let cat = catalog();
let via_alias = cat.resolve("cc").map(|a| a.name.clone());
assert_eq!(via_alias.as_deref(), Some("claude-code"));
}
#[test]
fn resolves_groups() {
let cat = catalog();
assert!(cat.has_group("dev"));
let members = cat.group_members("dev");
assert!(!members.is_empty());
assert!(members.iter().any(|a| a.name == "claude-code"));
}
#[test]
fn searches() {
let cat = catalog();
let hits = cat.search("anthropic", None);
assert!(hits.iter().any(|a| a.name == "claude-code"));
let cats = cat.categories();
assert!(cats.contains(&"coding-agent".to_string()));
}
}