mars_agents/cli/
agents.rs1use 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#[derive(Debug, clap::Args)]
19pub struct AgentsArgs {
20 #[arg(long)]
22 pub mode: Option<String>,
23
24 #[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 {
36 name: String,
38 },
39}
40
41pub 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 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 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 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}