Skip to main content

aether_cli/agent/
list.rs

1use aether_project::{AetherSettings, AgentConfig};
2use crossterm::style::Stylize;
3use std::fs;
4
5use crate::agent::ListArgs;
6use crate::error::CliError;
7
8pub fn run_list(args: ListArgs) -> Result<(), CliError> {
9    let project_root = args.path.canonicalize().unwrap_or(args.path);
10    let settings_path = project_root.join(".aether/settings.json");
11
12    let content = match fs::read_to_string(&settings_path) {
13        Ok(c) => c,
14        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
15            println!("No agents found. Run `aether agent new` to create one.");
16            return Ok(());
17        }
18        Err(e) => return Err(CliError::IoError(e)),
19    };
20
21    let config: AetherSettings =
22        serde_json::from_str(&content).map_err(|e| CliError::AgentError(format!("Failed to parse settings: {e}")))?;
23
24    if config.agents.is_empty() {
25        println!("No agents found. Run `aether agent new` to create one.");
26        return Ok(());
27    }
28
29    let mut sorted: Vec<&AgentConfig> = config.agents.iter().collect();
30    sorted.sort_by(|a, b| a.name.cmp(&b.name));
31
32    for (i, agent) in sorted.iter().enumerate() {
33        if i > 0 {
34            println!();
35        }
36        print_agent(agent, &config.prompts, &config.mcps);
37    }
38
39    Ok(())
40}
41
42fn print_agent(
43    agent: &AgentConfig,
44    default_prompts: &[aether_project::PromptSource],
45    default_mcps: &[aether_project::McpSourceSpec],
46) {
47    println!("{}", agent.name.as_str().bold().cyan());
48    println!("  {}       {}", "model:".dim(), agent.model);
49
50    let reasoning = agent.reasoning_effort.as_ref().map_or("none".to_string(), std::string::ToString::to_string);
51    println!("  {}   {reasoning}", "reasoning:".dim());
52
53    println!("  {} {}", "description:".dim(), agent.description);
54
55    let mut surfaces = Vec::new();
56    if agent.user_invocable {
57        surfaces.push("user");
58    }
59    if agent.agent_invocable {
60        surfaces.push("agent");
61    }
62    println!("  {}   {}", "invocable:".dim(), surfaces.join(", "));
63
64    let prompts = if agent.prompts.is_empty() { default_prompts } else { &agent.prompts };
65    if !prompts.is_empty() {
66        println!(
67            "  {}     {}",
68            "prompts:".dim(),
69            prompts.iter().filter_map(aether_project::PromptSource::path).collect::<Vec<_>>().join(", ")
70        );
71    }
72
73    let mcps = if agent.mcps.is_empty() { default_mcps } else { &agent.mcps };
74    if !mcps.is_empty() {
75        println!(
76            "  {} {}",
77            "mcp servers:".dim(),
78            mcps.iter().filter_map(aether_project::McpSourceSpec::path).collect::<Vec<_>>().join(", ")
79        );
80    }
81
82    if !agent.tools.allow.is_empty() {
83        println!("  {} {}", "tools allow:".dim(), agent.tools.allow.join(", "));
84    }
85    if !agent.tools.deny.is_empty() {
86        println!("  {}  {}", "tools deny:".dim(), agent.tools.deny.join(", "));
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93    use crate::agent::new_agent_wizard::{DraftAgentEntry, build_system_md, scaffold};
94
95    #[test]
96    fn list_empty_project() {
97        let dir = tempfile::tempdir().unwrap();
98        let args = super::super::ListArgs { path: dir.path().to_path_buf() };
99        run_list(args).unwrap();
100    }
101
102    #[test]
103    fn list_project_with_agents() {
104        let dir = tempfile::tempdir().unwrap();
105        scaffold(dir.path(), &test_draft()).unwrap();
106        let args = super::super::ListArgs { path: dir.path().to_path_buf() };
107        run_list(args).unwrap();
108    }
109
110    fn test_draft() -> DraftAgentEntry {
111        let mut draft = DraftAgentEntry {
112            entry: AgentConfig {
113                name: "Coder".to_string(),
114                description: "A coding agent".to_string(),
115                user_invocable: true,
116                agent_invocable: true,
117                model: "anthropic:claude-sonnet-4-5".to_string(),
118                prompts: vec![aether_project::PromptSource::file("AGENTS.md")],
119                ..AgentConfig::default()
120            },
121            system_md_content: String::new(),
122            system_md_edited: false,
123            selected_mcp_servers: vec!["coding".into()],
124            workspace_mcp_configs: vec![],
125        };
126        draft.system_md_content = build_system_md(&draft);
127        draft
128    }
129}