Skip to main content

mars_agents/cli/
agents.rs

1//! `mars agents` — list and inspect agents from the .mars/ canonical store.
2
3use crate::compiler::agents::{parse_agent_content, parse_agent_profile};
4use crate::error::MarsError;
5use crate::frontmatter;
6use crate::lock::ItemKind;
7
8use super::output;
9
10#[derive(serde::Serialize)]
11struct AgentEntry {
12    name: String,
13    description: String,
14    mode: String,
15}
16
17/// Arguments for `mars agents`.
18#[derive(Debug, clap::Args)]
19pub struct AgentsArgs {
20    /// Filter by mode (primary or subagent).
21    #[arg(long)]
22    pub mode: Option<String>,
23
24    /// Filter by source name.
25    #[arg(long)]
26    pub source: Option<String>,
27
28    #[command(subcommand)]
29    pub command: Option<AgentsCommand>,
30}
31
32#[derive(Debug, clap::Subcommand)]
33pub enum AgentsCommand {
34    /// Show full metadata for a named agent.
35    Show {
36        /// Agent name.
37        name: String,
38    },
39}
40
41/// Run `mars agents`.
42pub fn run(args: &AgentsArgs, ctx: &super::MarsContext, json: bool) -> Result<i32, MarsError> {
43    match &args.command {
44        Some(AgentsCommand::Show { name }) => run_show(name, ctx, json),
45        None => run_list(args, ctx, json),
46    }
47}
48
49fn run_list(args: &AgentsArgs, ctx: &super::MarsContext, json: bool) -> Result<i32, MarsError> {
50    let lock = crate::lock::load(&ctx.project_root)?;
51    let mars_dir = ctx.project_root.join(".mars");
52
53    let mut entries: Vec<AgentEntry> = Vec::new();
54
55    for (dest_path, item) in lock.canonical_flat_items() {
56        if item.kind != ItemKind::Agent {
57            continue;
58        }
59
60        // source filter
61        if let Some(ref filter_source) = args.source
62            && item.source != *filter_source
63        {
64            continue;
65        }
66
67        let disk_path = dest_path.resolve(&mars_dir);
68        let content = match std::fs::read_to_string(&disk_path) {
69            Ok(c) => c,
70            Err(err) => {
71                eprintln!("warning: skipping {}: {err}", disk_path.display());
72                continue;
73            }
74        };
75
76        let fm = match frontmatter::parse(&content) {
77            Ok(fm) => fm,
78            Err(err) => {
79                eprintln!("warning: skipping {}: {err}", disk_path.display());
80                continue;
81            }
82        };
83
84        let mut diags = Vec::new();
85        let profile = parse_agent_profile(&fm, &mut diags);
86
87        // mode filter
88        let mode_str = match &profile.mode {
89            Some(m) => m.as_str().to_string(),
90            None => String::new(),
91        };
92        if let Some(ref filter_mode) = args.mode
93            && mode_str != *filter_mode
94        {
95            continue;
96        }
97
98        let name = profile
99            .name
100            .clone()
101            .unwrap_or_else(|| path_stem(&disk_path));
102        let description = profile.description.clone().unwrap_or_default();
103
104        entries.push(AgentEntry {
105            name,
106            description,
107            mode: mode_str,
108        });
109    }
110
111    entries.sort_by(|a, b| a.name.cmp(&b.name));
112
113    if json {
114        output::print_json(&serde_json::json!({ "agents": entries }));
115    } else {
116        if entries.is_empty() {
117            println!("  no agents");
118        } else {
119            // Compute column widths
120            let name_w = entries
121                .iter()
122                .map(|e| e.name.len())
123                .max()
124                .unwrap_or(4)
125                .max(4);
126            let mode_w = entries
127                .iter()
128                .map(|e| e.mode.len())
129                .max()
130                .unwrap_or(4)
131                .max(4);
132            println!("{:<name_w$}  {:<mode_w$}  DESCRIPTION", "NAME", "MODE");
133            for e in &entries {
134                println!(
135                    "{:<name_w$}  {:<mode_w$}  {}",
136                    e.name, e.mode, e.description
137                );
138            }
139        }
140    }
141
142    Ok(0)
143}
144
145fn run_show(name: &str, ctx: &super::MarsContext, json: bool) -> Result<i32, MarsError> {
146    let lock = crate::lock::load(&ctx.project_root)?;
147    let mars_dir = ctx.project_root.join(".mars");
148
149    for (dest_path, item) in lock.canonical_flat_items() {
150        if item.kind != ItemKind::Agent {
151            continue;
152        }
153
154        let disk_path = dest_path.resolve(&mars_dir);
155        let content = match std::fs::read_to_string(&disk_path) {
156            Ok(c) => c,
157            Err(err) => {
158                eprintln!("warning: skipping {}: {err}", disk_path.display());
159                continue;
160            }
161        };
162
163        let mut diags = Vec::new();
164        let (profile, _fm) = match parse_agent_content(&content, &mut diags) {
165            Ok(p) => p,
166            Err(err) => {
167                eprintln!("warning: skipping {}: {err}", disk_path.display());
168                continue;
169            }
170        };
171
172        let stem = path_stem(&disk_path);
173        let agent_name = profile.name.as_deref().unwrap_or(stem.as_str());
174        if !agent_name.eq_ignore_ascii_case(name) {
175            continue;
176        }
177
178        let mode_str = profile.mode.as_ref().map(|m| m.as_str()).unwrap_or("");
179        let harness_str = profile
180            .harness
181            .as_ref()
182            .map(|h| h.to_harness_id().as_str())
183            .unwrap_or("");
184        let model_str = profile.model.as_deref().unwrap_or("");
185        let approval_str = profile
186            .approval
187            .as_ref()
188            .map(|a| a.as_str())
189            .unwrap_or_default();
190        let sandbox_str = profile
191            .sandbox
192            .as_ref()
193            .map(|s| s.as_str())
194            .unwrap_or_default();
195        let effort_str = profile
196            .effort
197            .as_ref()
198            .map(|e| e.as_str())
199            .unwrap_or_default();
200        let description_str = profile.description.as_deref().unwrap_or("");
201
202        if json {
203            output::print_json(&serde_json::json!({
204                "name": agent_name,
205                "description": description_str,
206                "mode": mode_str,
207                "harness": harness_str,
208                "model": model_str,
209                "skills": profile.skills,
210                "subagents": profile.subagents,
211                "approval": approval_str,
212                "sandbox": sandbox_str,
213                "effort": effort_str,
214                "tools": profile.tools,
215                "disallowed-tools": profile.disallowed_tools,
216                "tools-denied": profile.tools_denied,
217                "mcp-tools": profile.mcp_tools,
218            }));
219        } else {
220            println!("name:        {agent_name}");
221            println!("description: {description_str}");
222            println!("mode:        {mode_str}");
223            println!("harness:     {harness_str}");
224            println!("model:       {model_str}");
225            println!("approval:    {approval_str}");
226            println!("sandbox:     {sandbox_str}");
227            println!("effort:      {effort_str}");
228            print_str_list("skills", &profile.skills);
229            print_str_list("subagents", &profile.subagents);
230            print_str_list("tools", &profile.tools);
231            print_str_list("disallowed-tools", &profile.disallowed_tools);
232            print_str_list("tools-denied", &profile.tools_denied);
233            print_str_list("mcp-tools", &profile.mcp_tools);
234        }
235
236        return Ok(0);
237    }
238
239    eprintln!("error: agent `{name}` not found");
240    Ok(1)
241}
242
243fn print_str_list(label: &str, items: &[String]) {
244    if items.is_empty() {
245        println!("{label}:        (none)");
246    } else {
247        println!("{label}:        {}", items.join(", "));
248    }
249}
250
251fn path_stem(path: &std::path::Path) -> String {
252    path.file_stem()
253        .and_then(|s| s.to_str())
254        .unwrap_or("unknown")
255        .to_string()
256}